참고자료)
Android Developers - Android Dev Summit 2015
Android Developers document- RecyclerView.RecycledViewPool
RecyclerView 제대로 알고쓰자
[안드로이드 공식문서 파헤치기] RecyclerView의 모든 것! - 1편에서, RecyclerView가 유저와의 상호작용의 결과, 스크롤 알림을 Layout Manager에게 알린다고 설명했습니다. 알림을 받은 Layout Manager는 몇 번째 위치(=position)에 새로운 ItemView가 배치되어야 하는지를 계산하고 이 위치를 다시 RecyclerView에게 알립니다. RecyclerView에게 위치를 알리는 이유는 RecyclerView에게 해당 위치의 뷰를 달라고 요청하는 것입니다. 그럼 RecyclerView는 Cache(=캐시)에서 해당 위치에 배치하도록 지정된 ItemView가 있는지 확인합니다.(RecyclerView 내부 동작 원리 상 일정 양의 ItemView를 캐시에 저장해놓기 때문) 만약 해당 위치에 배치되어야 하는 ItemView가 Cache에 저장되어 있다면 RecyclerView는 해당 ItemView를 다시 Layout Manager에게 전달합니다.
그러나 만약 Pool에 해당 ViewType을 위한 ViewHolder가 존재한다면 RecyclerView는 Adapter에게 이 ViewHolder를 ItemView가 배치될 위치(=position)에 연결(bind)해달라고 요청합니다. 그리고 Adapter는 RecyclerView에게 해당 ItemView를 전달하고 RecyclerView는 이 ItemView를 다시 Layout Manager에게 최종적으로 전달합니다.
결과적으로 Layout Manager는 해당 위치에 ItemView를 배치하고 RecyclerView에게 이를 알립니다.(addView()라는 함수를 통해) 그러면 RecyclerView는 Adapter에게 해당 position에 ItemView가 잘 배치되었다고 알립니다.(onViewAttachedToWindow()라는 함수를 통해)
그리고 다시 한 번 전체 과정을 살펴보면 Layout Manager가 어떤 position에 배치될 ItemView를 요청할 경우 해당 ItemView가 Cache에 저장되어 있다면 Adapter를 거치지 않고 바로 Layout Manager에게 해당 ItemView를 전달할 수 있음을 깨달을 수 있습니다.
1) 레이아웃 매니저가
getViewForPosition
으로 view를 요청
2) RecyclcerView는 캐시에getViewForPosition
으로 확인. 있으면 LayoutManager에게 반환
3) 캐시에 없으면 adapter에게 type이 뭔지 물어보고 Recycled Pool에getViewHolderByType
으로 요청
4) Pool에 있으면 반환, 없으면 adapter에게createViewHolder
로 아이템 생성.
5) 뷰를 찾으면 adapter에서bindview
를 하고 LayoutManager에게 리턴.
6) LayoutManager는 RecyclerView에게addView
를 수행하고 adapter의onViewAttachedToWindow
가 호출됨.
캐시와 Recycled Pool은 RecyclerView안에 이미 선언되어있음.
setItemViewCacheSize
함수로 캐시 사이즈 변경 가능.
ViewHolder를 어떻게 재활용하는지 알아보기 전에, onBindViewHolder() 메소드를 자세히 볼 필요가 있습니다!
onBindViewHolder()는 파라미터로 ViewHolder객체와 position값을 받고 반환타입은 없습니다. position값은 Adapter 클래스 프로퍼티인 데이터셋의 특정 포지션입니다. 예를 들어 데이터 셋이 배열 자료구조로 구성되었으면 position은 배열의 특정 index입니다.
ViewHolder객체는 데이터 셋의 특정 position에 저장되어 있는 아이템을 보여주기 위해 업데이트 되어야하는 ViewHolder입니다
onBindViewHolder()메소드는 특정 position의 데이터(item)을 보여주기 위해 RecyclerView가 호출하는 것입니다. 즉 호출하는 주체가 RecyclerView인데 RecyclerView는 내부에 tryBindViewHolderByDeadLine()이라는 메서드 내부에서 mAdapter.bindViewHolder()를 호출합니다. bindViewHolder()에서 onBindViewHolder()가 호출됩니다
즉 RecyclerView가 특정 상황이 발생했을 때 이를 알리기 위해 bindViewHolder를 호출하고 이 알림을 받았을때 해야하는 작업을 우리가 Adapter내에 onBindViewHolder()메소드를 오버라이딩해서 함수 내부에서는 RecyclerView.ViewHolder.itemView의 컨텐츠를 업데이트하는 작업이 실행됩니다. 업데이트만하고 반환되는 작업이 없기에 onBindViewHolder()는 콜백함수라는 걸 알 수 있습니다
사용자가 스크롤을 위로 올려서 기존의 ItemView가 화면에서 사라지는 경우는 아래와 같은 과정이 진행됩니다.
(1) LayoutManager가 화면에서 벗어난 ItemView의 position을 계산하고 RecyclerView에게 이를 알립니다.
(2) RecyclerView는 화면에서 ItemView를 제거한 후 Adapter에게 이를 알리게 되고, ItemView 안에 있는 것들의 캐싱을 해제할 수 있게 됩니다.
(3) RecyclerView는 Cache에게 제거되는 position의 ItemView가 캐시에 계속 남아있어도 되는 것인지를 Cache에게 물어봅니다.
만약, 이 ItemView가 사용된 지 오래된 ItemView라면, (3–1) Cache는 Recycled Pool에게 이 오래된 ItemView를 전달하고, (3–2) Recycled Pool은 Adapter에게 이 ItemView를 메모리에서 제거해도 된다는 메시지를 보냅니다. 그렇지 않고 계속 캐시에 저장할 필요가 있는 경우,
Cache에 계속 저장하라는 지시를 내리고, 이를 통해 나중에 LayoutManager가 해당 position에 대한 ItemView를 요청할 경우 Adapter를 거치지 않고 사용할 수 있게 됩니다.
RecycledViewPool
에 대해 자세히 알아봅시다!public static class RecycledViewPool {
private static final int DEFAULT_MAX_SCRAP = 5;
public void setMaxRecycledViews(int viewType, int max) {
ScrapData scrapData = getScrapDataForType(viewType);
scrapData.mMaxScrap = max;
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
while (scrapHeap.size() > max) {
scrapHeap.remove(scrapHeap.size() - 1);
}
}
public int getRecycledViewCount(int viewType) {
return getScrapDataForType(viewType).mScrapHeap.size();
}
@Nullable
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
RecycledViewPool은 RecyclerView.Recycler의 inner class입니다. getRecycledView의 파라미터로 ViewType을 전달하면, ViewType에 맞는 ViewHolder를 return해준다는 것을 알 수 있고, ViewType마다 ViewHolder Pool을 가지고 있다는 것을 알 수 있습니다.
즉 캐시에서 원하는 ViewHolder를 찾지 못한 경우 마지막으로 RecycledViewPool의 getRecycledView로 해당 ViewType에 해당하는 ViewHolder를 달라고 요청하는 것입니다
또한 상수로 DEFAULT_MAX_SCRAP =5
로 선언되어 있는 것은 ViewType별로 가지고 있는 pool의 기본 용량이 5개라는 것입니다
setMaxRecycledViews
의 파라미터로 뷰타입과 pool이 가지고 있는 ViewHolder의 개수를 전달하면 pool의 용량을 늘리거나 줄일 수 있습니다.
또 하나 중요한 점은 RecycledViewPool
가 public으로 설정된 class라는 것입니다. 즉 RecyclerView.RecycledViewPool()
처럼 RecycledViewPool 객체를 생성하여 해당 Pool을 '공유'할 수 있습니다. 즉 여러 RecyclerView들이 같은 Pool을 공유해 메모리를 절약할 수 있습니다.
이처럼 pool에 있는 뷰들을 dirty view
라고 부릅니다. dirty view
는 pool에 들어올 때 뷰와 뷰타입만 남기고 potition, flags등의 상태는 초기화 되기 때문에 pool에 존재하는 dirty view
들을 꺼내 쓰려면 데이터를 다시 바인딩해주어야 합니다
반면 pool이 아닌 캐시에 있는 view는 position,flags등의 상태를 그대로 가지고 있기 때문에 바인딩없이 그대로 재사용할 수 있습니다.
원하는 ViewHoler가 있는지 RecyclerdViewPool에서 찾기 전에 Cache
를 먼저 방문한다고 했습니다.
Cache
는 ViewHolder로 이루어진 리스트로, RecyclerdViewPool와 다르게 view type으로 ViewHolder를 구분하지 않습니다. 대신 "position"을 기준으로 탐색합니다. 따라서 캐시에 있는 ViewHolder는 데이터를 다시 바인딩 할 필요 없이 원래 위치해 있던 position에 그대로 재사용 될 수 있는 것입니다
예를 들어 가장 위에 있던 position 5 아이템이 위로 스크롤 되어 화면에서 벗어난 후 다시 아래로 스크롤되어 화면에 보여질 때 position 5 에 해당하는 뷰홀더가 cache에 있었다면 5라는 position을 다시 바인딩할 필요없이 바로 재사용 할 수 있습니다
즉, 뷰홀더가 어디에도 존재하지 않으면->새로 생성되고 바인딩됨->뷰홀더를 cache에서 찾았다면 view변경없이 바로 재활용->pool에서 찾았다면 바인딩 필요
onAttachedToRecyclerView
가 호출되어 붙음onCreateViewHolder
호출 addView
를 호출해 itemView를 position에 잘 붙인 후 RecyclerView에게 알리면 RecyclerView는 Adapter에게 onViewAttachedToWindow
호출중요한건, 화면에 최초로 보이는 4개의 ViewHolder를 생성하고 난 후 스크롤을 내렸을 때입니다.
우리는 이때 바로 위에 있는 ViewHolder인 0번 홀더가 사라지고 5번째 itemView가 나타나면서 0번째 ViewHolder를 재사용할 것이기 때문에 바로 onCreateViewHolder를 건너뛰고 onBindViewHolder를 호출할 것이라 생각합니다.
하지만 5번째 itemView가 나타나며 또 ViewHolder를 생성합니다. 8번째 itemView가 나타날때까지 ViewHolder는 재활용되지 않고 계속 생성됩니다. 이는 RecyclerView가 사라진 ViewHolder를 '바로' 재활용하지 않기 때문에 나타나는 현상입니다
계속 스크롤하다가 9번째 itemView가 만들어지는 순간 가장 먼저 detached되었던 0번째 ViewHolder가 onViewRecycled되어 나타납니다. onViewRecycled는 "재활용할 홀더를 가지고 왔음"을 알리는 메서드 입니다. 이후 9번째 itemView부터는 onCreateViewHolder가 호출되지 않고 detached되었던 ViewHolder들이 차례로 onViewRecycled되어 재사용됩니다.
onViewRecycled
가 호출되지 않는다는 것을 알 수 있습니다.
감사합니다!