Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

Jaesung Lee

June 01, 2023
Tweet

More Decks by Jaesung Lee

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

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

    View Slide

  4. RecyclerView의 구조
    Session Android Team

    View Slide

  5. LayoutManager
    Session Android Team

    View Slide

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

    View Slide

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

    View Slide

  8. @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

    View Slide

  9. 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)

    View Slide

  10. 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)

    View Slide

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

    View Slide

  12. 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로 저장됨

    View Slide

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

    View Slide

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

    View Slide

  15. 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로 저장됨

    View Slide

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

    View Slide

  17. 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)

    View Slide

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

    View Slide

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

    View Slide

  20. ViewHolder 저장
    Session Android Team

    View Slide

  21. Session Android Team
    ViewHolder 저장

    View Slide

  22. Session Android Team
    ViewHolder 저장

    View Slide

  23. Pool and Cache in Action
    Session Android Team

    View Slide

  24. 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

    View Slide

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

    View Slide

  26. 2023.06.01
    Thank you.
    Android Team 이재

    Mash-Up 13th
    Android Seminar

    View Slide