RecyclerView Prefetch – 스크롤을 부 드럽게 하기위한 시도

RecyclerView Prefetch를 번역한 글입니다.

내가 어렸을 때, 어머니께서 지금 방청소를 하면 나중에 할 필요가 없다며 버릇을 고치려고 하였습니다. 하지만 나는 그것에 속지 않았습니다. 나는 가능한 오랫동안 하지 않는 것이 항상 최선이라는 것을 알았습니다. 한 가지 생각해보면, 만약 청소를 지금 당장 하게 된다면 다시 더러워질 것이고 청소를 두 번 해야 됩니다. 만약에 더 오랫동안 미룬다면 어머니는 잊어버릴 수도 있습니다.

미루는 버릇은 항상 나의 일이었습니다. 하지만 내 친구 인 RecyclerView는 일관된 프레임 속도에 대한 문제가 없습니다.

 

문제점

스크롤이나 플링 연산중 RecyclerView는 화면에 새로운 아이템 정보를 표시해야 합니다. 이러한 아이템은 데이터와 바인딩합니다. (캐시에 항목이 없는 경우 새롭게 생성될 수 있습니다.) 그런 다음 배치되며 화면에 그려지게 됩니다. 이 모든 작업이 완료되면, 프레임에서 처리할 작업이 완료될 때까지 UI스레드는 멈춰있게 됩니다. 그러면 렌더링이 진행되고 스크롤은 스무스하게 움직일 수 있습니다… 다음 새로운 아이템이 나타날 때까지

일반적으로 RecyclerView에서 콘텐츠를 스크롤하는 동안 렌더링 하는 단계입니다 (롤리팝 이상에서). UI스레드에서 입력 이벤트, 이벤트 핸들링, 레이아웃을 수행하고 드로잉 연산 작업을 기록합니다. 그런 다음 렌더스레드가 명령을 GPU로 보냅니다.

스크롤하는 동안의 프레임은 처리할 새로운 내용이 없으므로 RecyclerView는 필요한 작업을 수행하는데 아무런 문제가 없습니다. 이 프레임 동안, UI스레드 입력을 처리하고, 애니메이션을 처리하며 레이아웃을 수행하고 드로잉 명령을 기록합니다. 그런 다음 드로잉 정보를 렌더스레드와 동기화합니다. (롤리팝 이상, 이전 버전은 UI 스레드에서 모든 작업을 수행함) 이러한 명령을 GPU로 보냅니다.

새로운 아이템 생성 시 뷰 생성, 바인딩 및 배치될 때 입력 단계가 가장 오래 걸리게 합니다. 결과적으로 프레임 경계 이후에 끝나기 때문에 렌더링 단계가 늦어져 프레임 누락의 원인이 됩니다.

새로운 아이템이 스크린에 나타나면 입력 단계에 더 많은 작업이 필요하며, 적합한 뷰를 생성할 수 있습니다. 이것은 렌더스레드의 후속 작업뿐만 아니라 나머지 UI 스레드를 늦추어 프레임 범위를 벗어나기 때문에 무의미한 프레임이 됩니다.

입력을 받는 동안 새로운 아이템을 보여주기 위해 생성하고 바인딩 하는데 사용할 수 있는 많은 시간을 할당 할 수 있습니다.

새로운 아이템이 나타날 때 입력 단계에서 호출 스택을 검사하면 많은 시간이 뷰 생성 및 바인딩에 소비된다는 것을 알 수 있습니다. 새로운 아이템을 준비 하는 동안 다른 작업을 지연하지 않는 방법이 있으면 좋지 않을까요?

뷰 생성 및 바인딩은 렌더링 되기 전에 완료되어야 하며 필요한 경우 처리하는 동안 UI스레드에서는 많은 시간을 소비합니다. 이전 프레임에서 많은 시간을 보내고 있으면 지연이 발생됩니다.

Chris Craik(안드로이드 UI 툴킷팀, 그래픽 엔지니어)가 RecyclerView의 스크롤을 관측하기 위해 Systraces를 만들었습니다. 특히, 그는 아이템이 필요할 때 준비하는 시간이 오래 걸린다는 것을 알았습니다. 그리고 프레임을 처리는 동안에 UI스레드가 잠자는데 많은 시간을 보내고 있었습니다. 왜냐하면 UI스레드는 작업을 일찍 끝마치기 때문입니다.

 

해결책

생성과 바인드 연산을 이전 프레임에서 허용하면 UI스레드와 렌더스레드가 병렬로 작업을 수행할 수 있게 되며, 렌더스레드의 작업이 완료되기 전 동기적으로 수행하여 나중에 처리할 필요가 없습니다.

분명히 시간을 가지고 놀 시간이 없습니다. 특히 Chris는 기본 RecyclerView 레이아웃에서 일어나는 일을 재배열하여 필요한 View의 아이템을 미리 가져옵니다. 유휴 상태에서 이 작업을 수행하여 결과를 기다리는 것을 피할 수 있습니다.

이제 이 작업은 기본적으로 무료로 가능합니다. 유휴시간을 사용하여 나중에 해야 될 작업을 수행하기 때문에 UI스레드는 프레임들 사이의 갭에서 어떤 일도 하지 않습니다. 어려운 일이 이미 완료되었기 때문에 훤씬 빠른 속도로 프레임을 만들어 갈 수 있습니다.

 

세부 설명

이 시스템은 RecyclerView가 스크롤링 작업을 시작할 때마다 Runnable을 스케쥴링함으로써 작동합니다. 이 Runnable은 레이아웃 매니저와 뷰가 스크롤되는 방향에 따라 즉시 나타나며 아이템의 프리페치를 수행합니다.

프리페치는 단일 아이템에 국한되지 않습니다. GridLayoutManager 아이템의 행이 화면에 오는 등 여러 아이템을 한 번에 검색할 수 있습니다. 버전 25.1에서 프리페치 연산은 개별 생성/바인드 연산으로 분리되어 아이템별 전체 그룹의 연산보다 UI스레드 갭이 더 쉽게 구분됩니다.

프리 페치 방식에 대한 흥미로운 점 중 하나는 시스템에서 작업에 소요되는 시간과 사용 가능한 갭 내에 들어갈 수 있는지 여부를 예측해야 한다는 것입니다. 결국, 프리 페치 작업이 타이밍을 찾지 못하고 놓쳐 프레임을 지연시키게 되면 프리 페치가 없는 것과 동일하게 무의미한 프레임이 될 수 있습니다. 시스템이 이 세부 사항을 처리하는 방식은 View 타입별 평균 생성 및 바인드 지속 기간을 추적하여 향후 생성 및 바인드 연산에 대해 합리적인 예측을 가능하게 합니다.

내부 RecyclerView를 바인딩해도 자식을 할당하지 않기 때문에 충첩된 RecyclerViews(RecyclerView 내부 컨테이너 아이템)에 대해 작업을 수행하는 것이 더 까다롭습니다. RecyclerView는 첨부 및 레이아웃 될 때 자식을 가져옵니다. 프리 페치 시스템은 여전히 RecyclerView 내부에서 자식들을 준비할 수 있지만 얼마나 많은지 알고 있어야 합니다. LinerLayoutMananger의 버전 25.1에 있는 새로운 API인 setInitialPrefetchItemCount()입니다. 이 API는 화면에서 스크롤할 때 RecyclerView를 채우기 위해 미리 가져올 항목의 수를 시스템에 알려줍니다.

 

주의사항

알고 있어야 할 몇 가지 중요한 주의사항:

  • 프리페칭은 필요로 하지 않는 작업을 할 수도 있습니다. View를 프리페칭하고 있기 때문에 적극적으로 일을 할 수 있으며, RecyclerView는 문제 되는 아이템에 도달하지 않을 수도 있습니다. 이것은 프리페치 작업이 낭비될 가능성이 있다는 것을 의미합니다. 동시에 발생했기 때문에 큰 문제는 없습니다. 필요하기 직전에 인출하고 있기 때문에 이것은 매우 드뭅니다. 스크롤이 두 프레임 간 정지나 역전하는 일은 거의 없습니다.
  • 렌더스레드: 렌더스레드는 롤리팝에서 성능 향상을 위해 도입된 기능으로 렌더링을 UI스레드에 영향을 받지 않도록 다른 스레드로 분리하여 불변의 애니메이션(ex. 잔물결 및 원형틀)을 실행하는 등 일부 개선을 허용합니다. 롤리팝 이전 버전은 병렬로 작업을 처리할 수 없기 때문에 최적화의 이점은 얻지 못합니다.

 

무엇을 원하며  — 어디서 얻을 수 있는가?

프리페치 최적화는 서포트 라이브러리 25 버전에 도입되었으며, 25.1.0 버전에서 더욱 향상된 기능이 추가되었습니다. 따라서 첫 번째 단계는 최신의 서포트 라이브러리 버전을 사용하는 것입니다.

RecyclerView와 함께 기본으로 제공되는 레이아웃 매니저를 사용하면 최적화가 자동으로 수행됩니다. 그러나 중첩된 RecyclerView를 사용하거나 직접 만든 레이아웃 매니저의 경우 기능을 활용하려면 코드 변경이 필요합니다.

중첩된 RecyclerView의 경우, 최상의 성능을 얻으려면 내부 레이아웃 매니저에서 LinearLayoutManager의 새로운 메소드인 setInitialPrefetchItemCount()를 호출하면 됩니다. 예를 들어 세로 목록의 행에 최소 3개 이상이 표시되면 setInitialPrefetchItemCount(4)를 호출하면 됩니다.

직접 LayoutManager를 구현한 경우, 프리페치를 활성화하기 위해 LayoutManager.collectAdjacentPrefetchPositions()를 구현해야합니다.
언제나 그렇듯이 가능한 작은 작업으로 생성과 바인드 단계를 최적화하는 것이 좋습니다. 가장 빠른 코드는 프레임워크가 프리 페치를 통해 수행된 작업을 병렬 처리할 수 있을 때 실행될 필요가 없으며, 여전히 시간이 걸리는 비싼 아이템 생성은 비용이 들 수 있습니다. 예를 들어, 뷰를 최소한으로 써가며 복잡한 구조를 만드는 것보다 항상 생성하고 바인딩하는 것이 더욱 저렴합니다. 바인딩은 기본적으로 setter를 호출하는 것처럼 간단하고 빠릅니다. 현재 코드로 프레임 제한 시간까지 잘 맞출 수 있어도 이를 최적화하면 저사양 기기에서 더 잘 작동할 가능성이 커지며 고사양 기기에서는 배터리 소모에 도움이 됩니다.

이미 생성과 바인딩을 빨리 할 수 있더라도 프리페칭은 프레임들 사이의 틈새에서 남은 시간을 당기는 것에 대해 도와주어야 합니다. LayoutManager.setItemPrefetchEnabled()를 토글 하여 결과를 바탕으로 최적화를 비교해볼 수 있습니다. 결과를 시각적으로 볼 수도 있습니다. 정말 중요하며, 특히 생성하고 바인딩하는데 상당한 시간이 걸리는 아이템 있습니다. 프리페치 사용 여부와 상관없이 실시간으로 모니터링하고 싶다면 Systrace를 실행하거나 GPU 프로파일링을 활성화하면 됩니다.

Systrace는 UI스레드의 유휴 시간동안 발생하는 프리페치를 보여줍니다.

 

마치며..

최신 서포트 라이브러리를 확인하고 RecyclerView의 새로운 프리페치를 사용해보세요. 한편, 나는 방을 청소하지 않고 돌아가겠습니다.

 

댓글 남기기