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

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

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

  1. 滑动 RecyclerView 下的缓存机制
  2. RecyclerView 初次加载过程的缓存机制

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

一、缓存层级

背景知识:负责回收和复用 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 mAttachedScrap = new ArrayList<>();// 存放可见范围内的 ViewHolder (但是在 onLayoutChildren 的时候,会将所有 View 都会缓存到这), 从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。
    ArrayList mChangedScrap = null;// 存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。

    final ArrayList mCachedViews = new ArrayList(); // 存放 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();

类型

变量名

存储说明

备注

Scrap

mAttachedScrap

存放可见范围内的 ViewHolder

从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。在 onLayoutChildren 的时候,会将所有 View 都会缓存到这

mChangedScrap

存放可见范围内并且数据发生了变化的 ViewHolder

从这里复用的 ViewHolder 需要重新绑定数据。


Cache

mCachedViews

存放 remove 掉的 ViewHolder

从这里复用的 ViewHolder 如果 position 或者 id 对应的上,则不需要重新绑定数据。

ViewCacheExtension

mViewCacheExtension

自定义缓存


RecycledViewPool

mRecyclerPool

存放 remove 掉,并且重置了数据的 ViewHolder

从这里复用的 ViewHolder 需要重新绑定数据。

二、场景分析:滑动中的 RecyclerView 缓存机制

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

入口是 ouTouchEvent

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

方法名

隶属的类

作用描述

onTouchEvent()

RecyclerView

处理点击事件,在 MOVE 事件中在一定条件下,拦截事件后,做事件处理

scrollByInternal()

RecyclerView

主要是调用 scrollStep()

scrollStep()

RecyclerView

通过 dx 和 dy 的值判断是调用scrollHorizontallyBy()还是 scrollVerticallyBy()

scrollHorizontallyBy()/scrollVerticallyBy()

LayoutManager

主要是调用 scrollBy()

scrollBy()

LayoutManager

通过调用 fill() 添加滑进来的View 和回收滑出去的 View

offsetChildrenVertical()/offsetChildrenHorizontal()

RecyclerView

做偏移操作

通过上述表格,我们知道了。最重要的东西那就是 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;
}
复制代码

总结

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

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

最后

这里也分享一些珍藏资源,面试简历模板到大厂面经汇总,从大厂内部技术资料到互联网高薪必读书单,以及Android面试核心知识点(844页)和Android面试题合集2022年最新版(354页)等等,这些资料整理给大家,希望踩过的坑不要再踩,遭遇的技术瓶颈一次性消灭。

如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!

部分内容展示如下

01.Android必备底层技术

  • Java序列化:Serializable原理、Parcelable接口原理、Json、XML
  • 注解、泛型与反射:自定义注解、注解的使用、泛型擦除机制、泛型边界、Java方法与Arm指令、Method反射源码、invoke方法执行原理
  • 虚拟机:JVM垃圾回收器机制、JVM内存分配策略、Android虚拟机与JVM底层区别、虚拟机底层Odex本地指令缓存机制、虚拟机如何分别加载class与object、虚拟机类加载模型
  • 并发:Java线程本质讲解、线程原理、线程通信、UnSafe类、线程池
  • 编译时技术:OOP面向切面之AspectJ、字节码手术刀JavaSSit实战、字节码插桩技术(ASM)实战
  • 动态代理:动态代理实现原理、动态代理在虚拟机中运行时动态拼接Class字节码分析、ProxyGenerator生成字节码流程
  • 高级数据结构与算法:HashMap源码、ArrayList源码、排序算法
  • Java IO:Java IO体系、IO文件操作

02.Framework

  • Binder:Linux内存基础、Binder四层源码分析、Binder机制、Binder进程通信原理
  • Handler:Loop消息泵机制、Message解析
  • Zygote:init进程与Zygote进程、Zygote启动流程、Socket通信模式、APP启动过程
  • AMS:ActivityThread源码分析、AMS与ActivityThread通信原理、Activity启动机制
  • PMS:PMS源码、APK安装过程分析、PMS对安装包的解析原理
  • WMS:PhoneWindow实例化流程、DecorView创建过程、ViewRootImpl渲染机制

03.Android常用组件

  • Activty:Activity管理栈与Activity的启动模式、Activity生命周期源码分析
  • Fragment:Fragment生命周期深入详解、Fragment事务管理机制详解、性能优化相关方案
  • Service:Service启动模式分析、Service管理与通信方案、Service生命周期底层详解

04.高级UI

  • UI绘制原理:setContentView()方法下到底做了什么、AppCompatActivity与Activity的区别、UI测量、布局、绘制的底层执行流程
  • 插件换肤:LayoutInflater加载布局分析、Android资源的加载机制、Resource与AssetManager
  • 事件分发机制原理:事件执行U形链与L形链、事件拦截原理
  • 属性动画:VSYNC刷新机制、ObjectAnimator与ValueAnimator源码讲解、Android属性动画:插值器与估值器
  • RecycleView:布局管理器LayoutManager详解、回收池设计思想、适配器模式原理
  • 高阶贝塞尔曲线

05.Jetpack

  • Lifecycle:Lifecycle源码、Lifecycle高阶应用
  • ViewModel:ViewModel源码、ViewModel应用技巧
  • LiveData:LiveData源码
  • Navigation:Navigation源码
  • Room:Room源码、Room+LiveData监听数据库数据变更刷新页面原理
  • WorkManager内核
  • Pagging原理
  • DataBinding:单向绑定、双向绑定、如何与RecyclerView的配合使用、底层原理

06.性能优化

  • 启动优化:系统启动原理、Trace工具分析启动卡顿、类重排机制、资源文件重排机制
  • 内存优化
  • UI渲染优化:UI层级规范及对UI加载的影响、UI卡顿原因及修复、UI绘制、布局、测量原因以及处理方案
  • 卡顿优化:造成卡顿的原因分析、内存抖动与GC回收、回收算法
  • 耗电优化
  • 崩溃优化:项目崩溃异常捕获、优雅的异常处理方案、如何避免异常弹框
  • 安全优化:APP加固实现(防反编译,dex加固)、https防抓包机制(数据传输加载,客户端服务器端双向加密校验)
  • 网络优化:serializable原理、parcelable接口原理、http与https原理详解、protbuffer网络IO详解、gzip压缩方案
  • 大图加载优化:Glide巨图加载机制原理分析、大图多级缓存实现方案
  • 多线程并发优化
  • 储存优化:Android文件系统-sdcard与内存存储、Shared Preference原理、MMAP内存映射
  • 安装包优化:shrinkResources去除无用资源、合理设置多语言、webp实现图片瘦身、合理配置armable-v7的so库、Lint检查工具实践

如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!

07.音视频

  • C/C++:数据类型、数组、内存布局、指针、函数、预处理器、结构体、共用体、容器、类型转换、异常、文件流操作、线程
  • H.265/H.265:音视频格式封装原理、编码原理、视频流H264的组装原理切片NAL单元、视频流H264码流分析、切片与宏快,运动矢量、信源编码器、高频滤波、帧间拆分与帧内预测、CTU,PU TU编码结构、DSP芯片解码流程、MediaPlayer与DSP芯片交互机制、投屏架构、MediaProjection与MeidiaCodec交互机制、H265码流交换
  • MediaCodec:dsp芯片、编解码器的生命周期、解码器中输入队列与解析队列设计思想、MediaCodec中平缓解码解析、MediaExtractor 多路复用、MediaMuxer合成器、MediaFormat格式
  • 音视频剪辑:视频剪辑、音频剪辑、音频合成、音谱显示、视频倒放
  • 音视频直播:硬编码、软编码、native实现rtmp推流、摄像头预览帧编码NV21转YUV、视频画面封装拼接Packet包、音频流数据拼接Packet包、RtmpDump实时同步发送音视频数据、MediaProjection、Medicodec编码H264码流、rtmp推流
  • OpenGL与音视频解码:OpenGL绘制流程、矩阵、Opencv详解、人脸识别效果实现
  • OpenGL特效:CPU与GPU运行机制详解、世界坐标,布局坐标,与FBO坐标系、图像镜像与旋转处理、人脸定位与关键点定位、大眼效果、贴纸效果、美颜效果
  • FFmpeg万能播放器:FFmpeg结构体、声音播放原理、Surface的渲染、像素绘制原理与对齐机制、音视频同步原理、视频播放器整体架构
  • Webrtc音视频通话:WebRtc服务端环境搭建与Webrtc编译、1v1视频通话实现方案、群聊视频通话实现思路、多对多视频会议实现、1V1音视频通话实现

08.开源框架原理

  • Okhttp
  • Retrofit
  • RxJava
  • Glide
  • Hilt
  • Dagger2
  • EventBus
  • 组件化、插件化、热修复等

09.Gradle

  • Groovy语法
  • Gradle Android插件配置
  • Gradle实践等

10.kotlin

  • Kotlin语法
  • 扩展使用
  • 进阶使用
  • 实践等

11.Flutter

  • Dart语法
  • UI
  • 进阶使用
  • 优化
  • 实践等

12.鸿蒙

  • Ability组件
  • 分布式任务
  • 事件总线
  • 鸿蒙线程
  • UI自定义控件等

如果需要的话,可以顺手帮我点赞评论一下,直接私信我【笔记】免费领取!

Android路漫漫,共勉!

发表评论
留言与评论(共有 0 条评论) “”
   
验证码:

相关文章

推荐文章