본문 바로가기

Android

[Android] RecyclerView and DiffUtil

Recycler view는 말 그대로 view를 재활용 한다는 측면에서 일반 ListView 보다 부하가 적고 효율적으로 동작한다는것을 알 수 있다! 그렇다면 어떤식으로 View를 재활용하는지 동작 원리를 알아보고, DiffUtil을 사용하여 쉽게 RecyclerView를 사용하는 방법을 알아보자!

Recycler View의 동작 원리 알아보기

RecyclerView 주요 컴포넌트 4가지

  1. RecyclerView.Adapter : RecyclerView에 각 itemView에 바인딩을 한다. adapter는 각 item-view의 위치를 연관된 data 위치에 연결 하는 방법을 알고 있다.
  2. RecyclerView.LayoutManager : RecyclerView내에 아이템을 배치, liner, grid layout등 여러가지 레이아웃 매니저를 사용할 수 있다.
  3. RecyclerView.ItemAnimator : 기본 애니메이션이 제공되고, 오버라이드 해서 필요에 따라 변경 가능
  4. RecyclerView.ViewHolder : 필수 항목, 화면에 그리고 싶은 개별적 item의 UI를 그릴 수 있도록 한다.

Recycler view의 두가지 view

1. Detached View (일시적으로 분리된 뷰)

  • 화면에 일시적으로 분리된뷰로 화면에 보여지고있는 뷰의 바로 위 아래 보이지 않는 뷰
  • 이러한 뷰는 Scrap Heap이라는 캐싱 시스템에 잠시 캐싱되며, 어뎁터에 다시 전달되지않고, 바인딩 없이 다시 layout manager로 직접 반환된다.
  • 이것이 가능한 이유는 데이터가 여전히 뷰 홀더에 연결되어있기 떄문
  • 따라서 데이터바인딩을 하기위해 어댑터에 다시 뷰를 전달 할 필요가 없다.
  • 화면밖으로 잠시 벗어나서 분리되지만 동일한 레이아웃을 그리며 재사용된다.

2. Dirty View (뷰를 표시하기 전에 어댑터에 의해 다시 바인딩 되어야하는 뷰)

  • 이전에 이미 생성된 view이면서, 새로운 아이템을 표시해서 재사용하기위한 view
  • Recycle pool 에서 뷰를 가져온다
  • 뷰홀더에 다시 연결하거나 바인딩 할 필요가 있기 때몬에 항상 어댑터에 전달된후 레이아웃 매니저로 반환된다.

동작 원리

1. DataSet(DataList)에 data가 보관된다

2. 어뎁터는 데이터를 뷰에 바인딩한 다음, 뷰를 제어하는 레이아웃 매니저에 제공

3. RecyclerView는 화면에 맞는 아이템 뷰의 수만 생성하고, 사용자가 스크롤 할 때 해당 아이템 레이아웃을 다시 재활용한다

4. 스크롤하여 화면 밖으로 벗어난 뷰는 재활용 프로세스를 거친다.

5. 보이지 않는 뷰는 scrap 뷰가 된다.

6. 새로 화면에 보여지는 아이템을 표시할경우 재사용하기위해 Recycle pool에서 뷰를 가져온다.

7. pool 에서 가져온 뷰는 dirty 뷰로 이전에 데이터가 남아있기때문에 어댑터에의해 다시 바인딩되어야한다.

  • recycle pool - view들을 캐싱하고있는 system으로 dirty view들을 캐싱하고있는 pool

RecyclerView.Adapter 구현하기

레이아웃 매니저가 호출하는 메소드

  • 레이아웃 매니저가 모든위치에서 적절한 뷰를 찾지 못하면 어댑터의 onCreatViewHolder를 호출하여 뷰를 생성
  • 그다음 onBindViewHolder를 통해 뷰를 바인딩하고 최종 리턴함

변경알림

  • notifyDataSetChanged, NotifyItemChanged, NotifyItemRemoved - 데이터 셋이 변경되었을때, 전체 업데이트, 지정된위치 업데이트 등을 알림

퍼포먼스 팁

  • RecyclerView.setHasFixedSize(true) : 리스트의 각 아이템의 높이와 너비가 콘텐츠에 따라 변경되지 않는경우는 해당 함수에 true를 명시하여 아이템 크기 계산을 막아 연산을 빠르게 한다.
  • RecyclerView.setItemViewCacheSize(size) : recycle pool에 추가하기 전에 유지할 뷰의 수를 설정..

DiffUtil은 무엇인가?

DiffUtil은 RecyclerView의 성능 향상을 도와주는 유틸리티 클래스로 data set 두 목록 간의 차이점을 편리하게 알려어 최소한의 UI갱신을 한다 .

 

RecyclerView.Adapter를 이용하여 recycler view를 구현했을때 데이터 변화가 있었을때 notifyDataSetChanged() 혹은 notifyItemRangeChanged() 등등을 이용하여 데이터 변화를 adapter에 notify하고, adapter는 알림을 기반으로, 해당되는 리스트 item을 새로 생성하는 과정을 수행한다. 그런데! 데이터가 전체적인 수정이 아닌 부분만 수정했을때도 notifyDataSetChanged()를 호출하여 리스트 전체를 다시 그리는것은 큰 비효율이다!!!!!

 

그래서 이를 보완하기 위해 notifyItemRangeChanged 등을 사용할 수 있지만, 어떤 데이터가 수정되었는 때 어떤범위에서 수정이되었는지 알지 못하는 경우도 있을뿐 아니라, 동적으로 프로그램중 range를 파악하거나 해야하는경우 올바르지 못하게 range를 지정하는등 문제가 발생할 수 있다! 이런경우 사용할수있는것이 DiffUtil이다!

 

Diff Util은 두가지 요소간의 차이를 계산하고 변경이 필요한 부분만 내부적으로 notifyItemRangeChanged, 등을 알아서 호출하여 최소한의 갱신만 할 수 있도록 보장한다.

 

DiffUtil 알고리즘

Diff Util은 Eugene W. Myers difference 라는 알고리즘을 사용한다. 이 알고리즘은 두가지 sets의 요소들간의 차이를 계산한다. 요소가 같은데 순서가다른 경우 두번째 단계로 넘어가 한목록을 다른목록으로 반환하는 최단 경로를 찾는다고한다.

 

DiffUtil 사용하여 ListAdapter 구현하기

  • viewBinding을 사용한 예제. viewHolder의 binding은 해당 view의 layout
  • 각 parameter들 이름을 좀 명시적으로 보여주기위해서 줄줄이 지었다..
class MyAdapter: ListAdapter<ListItemModelClass, MyAdapter.MyItemViewHolder>(myDiffUtil) {

	inner class MyItemViewHolder(private val binding: ItemViewBinding): RecyclerView.ViewHolder(binding.root) {
			fun binding(item: ListItemModelClass) {
					binding.textView.text = item.title				
			}
	}

	override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookItemViweHolder {
			return MyItemViewHolder(ItemViewBinding.inflate(LayoutInflater.from(parent.context), parent, false))
	}

	override fun onBindViewHolder(holder: BookItemViewHolder, position: Int) {
			holder.bind(currentList[position])
	}

	companion object {
		val myDiffUtil = object: DiffUtil.ItemCallback<ListItemModelClass>() {
			override fun areItemsTheSame(oldItem: ListItemModelClass, newItem: ListItemModelClass): Boolean {
					return oldItem == newItem
			}

			override fun areContentsTheSame(oldItem: ListItemModelClass, newItem: ListItemModelClass): Boolean {
					return oldItem.id == newItem.id
			}
		}
	}
}
  • ListItemModelClass : 리스트 아이템에 해당하는 모델 클래스
  • MyItemViewHolder : 뷰홀더 클래스 (Adapter의 inner class로 생성했기 떄문에 adapter.)
  • DiffUtil: 생성한 DiffUtil
  • ItemViewBinding : 아이템 view에 해당하는 view의 layout

DiffUtil 과정 ..(?)

  1. DiffUtil.ItemCallback 은 두개의 리스트간 차이를 계산해주는 역할을 한다 . OS는 필드를 모르기 때문에 정보를 제공해주기 위해서 areItemsTheSame, areContentsTheSame을 오버라이딩 해야한다.
  2. areContentsTheSame 에서 각 아이템을 구분할 수 있는 유니크한 값들을 이용하는것이 좋다
  3. 오버라이딩 한 함수들의 return 값을 기반으로 변경이 있는지 판단하여 값이 다른 항목만 업데이트 한다

ListAdapter에 Data 업데이트

ListAdapter는 데이터 리스트를 currentList라는 이름의 Inner field로 가지고 있다.

데이터를 업데이트 하기 위해서는 submitList를 호출해야한다.

그렇기 때문에 더이상 Adapter안에서 itemList들을 따로 관리할 필요가 없어졌다.

기존 itemList를 통해 접근했던 코드들을 currentList접근으로 수정해야한다.

onBindViewHolder에서는 아래처럼 접근

holder.binding(currentList[pos])

getItemCount도 아래처럼

return currentList.size

 

연산 Thread 분리하기 - AsyncListDiffer

DiffUtil은 두 리스트간의 차이를 알고리즘을 통해 계산하는데 이과정에서 리스트의 크기가 큰경우 시간이 오래 소요될 수 있다. 따라서 해당 계산은 UI Thread가 아닌 백그라운드에서 수행하도록 권장하고 있는데. 이러한 경우 사용할 수 있는것으로 AsyncListDiffer를 많이들 소개하고있다

주로 다른 글에서는 RecyclerView.Adapter와 AsyncListDiffer, DiffUtil 이렇게 세가지를 조합하여 자체적으로 멀티 쓰레드를 처리할 수 있도록, 동기화 처리를 생략할수 있도록 쉬운방법을 제공한다.

하지만 지금 위에서 사용한 ListAdapter(RecyclerView의 ListAdapter이다. 그냥 ListAdapter가 아님)는 AsyncListDiffer를 사용하는 수고까지도 줄여줄 수 있도록 AsyncListDiffer를 wapping하는 클래스이다. 따라서 위 예시가 Thread 분리까지 모두 해결되는 예시이다...! 개이득

 

Reference

https://www.raywenderlich.com/21954410-speed-up-your-android-recyclerview-using-diffutil

https://medium.com/hongbeomi-dev/번역-recyclerview의-내부-동작-941a2827fa5a

https://medium.com/1mgofficial/how-recyclerview-works-internally-71290de5d2c4