RecycleView 缓存机制
Posted 不会写代码的丝丽
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RecycleView 缓存机制相关的知识,希望对你有一定的参考价值。
参考文献
您可以先看看下面的文章后,再看博主的也许有更大的收货.
前言
-
本文所分析的源码版本:
androidx.recyclerview:recyclerview:1.1.0
-
缓存本质思想:
缓存
是一种空间换时间
的算法
思想. -
recyclerview
缓存了什么:
缓存了ViewHolder
对象. -
recyclerview
缓存的目的:- 复用视图和数据的绑定(减少
onBindViewHolder
) - 复用
ViewHolder
对象(减少onCreateViewHolder
调用)
- 复用视图和数据的绑定(减少
上面说到缓存的对象是ViewHold
,所以我们必须弄清楚什么是ViewHolder
,他解决了什么问题?
ViewHolder
基础功能:
- 1 他是
View
的持有者,在构造时直接findviewById
,减少后面再次寻找id控件所带来的的时间损失(寻找id采用深度优先算法)
class MyViewHold constructor(val view: View) : RecyclerView.ViewHolder(view) {
//构造时直接寻找一个子控件
val tv: TextView = view.findViewById(R.id.tv);
}
- 2 封装一些基础操作函数
class MyViewHold constructor(val view: View) : RecyclerView.ViewHolder(view) {
//构造时直接寻找一个子控件
val tv: TextView = view.findViewById(R.id.tv);
/**
* 更新信息
*/
fun upInfo(msg:String){
tv.setText(msg)
}
}
- 3 关联数据和视图的中间组件
class RVAdapter(val data: List<String>) : RecyclerView.Adapter<MyViewHold>(){
//更新数据和视图的绑定关系
override fun onBindViewHolder(
holder: MyViewHold,
position: Int
) {
holder.upInfo("${data[position]}")
}
}
网上有无数的文章说到四级缓存,也就是说会有四个地方会缓存ViewHolder
,既然分了四个地方那么必然存在不同的原因,我们先不深究这个,我们首先看下获取ViewHolder的流程:
可以看到ViewHolder
会被缓存到四处.
一级缓存
假设我们RecyclerView
被系统强制刷新了界面(比如系统的vsync
信号,或者invalidate()
触发),可是数据没有任何变化,我们的所有ViewHolder
还需要重新绑定数据是多余的.(也就是说这种情况不应该调用onBindViewHolder
和onCreateViewHolder
).
这个问题解决是交付给一级缓存的mAttachedScrap
所实现的.
RecyclerView
在数据未改动时刷新界面:
上面是在数据干净时的情况,也就是所有的ViewHolder
是干净的.假设我们我们Item4
是脏的怎么办?比如item4
显示张三
现在要显示李四
,mChangedScrap
就是用来存储屏幕内脏ViewHolder
的情况用于做预布局
.这里又扯到预布局
的概念具体的可以看上文的参考.
我们这里简单做下简单描述:
RecyclerView
会做一次预布局
和最终布局
.预布局就是用当前存在脏数据缓存摆放子view
方便得出当前状态,最后和最终布局做比较好执行动画等操作.
通过两个布局我们的RecyclerView
会感知出要执行Item4
的变化动画,后面执行动画,在执行动画后会将mChangedScrap
的数据放入四级缓存/RecycledViewPool
中,
注意mChangedScrap
缓存的对象会在预布局
和最终布局
放后移动到四级缓存
中,所以mChangedScrap
在当次刷新中是不会复用,只会保留在下次复用.
可能你会问,我们在代码中如何复现这个现象?
//修改张三为李四
dataList[3] = "李四"
//标记第四个元素是脏的,其他item是干净
rvAdapter.notifyItemChanged(3)
当你调用后你会发现第四个VIewHolder
会调用onBindViewHolder
.
- 总结:
一级缓存
用于缓存屏幕的ViewHolder
.mChangedScrap
用于缓存改变数据的ViewHolder
,主要用于预测布局,布局mAttachedScrap
用于缓存位改未变数据的ViewHolder
注意:
mAttachedScrap
存放的元素并不是一定不会调用onBindViewHolder
.
在如下情况是会将屏幕元素的Item
放入mAttachedScrap
然后调用他们的数据绑定函数onBindViewHolder
.
情况:一个adapter
设置有了有稳定id的选项setHasStableIds(true)
,然后直接调用notifyDataSetChanged
.
二级缓存
我们后很多时候会有一种习惯:向上滑动屏幕推出Item1
,然后又向下滑动推回Item1
,笔者就有这种习惯.假设我们能有一套机制快速恢复少量的Item
(不需要绑定数据onBindViewHolder
),对于这类行为是非常有帮助.而mCachedViews
就是帮助我们做这样的事情.
mCachedViews
大小很小默认数量为2(由androidx.recyclerview.widget.RecyclerView.Recycler#mRequestedCacheMax
变量初始化)
- 二级缓存还起到预加载的作用:
当你滑动Item5
时会预加载Item6
到二级缓存中(可能会创建ViewHolder
,或者复用其他不可见ViewHolder
),当滑动Item6
时已经提前绑定了数据所以用户可以更丝滑体验滑动.
三级缓存
ViewCacheExtension
这个我实在没深刻领会Google
工程师的设计,而且有点难用,为了不误导大家这里就跳过.
我看了下很多开源项目也都没有实现
四级缓存
我们上面的讨论的一级
和二级
在平常已经足够我们使用了.如果二级缓存
的数据是脏的直接调用onBindViewHolder
即可,何必又重新多出一级缓存?把这个弄明白才是根本,相比起深入源码无法自拔更加能领略作者的思想境界.
我认为有如下几个方面需要额外多出一级缓存的必要:
- 1 避免预加载时,
一级
和二级
缓存的浪费 - 2 多个
RecyclerView
之间的ViewHolder
共享
避免在一级
和二级
缓存的浪费
我们首先查看这个问题根本原因
-
步骤1
-
步骤2
-
步骤3
此时我们假设我们屏幕完全划入Item6
.item2
滑出屏幕,此时Item1
,和Item7
已经被被二级缓存存储,且数量已经到底上限(二级缓存默认情况下上限为2),item2
被缓存到哪里呢?或者用存丢弃掉item1
,item7
?
所以此时我们的四级缓存
出现了.我们把Item1
丢入四级缓存
,接着将item2
放入二级缓存.
此时多出的item1
可以在其他RecyclerView
中复用,或者在预加载Item8
的时候复用,所以在上面的情况下RecyclerView
会最多创建7个ViewHolder
然后进行循环复用.
这里给出一些会放入四级缓存的途径:
二级缓存
超限放入一级缓存的mAttachScrap
在执行完动画后放入- 其他情况的脏
ViewHolder
放入
四级缓存数据结构
相比起一二级缓存的List
结构来说,四级缓存结构复杂.四级缓存是根据ViewHolder
类别存储(ViewHolder.getItemViewType
).每个类别默认情况存储5
为上限.
//RecyclerView.java
public static class RecycledViewPool {
//标记每个类别的ViewHolder存储上限数量
private static final int DEFAULT_MAX_SCRAP = 5;
//用于存储每个每个类别ViewHolder
SparseArray<ScrapData> mScrap = new SparseArray<>();
static class ScrapData {
final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
//获取指定类别的ScrapData
private ScrapData getScrapDataForType(int viewType) {
ScrapData scrapData = mScrap.get(viewType);
if (scrapData == null) {
scrapData = new ScrapData();
mScrap.put(viewType, scrapData);
}
return scrapData;
}
//放入指定的元素
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
//超过上限就不放入
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
//给ViewHolder重置一些标志位方便下次使用
scrap.resetInternal();
//放入集合中
scrapHeap.add(scrap);
}
}
源码分析
看完上面的即便你不看源码也可以很明白四级缓存的作用了.
RecyclerView
是一个view
,所以必然经过measure
,layout
,draw
,我们这里忽略draw
和measure
函数,直接看layout
函数调用链即可.
//RecyclerView.java
class RecyclerView{
protected void onLayout(boolean changed, int l, int t, int r, int b) {
dispatchLayout();
}
void dispatchLayout() {
//..略,这里其他布局函数可能涉及预测布局,如果你有兴趣可以可看dispatchLayoutStep1 dispatchLayoutStep3
dispatchLayoutStep2();
//..略
}
private void dispatchLayoutStep2() {
// LayoutManager mLayout; 我们这里以LinearLayoutManager为例
mLayout.onLayoutChildren(mRecycler, mState);
}
}
上面RecyclerView
会把布局责任托付给LayoutManager
(其他绘制和测量也是如此).
我们这里以LinearLayoutManager
为例
//LinearLayoutManager.java
class LinearLayoutManager{
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
//略
fill(recycler, mLayoutState, state, false);
}
int fill(/**...略..**/) {
//循环填充view到RecyclerView中
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
layoutChunk(recycler, state, layoutState, layoutChunkResult);
}
}
void layoutChunk(/**...略..**/) {
//...略...
//从ViewHolder取出的view
//layoutState对象为LayoutState
View view = layoutState.next(recycler);
//放入RecyclerView中
addView(view);
//...略...
}
}
//LinearLayoutManager.java
static class LayoutState {
View next(RecyclerView.Recycler recycler) {
//从recycler获取ViewHolder,返回,可以看到总算到缓存核心类Recycler
final View view = recycler.getViewForPosition(mCurrentPosition);
return view;
}
}
//RecyclerView.java
public final class Recycler {
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
View getViewForPosition(int position, boolean dryRun) {
//缓存核心函数
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
}
可以看到最终缓存回去Recycler.tryGetViewHolderForPositionByDeadline
获取,
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0)预布局相关,tryGetViewHolderForPositionByDeadline会在预布局中调用一次,和最终布局调用一次
// 预布局可以辅助一些动画预测,这里我们不需要深究,预布局在dispatchLayoutStep1调用
// 如果是预布局isPreLayout为true.
//
if (mState.isPreLayout()) {
//从mChangedScrap取出ViewHolder,后面执行动画时会放入四级缓存中
//注意这个holder不会在本次最终布局进行复用,所以脏的ViewHolder,会被一个新的ViewHolder取代,您可以代码验证
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
//从mAttachedScrap和mCachedViews寻找
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
//略
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
final int type = mAdapter.getItemViewType(offsetPosition);
//如果设置稳定id,那么会再次通过mCachedViews和mAttachedScrap去寻找
//只不过内部寻找的时候主要同过ViewHolder的id去寻找.你一定会疑惑和上面有什么区别
//getScrapOrCachedViewForId内部会用id和ViewHolder类别比较得到一个符合的ViewHolder
//而getScrapOrHiddenOrCachedHolderForPosition仅通过position
if (mAdapter.hasStableIds()) {
//会去mCachedViews和mAttachedScrap去寻找
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
//更新holder信息
if (holder != null) {
// update position
holder.mPosition = offsetPosition;
fromScrapOrHiddenOrCache = true;
}
}
//通过三级缓存去寻找
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
//四级缓存区遍历
if (holder == null) { // fallback to pool
//
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
//都找不到自己创建一个返回
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
if (mState.isPreLayout() && holder.isBound()) {
//略
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
//函数内部会执行后holder.bindViewHolder操作
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
//略
return holder;
}
Stable Id 相关问题
setHasStableIds(true)
设置之后除了可以显示相关动画之外还有什么特性?
特点1
RecyclerView
在具有稳定id
的情况下,notifyDataSetChanged
会把所有元素放入1级缓存/mAttachedScrap
中,如果没有稳定id
会放入四级缓存
中.还记得我们说过四级缓存
仅能存储五个同类型的ViewHolder
吗?
如果没有稳定id
在上面的情况刷新后仅有五个ViewHolder
会被复用,其余的ViewHolder
又会重新创建.
onCreateViewHolder 创建 1
onBindViewHolder position 0
onCreateViewHolder 创建 2
onBindViewHolder position 1
onCreateViewHolder 创建 3
onBindViewHolder position 2
onCreateViewHolder 创建 4
//..略
onCreateViewHolder 创建 16
onBindViewHolder position 15
------ 无稳定id刷新 notifyDataSetChanged -----
onBindViewHolder position 0
onBindViewHolder position 1
onBindViewHolder position 2
onBindViewHolder position 3
onBindViewHolder position 4
onCreateViewHolder 创建 17
onBindViewHolder position 5
onCreateViewHolder 创建 18
onBindViewHolder position 6
onCreateViewHolder 创建 19
onBindViewHolder position 7
onCreateViewHolder 创建 20
onBindViewHolder position 8
onCreateViewHolder 创建 21
onBindViewHolder position 9
onCreateViewHolder 创建 22
onBindViewHolder position 10
onCreateViewHolder 创建 23
onBindViewHolder position 11
onCreateViewHolder 创建 24
onBindViewHolder position 12
onCreateViewHolder 创建 25
onBindViewHolder position 13
onCreateViewHolder 创建 26
onBindViewHolder position 14
onCreateViewHolder 创建 27
onBindViewHolder position 15
特点2
RecyclerView
在复用会尽量复用id
相同的ViewHolder
(上面源码分析你可以看到if (mAdapter.hasStableIds())
的代码就是),所以你可以利用这个特性进行判断加大局部刷新成功率.
class RVAdapter() : RecyclerView.Adapter<MyViewHold>(){
override fun onBindViewHolder(holder: MyViewHold, position: Int, payloads: MutableList<Any>) {
//因为recyclerview尽可能保证复用,所以很有可能命中
if (holder.view.tag == "标识符") {
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
}
如果你想了解相关源码可以直接看RecyclerViewDataObserver.onChanged
这个函数会在adapter数据改变后回调.这个类是RecyclerView
的内部类,比较简单.本来想写的,谁知道画图画了半天.
以上是关于RecycleView 缓存机制的主要内容,如果未能解决你的问题,请参考以下文章