美文网首页Android技术知识
真的很难理解?RecyclerView 缓存机制到底是几级缓存?

真的很难理解?RecyclerView 缓存机制到底是几级缓存?

作者: 搬砖小老弟 | 来源:发表于2022-07-27 15:24 被阅读0次

    0.前言

    RecyclerView 的缓存机制,可谓是面试中的常客了。不仅如此,在使用过程中,如果了解这个缓存机制,那么可以更好地利用其特性做开发。

    那么,我们将以场景化的方式,讲解 RecyclerView 的缓存机制。常见的两个场景是:

    1.滑动 RecyclerView 下的缓存机制

    2.RecyclerView 初次加载过程的缓存机制

    本文将讲解 滑动 RecyclerView 下 的缓存机制

    1.缓存层级

    背景知识:负责回收和复用 ViewHolder 的类是 Recycler,负责缓存的主要就是这个类的几个成员变量。我们贴点源码看看(下面源码的注释(和我写的注释),很重要,要记得认真看哦)

    /**
     * A Recycler is responsible for managing scrapped or detached item views for reuse.
     * A "scrapped" view is a view that is still attached to its parent RecyclerView but that has been marked for removal or reuse.
     * 
     * Typical use of a Recycler by a RecyclerView.LayoutManager will be to obtain views 
     * for an adapter's data set representing the data at a given position or item ID. 
     * If the view to be reused is considered "dirty" the adapter will be asked to rebind it.
     * If not, the view can be quickly reused by the LayoutManager with no further work. 
     * Clean views that have not requested layout may be repositioned by a LayoutManager without remeasurement.
     */
    public final class Recycler {
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();// 存放可见范围内的 ViewHolder (但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这), 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
        ArrayList<ViewHolder> mChangedScrap = null;// 存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。
    
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); // 存放 remove 掉的 ViewHolder,从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
    
        private int mRequestedCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2
        int mViewCacheMax = DEFAULT_CACHE_SIZE; // 默认值是 2
    
        RecycledViewPool mRecyclerPool; // 存放 remove 掉,并且重置了数据的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。 // 默认值大小是 5 
    
        private ViewCacheExtension mViewCacheExtension; // 自定义的缓存
        }
    

    至于到底有几级缓存,我觉得这个问题不大重要。有人说三层,有人说四层。有人说三层,因为觉得自定义那层,不是 RecyclerView 实现的,所以不算;也有人认为 Scrap 并不是真正的缓存,所以不算。

    从源码看来,我更同意后者,Scrap 不算一层缓存。因为在源码中,mCachedViews 被称为 first-level。至于为什么 Scrap 不算一层,我的理解是:因为这层的只是 detach 了,并没有 remove,所以这层也没有缓存大小的概念,只要符合规则就会加入进去。

    // Search the first-level cache
    final int cacheSize = mCachedViews.size();
    

    2.场景分析:滑动中的 RecyclerView 缓存机制

    通过 Android Studio 的 Profiles 工具,我们可以看到调用流程

    入口是 ouTouchEvent

    通过表格的方式,简要说明上图的流程都在做什么?

    通过上述表格,我们知道了。最重要的东西那就是 scrollBy 中调用了 fill 的方法了。那我们看看 fill 在做什么吧?滑出去的 View 最后去哪里了呢?滑进来的 View 是怎么来的?(带着这个问题,我们一起来读源码!一定要带着),源码只留下了核心部分

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        // max offset we should set is mFastScroll + available
        final int start = layoutState.mAvailable;
        //首选该语句块的判断,判断当前状态是否为滚动状态,如果是的话,则触发 recycleByLayoutState 方法
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // TODO ugly bug fix. should not happen
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 分析1----回收
            recycleByLayoutState(recycler, layoutState);
            }
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            //分析2----复用
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
        }
    }
    

    // 分析1----回收 
    // 通过一步步追踪,我们发现最后调用的是 removeAndRecycleViewAt() 
    public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
        final View view = getChildAt(index);
        //分析1-1
        removeViewAt(index);
        //分析1-2
        recycler.recycleView(view);
    }
    // 分析1-1
    // 从 RecyclerView 移除一个 View 
    public void removeViewAt(int index) {
        final View child = getChildAt(index);
        if (child != null) {
            mChildHelper.removeViewAt(index);
        }
    }
    //分析1-2 
    // recycler.recycleView(view) 最终调用的是 recycleViewHolderInternal(holder) 进行回收 VH (ViewHolder)
    void recycleViewHolderInternal(ViewHolder holder) {
        if (forceRecycle || holder.isRecyclable()) {
            //判断是否满足放进 mCachedViews 
            if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID| ViewHolder.FLAG_REMOVED| ViewHolder.FLAG_UPDATE| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)){
                // 判断 mCachedViews 是否已满
                if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                    // 如果满了就将下标为0(即最早加入的)移除,同时将其加入到 RecyclerPool 中
                    recycleCachedViewAt(0);
                    cachedViewSize--;
                    }  
                mCachedViews.add(targetCacheIndex, holder);
                cached = true;
                }
            //如果没有满足上面的条件,则直接存进 RecyclerPool 中    
            if (!cached) {
                addViewHolderToRecycledViewPool(holder, true);
                recycled = true;
             } 
         }
    }
    

    //分析2
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
            LayoutState layoutState, LayoutChunkResult result) {
        //分析2-1
        View view = layoutState.next(recycler);
        if (layoutState.mScrapList == null) {
            if (mShouldReverseLayout == (layoutState.mLayoutDirection
                    == LayoutState.LAYOUT_START)) {
                //添加到 RecyclerView 上
                addView(view);
            } else {
                addView(view, 0);
            }
        }
    }
    //分析2-1
    //layoutState.next(recycler) 最后调用的是 tryGetViewHolderForPositionByDeadline() 这个方法正是 复用 核心的方法
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
            boolean dryRun, long deadlineNs) {
        // 0) If there is a changed scrap, try to find from there
        // 例如:我们调用 notifyItemChanged 方法时
        if (mState.isPreLayout()) {
            // 如果是 changed 的 ViewHolder 那么就先从 mChangedScrap 中找
            holder = getChangedScrapViewForPosition(position);
            fromScrapOrHiddenOrCache = holder != null;
        }
        // 1) Find by position from scrap/hidden list/cache
        if (holder == null) {
            //如果在上面没有找到(holder == null),那就尝试从通过 pos 在 mAttachedScrap/ mHiddenViews / mCachedViews 中获取
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }
        if (holder == null) {
            // 2) Find from scrap/cache via stable ids, if exists
            if (mAdapter.hasStableIds()) {
                //如果在上面没有找到(holder == null),那就尝试从通过 id 在 mAttachedScrap/ mCachedViews 中获取
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            }
            if (holder == null && mViewCacheExtension != null) {
                //这里是通过自定义缓存中获取,忽略
            }
            //如果在上面都没有找到(holder == null),那就尝试在 RecycledViewPool 中获取
            if (holder == null) { // fallback to pool
                holder = getRecycledViewPool().getRecycledView(type);
                if (holder != null) {
                    //这里拿的是,要清空数据的
                    holder.resetInternal();
                }
            }
            //如果在 Scrap / Hidden / Cache / RecycledViewPool 都没有找到,那就只能创建一个了。
            if (holder == null) {
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
            }
        }
        return holder;
    }
    

    3.总结

    做一个总结,在分析源码前,我们提出了三个问题,那看看答案是什么吧

    Q:那我们看看 fill 在做什么吧?
    
    A:其实就是分析1(回收 ViewHolder ) + 分析 2 ( 复用 ViewHolder )
    

    Q:滑出去的 View 最后去哪里了呢?
    
    A:先尝试回收到 mCachedViews 中,未成功,则回收到 RecycledViewPool 中。
    

    Q:滑进来的 View 是怎么来的?
    
    A:如果是 isPreLayout 则先从 mChangedScrap 中尝试获取。
    
    未获取到,再从 mAttachedScrap / mHiddenViews / mCachedViews (通过 position ) 中尝试获取
    
    未获取到,再从 mAttachedScrap / mCachedViews (通过 id)中尝试获取
    
    未获取到,再从 自定义缓存中尝试获取
    
    未获取到,再从 RecycledViewPool 中尝试获取
    
    未获取到,创建一个新的 ViewHolder
    

    推荐阅读:

    真的服了!阿里面试4轮,RecyclerView问了七遍!!!

    Compose与RecyclerView结合效果会是怎样的?

    相关文章

      网友评论

        本文标题:真的很难理解?RecyclerView 缓存机制到底是几级缓存?

        本文链接:https://www.haomeiwen.com/subject/ttezirtx.html