[안드로이드 공식문서 파헤치기] RecyclerView의 모든 것! - 2편(ViewHolder수명주기)

dada·2022년 8월 7일
8
post-thumbnail

참고자료)
Android Developers - Android Dev Summit 2015
Android Developers document- RecyclerView.RecycledViewPool
RecyclerView 제대로 알고쓰자

✅ViewHolder의 생성

  • [안드로이드 공식문서 파헤치기] RecyclerView의 모든 것! - 1편에서, RecyclerView가 유저와의 상호작용의 결과, 스크롤 알림을 Layout Manager에게 알린다고 설명했습니다. 알림을 받은 Layout Manager는 몇 번째 위치(=position)에 새로운 ItemView가 배치되어야 하는지를 계산하고 이 위치를 다시 RecyclerView에게 알립니다. RecyclerView에게 위치를 알리는 이유는 RecyclerView에게 해당 위치의 뷰를 달라고 요청하는 것입니다. 그럼 RecyclerView는 Cache(=캐시)에서 해당 위치에 배치하도록 지정된 ItemView가 있는지 확인합니다.(RecyclerView 내부 동작 원리 상 일정 양의 ItemView를 캐시에 저장해놓기 때문) 만약 해당 위치에 배치되어야 하는 ItemView가 Cache에 저장되어 있다면 RecyclerView는 해당 ItemView를 다시 Layout Manager에게 전달합니다.

    • Cache(캐시) : 임시 저장 공간, 저장해 놓은 것을 빠르게 찾을 수 있으나 저장 공간의 용량이 작아 많은 양을 저장할 순 없다.

  • (노란색 화살표를 제외하고 읽어주세요) 하지만 만약 해당 위치에 배치될 ItemView가 캐시에 저장되어 있지 않다면 RecyclerView는 Adapter에게 해당 위치에 배치될 ItemView의 모양(ViewType)을 물어봅니다. (위에서 RecyclerView를 구성하는 ItemView가 모두 동일한 모양일 수도 있지만 다른 모양의 ItemView들이 섞여서 하나의 RecyclerView를 구성할 수도 있다고 설명했습니다.) 그럼 Adapter는 해당 위치에 배치될 ItemView의 모양을 RecyclerView에게 알려줍니다. 이번에는 RecyclerView가 Recycled Pool에 이 모양을 위한 ViewHolder가 있는지 체크합니다.
  • 만약 Recycled Pool에 해당 모양을 위한 ViewHolder가 존재하지 않는다면 RecyclerView는 Adapter에게 해당 모양을 위한 새로운 ViewHolder 생성을 요청합니다.

  • 그러나 만약 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함수로 캐시 사이즈 변경 가능.

✅onBindViewHolder()

  • 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()는 콜백함수라는 걸 알 수 있습니다

✅ViewHolder 재활용(ItemView를 캐시에 저장하는 원리)

  • 사용자가 스크롤을 위로 올려서 기존의 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를 거치지 않고 사용할 수 있게 됩니다.

✅RecyclerdViewPool

  • 여기까지 캐시와 RecyclerdViewPool을 탐색하며 RecyclerView가 ViewHolder를 찾는 과정, ViewHolder의 생성을 간략하게 알아봤습니다. 이제 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의 용량을 늘리거나 줄일 수 있습니다.

    • 이렇게 pool의 용량을 개발자가 직접 조절할 수 있다는 것은 매우 중요합니다. 만약 화면에 동일한 viewType을 가지는 아이템이 몇십개 존재하면 이들이 동시에 변경되어야 할땐 해당 viewType을 가지는 pool의 용량을 크게 설정하는 게 좋습니다. ViewHolder를 많이 저장해두면 재사용할 수 있는 ViewHolder도 많아지기 때문입니다. 반면 화면에 딱 하나만 보여지는 ViewType이 있다면 용량을 1로 설정하면 메모리를 절약할 수 있습니다
  • 또 하나 중요한 점은 RecycledViewPool가 public으로 설정된 class라는 것입니다. 즉 RecyclerView.RecycledViewPool() 처럼 RecycledViewPool 객체를 생성하여 해당 Pool을 '공유'할 수 있습니다. 즉 여러 RecyclerView들이 같은 Pool을 공유해 메모리를 절약할 수 있습니다.

✅dirty view

  • 이처럼 pool에 있는 뷰들을 dirty view라고 부릅니다. dirty view는 pool에 들어올 때 뷰와 뷰타입만 남기고 potition, flags등의 상태는 초기화 되기 때문에 pool에 존재하는 dirty view들을 꺼내 쓰려면 데이터를 다시 바인딩해주어야 합니다

  • 반면 pool이 아닌 캐시에 있는 view는 position,flags등의 상태를 그대로 가지고 있기 때문에 바인딩없이 그대로 재사용할 수 있습니다.

✅Cache

  • 원하는 ViewHoler가 있는지 RecyclerdViewPool에서 찾기 전에 Cache를 먼저 방문한다고 했습니다.

  • Cache는 ViewHolder로 이루어진 리스트로, RecyclerdViewPool와 다르게 view type으로 ViewHolder를 구분하지 않습니다. 대신 "position"을 기준으로 탐색합니다. 따라서 캐시에 있는 ViewHolder는 데이터를 다시 바인딩 할 필요 없이 원래 위치해 있던 position에 그대로 재사용 될 수 있는 것입니다

  • 예를 들어 가장 위에 있던 position 5 아이템이 위로 스크롤 되어 화면에서 벗어난 후 다시 아래로 스크롤되어 화면에 보여질 때 position 5 에 해당하는 뷰홀더가 cache에 있었다면 5라는 position을 다시 바인딩할 필요없이 바로 재사용 할 수 있습니다

  • 즉, 뷰홀더가 어디에도 존재하지 않으면->새로 생성되고 바인딩됨->뷰홀더를 cache에서 찾았다면 view변경없이 바로 재활용->pool에서 찾았다면 바인딩 필요

✅ViewHolder생성 시점 및 개수

  • 우리는 RecyclerView가 ViewHolder를 재활용하는 과정에서 가장 상단에 있는 View가 사라지면 해당 ViewHolder가 스크롤시 하단에 나타나는 View에 바로 재활용된다고 생각합니다. 하지만 ViewHolder는 "바로" 재활용 되지 않습니다.
  • 가장 처음 RecyclerView가 Adapter에 set되었을 때 호출되는 콜백함수를 보시면,
  1. RecyclerView가 화면에 onAttachedToRecyclerView가 호출되어 붙음
  2. 화면에 총 4개의 뷰홀더를 그리기 위한 onCreateViewHolder호출
  3. LayoutManager가 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되어 재사용됩니다.

  • 정리하자면, 9번째 itemView가 나타났을 때 가장 먼저 사라졌던 0번째 홀더가 재사용됨을 알리는 onViewRecycled:0 이 호출되고 이 홀더가 9번째 홀더가되어 bind되는 것입니다.
    바로 ViewHolder를 재사용한다는 의미의 콜백인 onViewRecycled가 호출되지 않는다는 것을 알 수 있습니다.
profile
'왜?'라는 물음을 해결하며 마지막 개념까지 공부합니다✍

3개의 댓글

comment-user-thumbnail
2022년 8월 12일

감사합니다!

1개의 답글
comment-user-thumbnail
2023년 3월 15일

정리를 너무 잘 하시네요 :)
많은 도움이 되고 있습니다.
감사합니다.

답글 달기