Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

RecyclerView는 ViewHolder를 어떻게 재활용하는가

RecyclerView는 ViewHolder를 어떻게 재활용하는가

MashUp 13th 안드로이드 세미나 발표 자료

Jaesung Lee

June 01, 2023
Tweet

More Decks by Jaesung Lee

Other Decks in Programming

Transcript

  1. 필요 기반 지식 Session Android Team 1. ListView와 RecyclerView의 성능적

    차이점을 이해하고 있다. 2. RecyclerView를 만드는 방법을 알고 있다. 3. RecyclerView Adapter에서 반드시 override해야 하는 메서드들의 의미를 알고있다.
  2. RecyclerView ins and outs - Google I/O 2016 Session Android

    Team https://www.youtube.com/watch?v=LqBlYJTfLP4
  3. /** * RecyclerView에 position을 알리고 필요한 View를 요청 * 현재

    아이템의 position을 다음 아이템의 position으로 업데이트 함 */ View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; } LinearLayout.LayoutState#next
  4. @NonNull public View getViewForPosition(int position) { return getViewForPosition(position, false); }

    View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } RecyclerView.Recycler
  5. @NonNull public View getViewForPosition(int position) { return getViewForPosition(position, false); }

    View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } RecyclerView.Recycler
  6. ViewHolder 탐색 순서 Session Android Team 1. changed scrap 탐색

    2. attached scrap 탐색 3. hidden view 탐색 4. view cache 탐색 5. stable ids를 갖는 경우, attached scrap, view cache를 재탐색 6. ViewCacheExtension 탐색 7. RecycledViewPool 탐색 8. 7번까지 찾지 못했다면 새로운 ViewHolder 생성 (onCreateViewHolder) 9. ViewHolder에 바인딩이 필요하다면 바인딩 실행 (onBindViewHolder)
  7. ViewHolder 탐색 순서 Session Android Team 1. changed scrap 탐색

    2. attached scrap 탐색 3. hidden view 탐색 4. view cache 탐색 5. stable ids를 갖는 경우, attached scrap, view cache를 재탐색 6. ViewCacheExtension 탐색 7. RecycledViewPool 탐색 8. 7번까지 찾지 못했다면 새로운 ViewHolder 생성 (onCreateViewHolder) 9. ViewHolder에 바인딩이 필요하다면 바인딩 실행 (onBindViewHolder)
  8. @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ...

    ViewHolder holder = null; // 1) Find by position from scrap/hidden list/cache if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); ... } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); ... final int type = mAdapter.getItemViewType(offsetPosition); ... if (holder == null) { // fallback to pool ... holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); ... } } if (holder == null) { ... holder = mAdapter.createViewHolder(RecyclerView.this, type); ... } } ... boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } ... return holder; } RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline 1. view cache 탐색 2. RecycledViewPool 탐색 3. 7번까지 찾지 못했다면 새로운 ViewHolder 생성 (onCreateViewHolder) 4. ViewHolder에 바인딩이 필요하다면 바인딩 실행 (onBindViewHolder)
  9. public final class Recycler { ... final ArrayList<ViewHolder> mCachedViews =

    new ArrayList<ViewHolder>(); int mViewCacheMax = DEFAULT_CACHE_SIZE; static final int DEFAULT_CACHE_SIZE = 2; ... } View Cache Session Android Team 1. mCachedViews에 ViewHolder 캐싱 2. 기본 cache size : 2 3. position 기반의 캐싱 및 탐색 4. Queue와 유사하게 FIFO로 저장됨
  10. @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ...

    ViewHolder holder = null; // 1) Find by position from scrap/hidden list/cache if (holder == null) { holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); ... } ... return holder; } RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline 1. view cache 탐색 2. RecycledViewPool 탐색 3. 7번까지 찾지 못했다면 새로운 ViewHolder 생성 (onCreateViewHolder) 4. ViewHolder에 바인딩이 필요하다면 바인딩 실행 (onBindViewHolder)
  11. RecyclerView.Recycler#getScrapOrHiddenOrCachedHolderForPosition ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount

    = mAttachedScrap.size(); ... // Search in our first-level recycled view cache. final int cacheSize = mCachedViews.size(); for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); // invalid view holders may be in cache if adapter has stable ids as they can be // retrieved via getScrapOrCachedViewForId if (!holder.isInvalid() && holder.getLayoutPosition() == position && !holder.isAttachedToTransitionOverlay()) { if (!dryRun) { mCachedViews.remove(i); } ... return holder; } } return null; }
  12. public final class Recycler { ... RecycledViewPool mRecyclerPool; ... }

    public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray<ScrapData> mScrap = new SparseArray<>(); ... } RecycledViewPool Session Android Team 1. RecycledViewPool 클래스에서 관리 2. 기본 pool size : 5 3. 별도의 Heap을 가짐 4. ViewType 기반의 캐싱 및 탐색 5. Stack과 유사하게 LIFO로 저장됨
  13. @Nullable ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ...

    if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); final int type = mAdapter.getItemViewType(offsetPosition); ... if (holder == null) { // fallback to pool holder = getRecycledViewPool().getRecycledView(type); ... } ... } ... return holder; } RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline 1. view cache 탐색 2. RecycledViewPool 탐색 3. 7번까지 찾지 못했다면 새로운 ViewHolder 생성 (onCreateViewHolder) 4. ViewHolder에 바인딩이 필요하다면 바인딩 실행 (onBindViewHolder)
  14. RecyclerView.RecycledViewPool#getRecycledView @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; } 역순으로 탐색 (LIFO)
  15. 대표적인 예시 Session Android Team 1번째 ViewHolder(position 0)는 pool에 들어감과

    동시에 재활용 됨 : 단일 ViewType일 경우 거의 pool이 비어있다고 볼 수 있음
  16. 중간 정리 Session Android Team View Cache는 position을 기반으로 캐싱

    및 탐색 Pool은 ViewType을 기반으로 캐싱 및 탐색 View Cache에 들어간 ViewHolder는 데이터에 대한 바인딩을 해제하거나 초기화 하지 않음 Pool에 들어간 ViewHolder는 데이터에 대한 바인딩을 해제 및 초기화 하기 때문에 재바인딩 필요
  17. 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); } } RecyclerView.Recycler#tryGetViewHolderForPositionByDeadline public void setViewCacheSize(int viewCount) { mRequestedCacheMax = viewCount; updateViewCacheSize(); } void updateViewCacheSize() { int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0; mViewCacheMax = mRequestedCacheMax + extraCache; // first, try the views that can be recycled for (int i = mCachedViews.size() - 1; i >= 0 && mCachedViews.size() > mViewCacheMax; i--) { recycleCachedViewAt(i); } } cache, pool 사이즈 늘리기 Session Android Team 그리드 형태의 RecyclerView일 경우 사이즈 조절은 필수 RecycledViewPool View Cache
  18. 다루지 못한 RecyclerView 관련 중요한 내용들 Session Android Team 1.

    클릭 이벤트는 어디서 처리하는 것이 좋은가? 2. 중첩 스크롤 처리는 어떻게 하는 것이 좋은가? (feat. NestedScrollView) 3. Multi ViewType 부터 ConcatAdapter까지 4. Payload 관련 + NotifyDataSetChanged를 지양하는 이유 + DiffUtil