Slide 1

Slide 1 text

2023.06.01 RecyclerView가 ViewHolder를 재활용하는 방법 Mash-Up 13th Android Seminar Android Team 이재 성

Slide 2

Slide 2 text

필요 기반 지식 Session Android Team 1. ListView와 RecyclerView의 성능적 차이점을 이해하고 있다. 2. RecyclerView를 만드는 방법을 알고 있다. 3. RecyclerView Adapter에서 반드시 override해야 하는 메서드들의 의미를 알고있다.

Slide 3

Slide 3 text

RecyclerView ins and outs - Google I/O 2016 Session Android Team https://www.youtube.com/watch?v=LqBlYJTfLP4

Slide 4

Slide 4 text

RecyclerView의 구조 Session Android Team

Slide 5

Slide 5 text

LayoutManager Session Android Team

Slide 6

Slide 6 text

/** * 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

Slide 7

Slide 7 text

@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

Slide 8

Slide 8 text

@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

Slide 9

Slide 9 text

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)

Slide 10

Slide 10 text

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)

Slide 11

Slide 11 text

@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)

Slide 12

Slide 12 text

public final class Recycler { ... final ArrayList mCachedViews = new ArrayList(); 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로 저장됨

Slide 13

Slide 13 text

@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)

Slide 14

Slide 14 text

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; }

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

@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)

Slide 17

Slide 17 text

RecyclerView.RecycledViewPool#getRecycledView @Nullable public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = mScrap.get(viewType); if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { final ArrayList scrapHeap = scrapData.mScrapHeap; for (int i = scrapHeap.size() - 1; i >= 0; i--) { if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) { return scrapHeap.remove(i); } } } return null; } 역순으로 탐색 (LIFO)

Slide 18

Slide 18 text

대표적인 예시 Session Android Team 1번째 ViewHolder(position 0)는 pool에 들어감과 동시에 재활용 됨 : 단일 ViewType일 경우 거의 pool이 비어있다고 볼 수 있음

Slide 19

Slide 19 text

중간 정리 Session Android Team View Cache는 position을 기반으로 캐싱 및 탐색 Pool은 ViewType을 기반으로 캐싱 및 탐색 View Cache에 들어간 ViewHolder는 데이터에 대한 바인딩을 해제하거나 초기화 하지 않음 Pool에 들어간 ViewHolder는 데이터에 대한 바인딩을 해제 및 초기화 하기 때문에 재바인딩 필요

Slide 20

Slide 20 text

ViewHolder 저장 Session Android Team

Slide 21

Slide 21 text

Session Android Team ViewHolder 저장

Slide 22

Slide 22 text

Session Android Team ViewHolder 저장

Slide 23

Slide 23 text

Pool and Cache in Action Session Android Team

Slide 24

Slide 24 text

public void setMaxRecycledViews(int viewType, int max) { ScrapData scrapData = getScrapDataForType(viewType); scrapData.mMaxScrap = max; final ArrayList 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

Slide 25

Slide 25 text

다루지 못한 RecyclerView 관련 중요한 내용들 Session Android Team 1. 클릭 이벤트는 어디서 처리하는 것이 좋은가? 2. 중첩 스크롤 처리는 어떻게 하는 것이 좋은가? (feat. NestedScrollView) 3. Multi ViewType 부터 ConcatAdapter까지 4. Payload 관련 + NotifyDataSetChanged를 지양하는 이유 + DiffUtil

Slide 26

Slide 26 text

2023.06.01 Thank you. Android Team 이재 성 Mash-Up 13th Android Seminar