hong's android

스크롤 버벅거림을 일으킬 수 있는 중첩 리사이클러뷰의 뷰풀 개선 본문

Projects/TroubleShooting

스크롤 버벅거림을 일으킬 수 있는 중첩 리사이클러뷰의 뷰풀 개선

_hong 2023. 5. 15. 15:12

프로젝트의 홈 화면 입니다. 리사이클러뷰들이 모두 같은 뷰 타입의 아이템들을 가지고 있습니다.

문제

현재 홈 화면의 리사이클러뷰들은 같은 뷰 타입의 아이템을 가지고 있지만 각각 뷰풀을 소유하고 있습니다. 이 때문에 스크롤을 내릴수록 뷰를 재사용 하지않고 새로운 뷰를 인플레이트를 하게 됩니다.

리사이클러뷰는 인플레이트를 통해 뷰를 생성하는데 이때 메인 스레드를 사용합니다. 만약 다른 애니메이션, 하단에 시리즈 아이템들이 많이 모여있는 카테고리들이 추가된다면 렌더링을 담당하는 메인 스레드에 비교적 많은 부담을 주게 됩니다.

이는 스크롤 버벅거림이나 사용자 경험에 직접적인 연결이 되어있는 부분이기에 개선하고자 했습니다.

리사이클러뷰 원리

출처 : Microsoft

1.저장

리사이클러뷰는 스크롤을 할 때 캐시에 뷰(Scrap View)를 저장하고 캐시가 가득 차면 바인드 되어야 하는 Dirty 뷰로 뷰풀에 저장됩니다.

2. 재사용

스크롤을 해서 아이템 뷰를 가져올 때 캐시에 원하는 아이템이 없으면 뷰홀더와 데이터를 바인드해서 가져옵니다. 만약 뷰풀에도 존재하지 않을 경우엔 인플레이트를 통해 뷰홀더를 생성합니다.

리사이클러뷰에 대해선 다음 링크에서 더 자세히 작성했습니다. 리사이클러뷰의 원리

💡 본론으로 돌아와서

리사이클러뷰는 캐시 또는 뷰풀을 통해 뷰를 재사용 합니다. 그러나 홈 화면의 리사이클러뷰들이 캐시 또는 뷰풀을 공유하지 않고 각각 가지고 있다면 상위 리사이클러뷰에서 재사용을 위해 저장된 뷰들을 하위 리사이클러뷰에서 재사용 하지 못합니다.

해결

인플레이트 횟수를 줄이기 위해선 뷰홀더의 재사용 비율을 높여야 했습니다.

1) 리사이클러뷰들은 캐시와 뷰풀 중 어떤 것을 공유 해야할까?

캐시는 아이템의 Position 별로 뷰를 재사용하고, 뷰풀은 뷰 타입 별로 뷰를 재사용 합니다. 그러니까 캐시는 데이터를 별도로 바인드할 필요도 없는 똑같은 아이템을 재사용하는 용도 입니다.

그러나 현재 문제는 리사이클러뷰에서 Position에 따라 동일한 뷰 (데이터를 바인드하지 않아도 되는 뷰) 를 재사용하지 않는 것이 아닌 다른 리사이클러뷰의 Dirty 뷰 (다른 데이터가 바인드 되어야하는 뷰) 를 재사용 하지 않는것 입니다. 그래서 뷰타입 별로 아이템을 재사용하는 뷰풀을 공유하는 것이 적절하다고 생각했습니다.

중첩된 자식 리사이클러뷰들이 뷰풀을 공유하도록 부모 리사이클러뷰에서 한 개의 뷰풀을 선언해서 공유하도록 변경했습니다.

// HomeListAdapter.kt
class HomeListAdapter : ListAdapter<CategoryListItem, BindingViewHolder<*>>(DiffCallback()) {

    override fun getItemViewType(position: Int): Int {
        val item = getItem(position)
        return item?.viewType?.ordinal ?: -1
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingViewHolder<*> {
        return ViewHolderGenerator.get(parent, viewType)
    }

    override fun onBindViewHolder(holder: BindingViewHolder<*>, position: Int) {
        val item = getItem(position)
        if (item != null) {
            holder.bind(item)
        }
    }
}

HomeListAdapter.kt 클래스에서 ViewHolderGenerator 클래스에게 뷰홀더 객체를 요청합니다.

// ViewHolderGenerator.kt
object ViewHolderGenerator {
    val horizontalViewPool = RecyclerView.RecycledViewPool()

    fun get(
        parent: ViewGroup,
        viewType: Int
    ): BindingViewHolder<*> {
        return when (viewType) {
            ViewType.VIEWPAGER.ordinal -> AdViewPagerViewHolder(parent.toBinding())
            ViewType.HORIZONTAL.ordinal -> HorizontalViewHolder(
                parent.toBinding(),
                sharedPool = horizontalViewPool
            )

            ViewType.AD.ordinal -> AdViewHolder(parent.toBinding())
            ViewType.Series.ordinal -> SeriesViewHolder(parent.toBinding())
            else -> ItemViewHolder(parent.toBinding())
        }
    }
    ...
}

ViewHolderGenerator.kt는 뷰타입에 따라 뷰홀더를 생성해주는 Object 클래스 입니다. 뷰타입이 같은 뷰들은 공유된 뷰풀 (ex horizontalViewPool) 을 사용하게 됩니다.


변경 전

변경 후

 

onCreateViewHolder()에 로그를 추가해서 확인해 본 결과 새로운 아이템 뷰를 인플레이트하는 횟수는 기존에 26번 이였지만, 뷰풀에서 뷰 홀더들을 공유한 후엔 21번으로 줄어들었습니다.

약 2 ~ 30% 가량의 뷰를 재사용 할 수 있었습니다 😀

 

 

 

'Projects > TroubleShooting' 카테고리의 다른 글

로컬 캐싱을 통한 로딩 시간 단축  (0) 2023.05.15