Android RecyclerView 绘制流程及Recycler缓存(下)

简介: 缓存复用原理 Recycler 缓存复用是 RecyclerView 中另一个非常重要的机制,这套机制主要实现了 ViewHolder 的缓存以及复用。

缓存复用原理 Recycler


       缓存复用是 RecyclerView 中另一个非常重要的机制,这套机制主要实现了 ViewHolder 的缓存以及复用。


       核心代码是在 Recycler 中完成的,它是 RecyclerView 中的一个内部类,主要用来缓存屏幕内 ViewHolder 以及部分屏幕外 ViewHolder,部分代码如下:


public final class Recycler {
        //未与RecyclerView分离的ViewHolder列表
        final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
        //与RecyclerView分离的ViewHolder列表
        ArrayList<ViewHolder> mChangedScrap = null;
        //ViewHolder缓存列表
        final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
        //ViewHolder缓存池
        RecycledViewPool mRecyclerPool;
        //开发者可以控制的ViewHolder缓存的帮助类
        private ViewCacheExtension mViewCacheExtension;
        //默认情况下缓存个数是 2
        static final int DEFAULT_CACHE_SIZE = 2;
    }


       Recycler 的缓存机制就是通过上图中的这些数据容器来实现的,实际上 Recycler 的缓存也是分级处理的,根据访问优先级从上到下可以分为 4 级,如下:


  • 第一级缓存 mAttachedScrap&mChangedScrap


  • 第二级缓存 mCachedViews


  • 第三级缓存 ViewCacheExtension


  • 第四级缓存 RecycledViewPool


各级缓存功能


       RecyclerView 之所以要将缓存分成这么多块,是为了在功能上进行一些区分,并分别对应不同的使用场景。


第一级缓存 mAttachedScrap&mChangedScrap


       是两个名为 Scrap 的 ArrayList,这两者主要用来缓存屏幕内的 ViewHolder。为什么屏幕内的 ViewHolder 需要缓存呢?


       做过 App 开发的应该都熟悉通过下拉刷新列表中的内容,当刷新被触发时,只需要在原有的 ViewHolder 基础上进行重新绑定新的数据 data 即可,而这些旧的 ViewHolder 就是被保存在 mAttachedScrap 和 mChangedScrap 中。实际上当我们调用 RV 的 notifyXXX 方法时,就会向这两个列表进行填充,将旧 ViewHolder 缓存起来。


第二级缓存 mCachedViews


       它用来缓存移除屏幕之外的 ViewHolder,默认情况下缓存个数是 2,不过可以通过 setViewCacheSize 方法来改变缓存的容量大小。如果 mCachedViews 的容量已满,则会根据 FIFO(先进先出) 的规则将旧 ViewHolder 抛弃,然后添加新的 ViewHolder。


       通常情况下刚被移出屏幕的 ViewHolder 有可能接下来马上就会使用到,所以 RecyclerView 不会立即将其设置为无效 ViewHolder,而是会将它们保存到 cache 中,但又不能将所有移除屏幕的 ViewHolder 都视为有效 ViewHolder,所以它的默认容量只有 2 个。


第三级缓存 ViewCacheExtension


       这是 RecyclerView 预留给开发人员的一个抽象类,在这个类中只有一个抽象方法。


public abstract static class ViewCacheExtension {
        @Nullable
        public abstract View getViewForPositionAndType(@NonNull Recycler recycler, int position,
                int type);
    }


       开发人员可以通过继承 ViewCacheExtension,并复写抽象方法 getViewForPositionAndType 来实现自己的缓存机制。一般情况下我们不会自己实现也不建议自己去添加缓存逻辑,因为这个类的使用门槛较高(牛掰的人请忽略)。


第四级缓存 RecycledViewPool


       RecycledViewPool 同样是用来缓存屏幕外的 ViewHolder,当 mCachedViews 中的个数已满(默认为 2),则从 mCachedViews 中淘汰出来的 ViewHolder 会先缓存到 RecycledViewPool 中。ViewHolder 在被缓存到 RecycledViewPool 时,会将内部的数据清理,因此从 RecycledViewPool 中取出来的 ViewHolder 需要重新调用 onBindViewHolder 绑定数据。这就同最早的 ListView 中的使用 ViewHolder 复用 convertView 的道理是一致的,因此 RV 也算是将 ListView 的优点完美的继承过来。


       RecycledViewPool 还有一个重要功能,官方对其有如下解释:


RecycledViewPool 允许您在多个 RecyclerView 之间共享视图。

如果要跨 RecyclerViews 回收视图,请创建 RecycledViewPool 的实例并使用RecyclerView.setRecycledViewPool(RecycledViewPool)。

如果您不提供,RecyclerView 会自动为自己创建一个池。


       可以看出,多个 RecycledView之间可以共享一个 RecycledViewPool,这对于多 tab 界面的优化效果会很显著。需要注意的是,RecycledViewPool 是根据 type 来获取 ViewHolder,每个 type 默认最大缓存 5 个。因此多个 RecyclerView 共享 RecycledViewPool 时,必须确保共享的 RecyclerView 使用的 Adapter 是同一个,或 view type 是不会冲突的。


RecyclerView 是如何从缓存中获取 ViewHolder 的


       在上面介绍 onLayout 阶段时,有介绍在 layoutChunk 方法中通过调用 layoutState.next 方法拿到某个子 ItemView,然后添加到 RecyclerView 中。


LinearLayoutManager.LayoutState.next


    View next(RecyclerView.Recycler recycler) {
        if (mScrapList != null) {
            return nextViewFromScrapList();
        }
        final View view = recycler.getViewForPosition(mCurrentPosition);
        mCurrentPosition += mItemDirection;
        return view;
    }
    //继续跟getViewForPosition(mCurrentPosition);
    @NonNull
    public View getViewForPosition(int position) {
        return getViewForPosition(position, false);
    }
    //继续跟getViewForPosition
    View getViewForPosition(int position, boolean dryRun) {
        return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
    }


 可以看出最终调用 tryGetViewHolderForPositionByDeadline 方法来查找相应位置上的ViewHolder。


tryGetViewHolderForPositionByDeadline


       在这个方法中会从上面介绍的 4 级缓存中依次查找:


    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
        ...
        ViewHolder holder = null;
        // 1)根据位置从scrap和mCachedViews返回位置的视图。
        if (holder == null) {
            //根据位置从scrap和mCachedViews返回位置的视图。
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        }
        if (holder == null) {
            //获取这个位置的数据的类型(子View复写的方法)
            final int type = mAdapter.getItemViewType(offsetPosition);
            // 2)根据ID在scrap和cache 中查找
            if (mAdapter.hasStableIds()) {
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);
            }
            //从ViewCacheExtension获取ViewHolder
            if (holder == null && mViewCacheExtension != null) {
                 //返回 绑定到给定位置的视图,如果没有可重用的视图,则为NULL
                final View view = mViewCacheExtension.getViewForPositionAndType(this, position, type);
            }
            //3)从RecycledViewPool获取ViewHolder
            if (holder == null) {
                //4)从RecycledViewPool获取ViewHolder
                holder = getRecycledViewPool().getRecycledView(type);
            }
            if (holder == null) {
                //5)如果在各级缓存中都没有找到相应的 ViewHolder,则会使用 Adapter 中的 createViewHolder 方法创建一个新的 ViewHolder。
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
            }
        }
        ...
        return holder;
    }


  代码中1-4在各级缓存中都没有找到相应的 ViewHolder,则会使用 Adapter 中的 createViewHolder 方法创建一个新的 ViewHolder。


何时将 ViewHolder 存入缓存


       接下来看下 ViewHolder 被存入各级缓存的场景。


第一次 layout


       当调用 setLayoutManager 和 setAdapter 之后,RecyclerView 会经历第一次 layout 并被显示到屏幕上,此时并不会有任何 ViewHolder 的缓存,所有的 ViewHolder 都是通过 createViewHolder 创建的。


刷新列表


       通过手势下拉刷新,获取到新的数据 data 之后,我们会调用 notifyXXX 方法通知 RecyclerView 数据发生改变。


       RecyclerView 会先将屏幕内的所有 ViewHolder 保存在 Scrap 中。


       当缓存执行完之后,后续通过 Recycler 就可以从缓存中获取相应 position 的 ViewHolder(姑且称为旧 ViewHolder)。


       然后将刷新后的数据设置到这些 ViewHolder 上,再将新的 ViewHolder 绘制到 RecyclerView 。


       RecyclerView 的代码极其庞大,理解 RecyclerView 的源码实现,有助于我们快速定位问题原因、拓展 RecyclerView 功能、提高分析 RecyclerView 性能问题的能力,感兴趣的可以自己去捣鼓高估。


以上就是本文的全部内容啦,希望对你有所帮助。


相关文章
|
4月前
|
缓存 安全 Android开发
Android经典实战之用Kotlin泛型实现键值对缓存
本文介绍了Kotlin中泛型的基础知识与实际应用。泛型能提升代码的重用性、类型安全及可读性。文中详细解释了泛型的基本语法、泛型函数、泛型约束以及协变和逆变的概念,并通过一个数据缓存系统的实例展示了泛型的强大功能。
46 2
|
2月前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
81 15
Android 系统缓存扫描与清理方法分析
|
2月前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
94 6
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
|
2月前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
Android面试高频知识点(4) 详解Activity的启动流程
31 3
|
2月前
|
XML 前端开发 Android开发
Android面试高频知识点(3) 详解Android View的绘制流程
Android面试高频知识点(3) 详解Android View的绘制流程
29 2
|
2月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
51 3
|
2月前
|
Android开发
Android面试之Activity启动流程简述
Android面试之Activity启动流程简述
21 0
|
3月前
|
消息中间件 Android开发 索引
Android面试高频知识点(4) 详解Activity的启动流程
讲解Activity的启动流程了,Activity的启动流程相对复杂一下,涉及到了Activity中的生命周期方法,涉及到了Android体系的CS模式,涉及到了Android中进程通讯Binder机制等等, 首先介绍一下Activity,这里引用一下Android guide中对Activity的介绍:
59 4
|
3月前
|
Android开发 开发者 索引
Android实战经验之如何使用DiffUtil提升RecyclerView的刷新性能
本文介绍如何使用 `DiffUtil` 实现 `RecyclerView` 数据集的高效更新,避免不必要的全局刷新,尤其适用于处理大量数据场景。通过定义 `DiffUtil.Callback`、计算差异并应用到适配器,可以显著提升性能。同时,文章还列举了常见错误及原因,帮助开发者避免陷阱。
269 9