안드로이드 연구소

[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(2) - onCreateViewHolder 최적화 본문

안드로이드 연구소

[리싸이클러뷰 박사 교육과정] 리싸이클러뷰 최적화(2) - onCreateViewHolder 최적화

안드로이드 연구원 2023. 5. 8. 19:56

지난번에는 리싸이클러뷰를 최적하기 위해서 onBindViewHolder최적화하는 방법을 알아보았습니다.

 

오늘도 이어서 리싸이클러뷰를 최적화할 수 있는 여러 방법들을 알아보겠습니다.

 

아래 내용들은 지난 내용들과 마찬가지로 선배님의 블로그를 바탕으로

저 자신만의 리싸이클러뷰를 만드는데 집중하였습니다.

https://gift123.tistory.com/67

 

안드로이드 개발 (34) RecyclerView 성능 향상

Android 개발을 하다보면 불가피 하게 RecyclerView를 사용해야 하는 순간이 대부분 옵니다. 요새 Compose열풍이 휩쓸고 있지만 아직은 회사에서 사용하는 목록형 UI는 RecyclerView를 통해 만들었을 겁니다

gift123.tistory.com

 

Q1. LayoutManager() 최적화하는 방법을 알려줘

 

방법1. PreCacheLayoutManager 사용

PreCacheLayoutManager는 항목이 항목의 크기와 위치를 미리 계산하여

스크롤할 항목을 미리 로드하고 미리 그립니다.

RecyclerView에 많은 수의 항목이 포함되어 있는 시나리오에서 특히 유용합니다.

 

아래의 예제와 같이 사용하면

아직 리싸이클러뷰 화면 표시되지 않은 최대 600px까지 먼저 그려놓습니다.

이를 통해 600px안에 생성된 뷰홀더는 스크롤할 때 지연이나 느린 렌더링을 방지할 수 있습니다.

val layoutManager = PreCacheLayoutManager(this,600)
layoutManager.orientation = LinearLayoutManager.VERTICAL
recyclerView.layoutManager = layoutManager

만약 기본값인 PreCacheLayoutManager(this)로 픽셀 값 없이 사용한다면

화면에 보이는 리싸이클러뷰의 높이를 계산하여 

1.5가 곱한 값이 들어가기 때문에 적정 수치가 고려된 값을 사용할 수 있습니다.

val layoutManager = PreCacheLayoutManager(this)
layoutManager.orientation = LinearLayoutManager.VERTICAL
recyclerView.layoutManager = layoutManager

값이 클수록 더 많은 항목을 캐시하여 렌더링에는 유리하지만,

더 많은 메모리와 처리 능력을 사용하게되어 부하가 걸릴 수 도 있습니다.

성능과 메모리 사용량의 균형을 맞추는 값을 선택하는 것이 중요합니다.

 

 

방법2. LayoutManager.setItemPrefetchEnabled

.setItemPrefetchEnabled(true)는 RecyclerView가 미리 항목을 가져오고 배치하여

부드로운 스크롤할 수 있도록 하는 기능입니다.

디폴트가 true여서 따로 세팅할 필요는 없다.

LayoutManager.setItemPrefetchEnabled(true)

미리 보기할 항목 갯수를 조정할 수 도 있다.

.setInitialPrefetchItemCount(10)을 사용하여

디폴트 2개의 미리 보기 항목수를 10개로 늘릴 수 도 있다.

val layoutManager = LinearLayoutManager(context)
layoutManager.setItemPrefetchEnabled(true)
layoutManager.setInitialPrefetchItemCount(10)
recyclerView.layoutManager = layoutManager

이 또한 수치를 많이 늘리게 되면 메모리 크기 또한 늘어나 부하가 발생할 수 있음으로

전체 데이터 갯수가 적어 스크롤이 적게하는 리싸이클러뷰일 경우 false를한다.

val layoutManager = LinearLayoutManager(context)
layoutManager.setItemPrefetchEnabled(false)
recyclerView.layoutManager = layoutManager

 

 

방법3. AsyncLayourInflater로 비동기 백그라운드에서 뷰홀더 생성

비동기 백그라운드에서

뷰홀더를 생성하여 onCreateViewHolder()의 부담을 줄이는 방식

이는 리스트에 새로운 아이템이 insert되는 경우에 사용된다.

 

AsyncLayoutInflater는 Deprecated되어 AsyncLayourInflater로 대체하여 사용하였습니다.

class SmoothListAdapter(val context: Context) :
    ListAdapter<SmoothListAdapter.ListItem, SmoothListAdapter.ListItemViewHolder>(ListItemViewHolder.MyDiffCallback()) {

    data class ListItem(val id: String, val text: String)

    class ListItemViewHolder(view: View) : RecyclerView.ViewHolder(view) {

        private val asyncLayoutInflater = AsyncLayoutInflaterCompat(context)
        private val cachedViews = Stack<View>()
        
        companion object {
            const val NUM_CACHED_VIEWS = 5
        }
        

        init {
            // 비동기 백그라운드에서 뷰 생성 후 스택에 쌓음
            for (i in 0..NUM_CACHED_VIEWS) {
                asyncLayoutInflater.inflate(R.layout.list_item, null, object : AsyncLayoutInflaterCompat.OnInflateFinishedListener {
                        override fun onInflateFinished(view: View, layoutResId: Int, parent: ViewGroup?) {
                            cachedViews.push(view)
                        }
                })
            }
        }

        override fun onCreateViewHolder(parent: ViewGroup,viewType: Int): ListItemViewHolder {
            // 스택이 비어있을 경우 새로운 뷰를 생성하기
            val view = if (cachedViews.isEmpty()) {
                LayoutInflater.from(context).inflate(R.layout.list_item, parent, false)
            } 
            // 스택에 있을 경우 최근에 저장된 뷰 pop으로 꺼내기
            else {
                cachedViews.pop().also {
                    it.layoutParams = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.WRAP_CONTENT)
                }
            }
            return ListItemViewHolder(view)
        }

        // ListItem 개체를 사용하고 적절한 데이터로 ListItemViewHolder의 뷰를 채웁니다
        fun populateFrom(listItem: ListItem) {
            //TODO: populate your view
        }

        // 뷰홀더를 사용하여 populateFrom호출
        override fun onBindViewHolder(viewHolder: ListItemViewHolder, position: Int) = viewHolder.populateFrom(getItem(position))

        // 변경된 데이터 파악 후 자동으로 부분 변경하는 유틸리티 
        class MyDiffCallback : DiffUtil.ItemCallback<ListItem>() {
            override fun areItemsTheSame(firstItem: ListItem, secondItem: ListItem) = firstItem.id == secondItem.id

            override fun areContentsTheSame(firstItem: ListItem, secondItem: ListItem) = firstItem == secondItem
        }
    }
}

 

 

방법4. 코루틴로 비동기 백그라운드에서 뷰홀더 생성 후 바인딩까지

비동기 백그라운드에서

뷰홀더를 생성하여 onCreateViewHolder()의 부담을 줄이고

또 뷰홀더에 데이터를 바인딩하여 onBindingViewHolder()또한 부담을 줄이는 방식

open class AsyncCell(context: Context) : FrameLayout(context, null, 0, 0) {
    init {
        layoutParams = LayoutParams(MATCH_PARENT, WRAP_CONTENT)
    }

    open val layoutId = -1

    // override with your layout Id 
    private var isInflated = false
    private val bindingFunctions: MutableList<AsyncCell.() -> Unit> = mutableListOf()

    fun inflate() {
        GlobalScope.launch(Dispatchers.Main) {
            val view = withContext(Dispatchers.IO) {
                LayoutInflater.from(context).inflate(layoutId, this@AsyncCell, false)
            }
            isInflated = true
            addView(createDataBindingView(view))
            bindView()
        }
    }

    private fun bindView() {
        with(bindingFunctions) {
            forEach { it() }
            clear()
        }
    }

    fun bindWhenInflated(bindFunc: AsyncCell.() -> Unit) {
        if (isInflated) {
            bindFunc()
        } else {
            bindingFunctions.add(bindFunc)
        }
    }

    // override for usage with dataBinding
    open fun createDataBindingView(view: View): View? = view
}
class RecyclerViewAsyncAdapter(private val items: List<TestItem>) :
    RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
        SmallItemViewHolder(SmallItemCell(parent.context).apply { inflate() })

    override fun getItemCount(): Int = items.size
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is SmallItemViewHolder) {
            setUpSmallViewHolder(holder, position)
        }
    }

    private fun setUpLargeViewHolder(holder: LargeItemViewHolder, position: Int) {
        (holder.itemView as LargeItemCell).bindWhenInflated {
            items[position].let { item ->
                holder.itemView.binding?.item = item
            }
        }
    }

    private fun setUpSmallViewHolder(holder: SmallItemViewHolder, position: Int) {
        (holder.itemView as SmallItemCell).bindWhenInflated {
            items[position].let { item ->
                holder.itemView.binding?.item = item
            }
        }
    }

    private inner class SmallItemViewHolder internal constructor(view: ViewGroup) : RecyclerView.ViewHolder(view)
    
    private inner class SmallItemCell(context: Context) : AsyncCell(context) {
        var binding: SmallItemCellBinding? = null override
        val layoutId =
            R.layout.small_item_cell override fun createDataBindingView(view: View): View? {
                binding = SmallItemCellBinding.bind(view) return view.rootView
            }
    }
}
Comments