Today
-
Yesterday
-
Total
-
  • 리사이클러뷰 해부하기(1)
    안드로이드 개발

     

    ※ 본 포스팅은 이 링크의 내용을 번역한 글입니다.

     

    Anatomy of RecyclerView: a Search for a ViewHolder

    Intro

    medium.com

     


    Intro

    리사이클러뷰는 거의 모든 안드로이드 앱 개발에 사용되기 때문에 개발자들이 리사이클러뷰를 다루는 것은 수백만명의 사용자들에게 영향을 끼칩니다. 그러나 리사이클러뷰를 "어떻게 사용하는지"에 대한 자료는 쉽게 찾아볼 수 있지만, "어떻게 동작하는지"에 대한 자료는 비교적 적은 것 같습니다. 복잡하고 다양한 커스터마이징과 성능 이슈가 많은 기능에 대한 "블랙박스 검사(소프트웨어를 내부 구조나 작동 원리를 모르는 상태에서 소프트웨어의 동작을 검사하는 것)"는 분명 좋지 않아 보입니다. Google I/O 2016 에서 소개되었던 RecyclerView ins and outs 역시 빙산의 일각에 불과합니다. 이 글에서는 리사이클러뷰의 내부 구조를 좀 더 깊게 다루어보겠습니다.

     

    먼저 리사이클러뷰 내부에서 가장 핵심적인 역할을 하는 getViewByPoisition() 이 호출될 때 무슨 일이 일어나는지 알아볼 것입니다. 이 메서드를 통해 우리는 뷰홀더의 재활용, 숨겨진 뷰(Hidden View), 예측 애니메이션(Predictive Animation), stable ids 등 리사이클러뷰의 다양한 측면을 공부할 수 있습니다.

     

    아이템 목록을 배치하는 과정에서 LayoutManager 는 RecyclerView 에게 "8번째 position 에 대한 뷰를 달라"고 요청합니다. 이 요청을 처리하기 위해 리사이클러뷰는 다음과 같은 일련의 과정을 거치게 됩니다.

    1. chaged scrap 탐색

    2. attached scrap 탐색

    3. removed 되지 않은 hidden view 탐색

    4. view cache 탐색

    5. 어댑터가 stable ids 를 가질 경우 - 주어진 id 를 기준으로 attched scrap 과 view cache 재탐색

    6. ViewCacheExtension 탐색

    7. RecycledViewPool 탐색

    만약 이 중 어떤 곳에서도 필요한 뷰를 찾지 못했다면, 어댑터의 onCreateViewHolder() 를 호출하여 새로운 뷰를 생성합니다. 필요한 경우 onBindViewHolder() 를 호출하여 뷰를 바인딩한 뒤, 마지막으로 LayoutManager 에게 해당 뷰를 반환해줍니다.

     

    내부적으로 엄청나게 많은 일들이 일어나는 것을 알 수 있습니다. 이 많은 캐시들이 무엇이고, 왜 필요한지, 어떤 일을 하는지 하나씩 알아보겠습니다.

     


    RecycledViewPool

    각각의 캐시들은 어떤 자료 구조로 구현되어 있으며, 어떤 조건에서 뷰홀더가 캐싱되고 검색되며, 과연 그 목적은 무엇일까요?

     

    아마 pool 의 목적은 대부분 알 것으로 생각됩니다. 리사이클러뷰가 아래로 스크롤되는 동안, 상단에서 사라지는 뷰는 하단에 새로 나타나는 뷰로 재사용하기 위해 pool 에 recycle 됩니다. 뷰홀더가 pool 에 저장되는 또 다른 시나리오를 알아보기 전에 먼저 RecycledViewPool 의 코드를 살펴보겠습니다. (RecycledViewPool 은 RecyclerView.Recycler 의 inner class 입니다.)

    public static class RecycledViewPool {
    
        private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<>();
        
        private SparseIntArray mMaxScrap = new SparseIntArray();
        
        …
        
        public ViewHolder getRecycledView(int viewType) {
            ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
            
            …

    ** mScrap 이라는 변수명은 위에서 언급했던 attached scrap, changed scrap 과는 관계 없는 이름으니 혼동하지 맙시다.

     

    위 코드에서 viewType 마다 각각의 뷰홀더 pool 을 가지고 있다는 것을 알 수 있습니다. 리사이클러뷰가 다른 모든 캐시들에서 뷰홀더를 찾지 못하면, 마지막으로 pool 에게 "viewType 에 해당하는 뷰홀더가 있는지" 묻습니다. 

     

    viewType 별로 가지고 있는 pool 의 기본 용량은 5 이지만, 아래 코드를 통해 변경이 가능합니다. 

    recyclerView.getRecycledViewPool()
        .setMaxRecycledViews(SOME_VIEW_TYPE, POOL_CAPACITY);

    이러한 유연성을 가지는 것은 굉장히 중요한 포인트입니다. 만약 화면에 동일한 viewType 을 가지는 아이템이 수십개가 존재하며, 이들이 동시에 변경되는 일이 종종 발생한다면 그 viewType 에 대해서는 capacity 를 크게 설정해주는 것이 좋습니다. 반대로, 화면에 딱 하나만 보여지는 viewType 에 대해서는 capacity 를 1로 설정하는 것이 좋습니다. 만약 5의 용량을 가지도록 그대로 둔다면 pool 에는 5개의 뷰홀더가 채워지지만 그 중 4개는 사용되지 않고 메모리를 낭비하기 때문입니다.

     

    또 다른 주목할 점은, recycledViewPool 의 getter, setter 가 public 이라는 것입니다. getRecycledViewPool(), setRecycledViewPool() 을 통해 하나의 pool 을 여러 리사이클러뷰가 공유할 수 있습니다.

    + 여기서 주의할 점은 pool 은 뷰홀더를 들고 있으며, 뷰홀더는 뷰를, 뷰는 context 를 들고 있다는 것입니다. 따라서 pool 을 공유하려는 여러 리사이클러뷰는 반드시 같은 액티비티 내에 존재해야합니다.  

     

     

    ways to pool

    이제, 뷰홀더가 pool 에 저장되는 또 다른 시나리오에 대해 알아보겠습니다. 다음 5가지 시나리오가 있습니다.

    1. 스크롤하는 동안 뷰가 리사이클러뷰의 범위를 벗어날 때 (여기서 '범위'란, 화면에 표시되는 영역을 뜻합니다.)

    2. 데이터의 변경으로 인해 해당 뷰가 더 이상 보여지지 않을 때 - 사라지는 애니메이션이 끝난 후 pool 에 추가

    3. view cache 에 있는 아이템이 update 또는 remove 되었을 때

    4. 뷰홀더를 탐색하는 도중에 scrap 이나 cache 에서 뷰를 발견했으나, view type 또는 id 가 맞지 않을 때

    5. LayoutManager 가 pre-layout 에서는 뷰를 추가했으나, post-layout 에서는 해당 뷰를 추가하지 않을 때

    1, 2번 시나리오는 쉽게 이해되지만, 한 가지 생각해볼만한 점이 있습니다. 2번의 경우 단지 아이템이 remove 될 때 뿐만아니라, 새로운 아이템이 insert 되면서 리사이클러뷰의 범위 밖으로 밀려나는 뷰에 대해서도 발생한다는 것입니다.

     

    나머지 시나리오에 대해서는 더 많은 설명이 필요해보입니다. 우리는 아직 view cache 와 scrap 에 대해 다루지 않았지만 3, 4번 시나리오는 그리 복잡하지 않습니다. 우리는 pool 에 있는 뷰를 "dirty view" 라고 말합니다. 뷰가 pool 에 캐싱될 때 view 와 view type 만 남기고 나머지 position, flags 등의 "상태(state)" 값은 초기화 되기 때문에 pool 에 존재하는 뷰를 꺼내 쓰려면 데이터를 다시 바인딩해주어야 합니다. 반면, pool 을 제외한 다른 캐시들에 존재하는 뷰는 상태값을 그대로 가지고 있기 때문에 바인딩하지 않고 그대로 재사용할 수 있습니다. pool 에서는 view type 을 기준으로 뷰홀더를 찾고, 그렇게 선택된 뷰홀더는 다시 새 삶을 시작하는 것입니다.

     

    이 개념을 토대로 3, 4번 시나리오를 이해할 수 있을 것입니다. 예를 들어, view cache 에 있는 아이템이 remove 되었다면 원래 그 뷰가 위치해있던 자리(position)에 그 상태 그대로 재사용 될 일은 없을 것입니다. 그렇다고 그 뷰를 완전히 버리는 것보다는 pool 에 넣는 것이 좋은 선택인 것 같습니다.

     

    마지막 5번 시나리오를 이해하려면 pre-layout 과 post-layout 에 대한 개념을 필요로 합니다. pre/post-layout 은 리사이클러뷰의 모든 파트에서 발생하는 메커니즘이기 때문에 먼저 알아보고 가는 것이 좋겠습니다. 

     


    Pre-layout, Post-layout and Predictive Animations

    다음의 상황을 생각해봅시다.

    아이템 a, b, c 중에 ab 만 화면에 보여지고 있는 상태에서 b 를 지우자, c 가 화면에 보여집니다.

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    우리가 기대하는 애니메이션은 당연히 c 가 아래에서부터 위로 자연스럽게 올라오는 애니메이션입니다. 그런데 어떻게 이게 가능할까요? c 가 위치해야 할 자리는 알겠지만, 어느 방향으로 슬라이딩을 해야하는지는 어떻게 알 수 있을까요? 단순히 new layout 의 모습을 보고 c 가 마지막에 위치하니까 아래에서 올라와야 한다고 가정하는 것은 좋지 못한 생각입니다. LayoutManager 에 따라 옆에서 넘어오거나 또 다른 애니메이션이 필요할 수도 있기 때문입니다. 그렇다면 previous layout 을 보면 결정할 수 있을까요? 그 시점에는 가 없기 때문에 별로 도움이 되질 않아 보입니다. 

     

    여기서 구글이 만들어 낸 해결책은 다음과 같습니다. 어댑터 내부에서 변화가 일어나고 나면, 리사이클러뷰는 LayoutManager 에게 두 개의 레이아웃을 요구합니다. 첫 번째는 pre-layout 입니다. pre-layout 은 이전의 어댑터 상태와 함께, 변경된 항목을 "힌트" 형태로 제공합니다. 이를 위해서는 추가적인 뷰 공간이 필요할 수도 있습니다. 우리가 가정한 상황에서도 b 는 삭제될 것이고, 추가적으로 c 가 보여질 것을 pre-layout 에 나타냅니다. 두 번째는 post-layout 입니다. post-layout 은 변경 이후의 어댑터의 상태를 나타냅니다.

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    이제 pre-layout 과 post-layout 두 개를 비교하여 c 에게 적절한 애니메이션을 줄 수 있게 되었습니다. 

     

    이렇게 변경 전(previous layout) 또는 변경 후(new layout) 둘 중 한 쪽에는 존재하지 않는 아이템뷰에 적절한 애니메이션을 주는 기법을 "예측 애니메이션(Predictive Animation)" 이라고 부릅니다. 이는 리사이클러뷰의 가장 중요한 컨셉중 하나입니다. 세부적인 내용은 뒤에서 더 자세히 다루고, 다른 시나리오를 하나 더 살펴봅시다. 만약 b 가 삭제(deleted)되는 것이 아니라 바뀐(changed)다면?

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    아마 이상하게 생각하셨을텐데요, LayoutManager 가 여전히 pre-layout 에 c 를 그려줍니다. 왜일까요? 그 이유는 b 의 높이가 줄어들면서 c 가 화면에 살짝 보이게 될 수도 있기 때문입니다. 그러나 이 예에서는 post-layout 에서 b 의 크기에는 변화가 없고, c 도 필요가 없어 졌습니다. 따라서 c 는 pool 로 들어가게 됩니다. 이것이 뷰홀더가 pool 에 저장되는 5번 시나리오 입니다.

     

    다시 RecycledViewPool 로 돌아가봅시다.

     


    뷰홀더가 pool 에 들어갈 때 생각해 볼 사항이 두 가지가 있습니다. 1)재활용이 불가능하거나, 2)일시적인 상태(transient state)의 뷰 일수도 있습니다. 

     

    Recyclability

    recyclability 는 뷰홀더가 가지는 flag 이며, setIsRecyclable() 메서드를 통해 변경 가능한 값입니다. 리사이클러뷰도 이를 활용하여 애니메이션 중에 있는 뷰홀더는 재활용이 불가능하도록 만듭니다.

     

    하나의 flag 를 여러곳에서 조작하는 것은 일반적으로 좋은 방식이 아닙니다. 실제로 recyclable flag 는 "pair" 하게 동작하기 때문에 만약 리사이클러뷰가 애니메이션이 끝난 후 setIsRecyclable(true) 를 호출했는데, 우리가 어떤 이유로 인해 해당 뷰가 재활용되는 것을 원하지 않아서 setIsRecyclable(false) 를 호출한다고 해도 이는 동작하지 않을 것입니다.

     setIsRecyclable(false) 를 두 번 호출했다면, setIsRecyclable(true) 똑같이 두 번 호출해야 원하는대로 재활용이 가능해집니다.

     

    Transient state

    transient state 는 뷰가 가지는 flag 이며 마찬가지로 "pair" 하게 동작하고, setHasTransientState() 로 변경할 수 있습니다. 뷰 클래스는 이 플래그를 직접 사용하지는 않지만, ListView 나 RecyclerView 와 같은 위젯에게 '해당 뷰를 재사용하지 않는 것이 좋다'는 정보를 제공하기 위해 들고 있는 값입니다.

     

    이 flag 가 사용되는 예는  view.animate() ..  를 통해 동작하는 ViewPropertyAnimator 에서 찾아볼 수 있습니다. ViewPropertyAnimator 는 애니메이션이 시작할 때 자동으로  setHasTransientState(true)  를 호출하고, 애니메이션이 끝나면  setHasTransientState(false)  를 호출해줍니다. 만약 여러분이 뷰에 애니메이션을 주기 위해 예를 들어 ValueAnimator 를 사용한다면, transient state 를 여러분이 직접 다뤄주어야 합니다. 또 다른 예로 EditText 를 들 수 있습니다. 어떤 텍스트가 선택되었을 때나 텍스트가 수정되고 있는 중에 뷰는 자동으로 transient state 가 됩니다.

     

    마지막으로 기억해두어야 할 것은, transient state 는 child 에서부터 parent 그리고 root view 까지 전파된다는 것입니다. 따라서 만약 아이템 내부의 어떤 뷰에 애니메이션을 준다면, 해당 뷰 뿐만아니라 뷰홀더가 참조하고 있는 root view 까지 모두 transient state 가 됩니다. 

     

    OnFailedToRecycleView

    만약 recyclability 또는 transient state 에 의해서 뷰홀더를 recycle 하는 것에 실패할 경우, 어댑터의  onFailedToRecycleView() 가 호출됩니다. 이것은 정말 중요한 포인트입니다. 이 메서드는 이벤트를 알리는 것과 더불어 여러분에게 이 상황을 어떻게 처리할 것인지를 묻는 것입니다. 

     

    이 메서드의 리턴 값으로 true 를 주는 것은 "어쨋거나 그냥 recycle 하라"는 것을 의미합니다. 만약 새로운 아이템을 바인딩할 때 모든 애니메이션과 이 문제의 모든 원인을 초기화시키고 있다면, onFailedToRecycleView() 메서드 내부에서 그 일들을 수행하고 정상적으로 뷰를 recycle 하도록 할 수 있습니다.

     

    우리가 반드시 피해야 할 것은 onFailedToRecycleView() 를 완전히 무시하는 것입니다. 다음의 "비참한" 상황을 상상해봅시다. 여러분은 아이템 내부의 이미지가 보여질 때 fade-in 애니메이션을 주었습니다. 사용자가 스크롤을 빠르게해서 애니메이션이 끝나기도 전에 리사이클러뷰의 범위 밖으로 벗어나면 해당 뷰홀더는 재활용이 불가능해집니다. 따라서 계속해서 새로운 뷰홀더가 생성되고, 또 생성되고, 스크롤이 지연되며 메모리가 점점 난잡해집니다.

     

    뷰홀더를 recycle 하는 것에 성공할 경우, 어댑터의 onViewRecycled() 메서드가 호출됩니다. 여기서 이미지와 같은 무거운 리소스를 해제할 수 있습니다. 일부 ViewHolder 는 사용되지 않은 채로 pool 에 오랜시간 상주하고 있을 수 있으며, 이는 곧 메모리 낭비를 유발할 수 있다는 점을 기억합시다.

     


    View Cache

    이 글에서 말하는 "view cache" 또는 그냥 "cache" 는 RecyclerView.Recycler 클래스의 mCachedViews 필드를 가리키는 것입니다. 코드의 주석에서는 이 필드를 "first level cache" 라고 부릅니다.

     

    이 필드는 ViewHolder 로 이루어진 ArrayList 입니다. RecycledViewPool 과 다르게 view types 으로 나누어져 있지 않습니다. 디폴트 크기는 2 이며, setItemViewCacheSize() 메서드를 통해 capacity 를 변경할 수 있습니다.

     

    앞서 언급했다시피, pool 과 다른 캐시들의 가장 중요한 차이점은 pool 은 view type 을 기준으로 뷰홀더를 탐색하는 반면, 다른 캐시들은 position 을 기준으로 탐색한다는 것입니다. 따라서 view cache 에 있는 뷰홀더는 데이터를 다시 바인딩할 필요 없이 원래 위치해있던 position 에 그대로 재사용될 수 있는 상태로 존재합니다. 이 구분을 좀 더 명확하게 하면 다음과 같습니다.

    • 뷰홀더가 어디에도 존재하지 않으면 - 새로 생성하고 바인딩합니다.

    • 뷰홀더를 pool 에서 찾았다면 - 바인딩합니다.

    • 뷰홀더를 cache 에서 찾았다면 - 아무것도 하지 않아도 됩니다.

    이제, 한 가지 중요한 점이 명확해졌습니다. 바인딩 되어있던 뷰홀더가 recycle 되어 pool 에 들어갈 때(onViewRecycled() 호출 시)를 생각해보면, 들어갈 때와 나갈 때 서로 같은 아이템이 아니라는 것입니다. 반면, 뷰홀더가 화면에서 사라질 때 pool 이 아닌 view cache 로 들어갈 수 있고, 이 뷰홀더가 다시 보여질 때는 view cache 에서 검색되며, 이 때는 같은 아이템이며, 다시 바인딩되지 않습니다.

     

    만약 어떤 아이템에 대해 화면에 존재하는지 여부를 추적하고 싶다면 어댑터의 onViewAttachedToWindow()  onViewDetachedFromWindow() 두 콜백 메서드를 사용하면 됩니다.

     

    Filling pool and cache

    그렇다면, 뷰홀더는 어떻게 view cache 에 저장될까요?

     

    앞서 설명드린 pool 에 저장되는 시나리오에서 3번을 제외한 나머지 상황에서 뷰홀더는 pool 또는 view cache 에 저장이 됩니다.

     

    둘 중 어느 곳에 저장할지 선택하는 규칙은 다음과 같습니다. pool 과 cache 가 모두 비어있는 초기 상태에서 아이템이 하나씩 recycle 될 때 pool 과 cache 가 채워지는 것을 그림으로 그려보겠습니다. (capacity 는 모두 기본 값이며, view type 은 하나만 존재한다고 가정합니다.)

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    cache 에 빈 공간이 남아 있으면 뷰홀더는 cache 에 저장됩니다. 만약 cache 가 가득 찼다면 먼저 들어간 뷰홀더가 pool 로 보내지고 새로 들어온 뷰홀더는 cache 에 저장됩니다. 만약 pool 또한 가득 찼다면 뷰홀더는 어디에도 저장되지 않고 버려져서 Garbage Collector 에 의해 수집될 것 입니다.

     


    Pool and Cache in Action

    이제, 실제 리사이클러뷰가 스크롤 될 때의 상황에서 pool 과 cache 의 동작을 살펴보겠습니다.

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    현재 화면에 보여지는 아이템 뒤에는 cache 와 pool 에 존재하는 아이템들이 "꼬리"처럼 붙어있습니다. 스크롤을 내리면 8번 아이템이 화면에 보여질 것입니다. position 8 에 해당하는 뷰홀더를 cache 에서는 찾을 수 없으므로 pool 에 있는 뷰홀더, 즉 이전에 3번 아이템을 표시했던 뷰홀더를 다시 8번으로 바인딩하여 재사용합니다. 그리고 6번 아이템이 화면에서 사라지면서 cache 로 들어가고 4번을 pool 로 밀어냅니다.

     

    반대로 스크롤된다면, 어떤 그림이 될까요?

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    5번 아이템이 다시 화면에 보여질 때 position 5 에 해당하는 뷰홀더를 cache 에서 찾아서 바로 재사용할 수 있습니다. 이러한 "반대 방향 스크롤"을 지원하는 것이 cache 의 주된 use-case 입니다. 따라서 뉴스 피드와 같이 사용자가 반대 방향으로 돌아갈 일이 거의 없는 리스트라면 cache 가 불필요할 수도 있습니다. 반면, 쇼핑 앱의 상품 목록과 같이 사용자가 항목을 고르고 비교하는 일이 잦은 리스트라면 cache 의 capacity 를 더 확장하는 것이 좋습니다.

     

    여기서 주목할만한 점이 몇 가지 있습니다. 첫 째로, 만약 3번 아이템까지 스크롤한다면 어떻게 될까요? pool 은 stack 처럼 동작한다는 것을 떠올려봅시다. 마지막으로 3번 아이템을 본 이후로 아래로 스크롤만 했다면 3번은 pool 에 가장 마지막으로 들어간 아이템일 것이며, 다시 3번 아이템을 보여주기 위해 pool 에서 꺼내져서 바인딩됩니다. 만약 3번의 데이터가 전혀 바뀌지 않은 상황이라면, 우리는 데이터를 다시 바인딩 할 필요가 없습니다. 따라서 onBindViewHolder() 에서는 데이터를 바인딩 해 줄 필요가 있는 상황인지 아닌지를 항상 체크해야 합니다.

     

    두 번째로 주목할 점은, 스크롤하는 동안에는 pool 에 1개의 아이템만 존재한다는 사실입니다. (물론, n 개의 열이 있는 grid 를 사용하는 경우, pool 에 n 개의 아이템이 존재합니다.) 만약 시나리오 2~5를 통해 이미 pool 에 들어왔던 다른 아이템들이 있다면 스크롤하는 동안 그들은 아무 쓸모없이 제자리에 유지됩니다.

     

    이번에는 한번에 많은 아이템이 pool 로 들어오는 시나리오를 생각해봅시다. notifyDataSetChanged() 가 호출되거나 넓은 범위의 nofityItemRangeChanged() 가 호출될 때를 예로 들 수 있습니다.

    이미지 출처 : 원문(https://medium.com/android-news/anatomy-of-recyclerview-part-1-a-search-for-a-viewholder-404ba3453714)

    모든 뷰홀더는 invalid 되며, 모두 cache 가 아닌 pool 로 들어갑니다. pool 의 공간이 충분하지 않을 때는 몇몇 뷰홀더는 버려져서 GC에 의해 수집될 것이며, 그 결과 새로운 뷰홀더를 생성해야 합니다. 스크롤할 때와는 달리, 이 상황에서는 용량이 큰 pool 이 필요해 보입니다. 또 다른 예로, 개발자가 코드상에서 직접 scrollToPosition() 을 호출할 때도 용량이 큰 pool 이 필요합니다. 

     

    그렇다면, 가장 최적의 pool 의 크기는 어떻게 결정할 수 있을까요?

    큰 pool 이 필요하기 직전에 capacity 를 늘리고, 상황이 끝난 후 다시 축소하는 방법을 생각해볼 수 있겠습니다. 다음은 이를 구현한 "더러운 코드" 입니다. 

    recyclerView.getRecycledViewPool().setMaxRecycledViews(0, 20);
    
    adapter.notifyDataSetChanged();
    
    new Handler().post(new Runnable() {
        @Override
        public void run() {
            recyclerView.getRecycledViewPool()
                .setMaxRecycledViews(0, 1);
        }
    });

     

     

    다음 글(리사이클러뷰 해부하기(2))에서 계속됩니다.

    댓글

Designed by black7375.