文章目录
深入理解 RecyclerView 的绘制流程和滑动原理(匠心巨作-上)
深入理解 RecyclerView 的嵌套滑动机制(匠心巨作-中)
深入理解 RecyclerView 的回收复用缓存机制详解(匠心巨作-下)
Android RecyclerView 缓存机制
绘制流程:RecyclerView的绘制三个步骤
1. setLayoutManager
必要的步骤,给RecyclerView设置布局管理器,常见的布局管理器有:线性布局管理器LinearLayoutManager,网格布局管理器GridLayoutManager,瀑布流式管理器StaggeredGridLayoutManager。
源码解析:重置回收 -> LayoutManager与RecyclerView关联起 -> 请求重绘requestLayout, 进入measure,layout,draw流程
public void setLayoutManager(@Nullable LayoutManager layout) {
// 如果新设置的 LayoutManager 和当前已有的 LayoutManager 是同一个,直接返回,无需操作
if (layout == mLayout) {
return;
}
// 停止当前的滚动操作(比如快速滑动或 fling 操作)
stopScroll();
// 如果当前已经有 LayoutManager,则需要先清理它相关的视图和资源
if (mLayout != null) {
// 如果有 ItemAnimator,结束所有正在执行的动画
if (mItemAnimator != null) {
mItemAnimator.endAnimations();
}
// 移除并回收所有由 LayoutManager管理的视图(包括已废弃的视图)
mLayout.removeAndRecycleAllViews(mRecycler);
mLayout.removeAndRecycleScrapInt(mRecycler); // 回收 scrap 中的视图
// 清空 Recycler 中的所有缓存视图
mRecycler.clear();
// 如果 RecyclerView 已经附加到 Window 上,调用 LayoutManager 的 dispatchDetachedFromWindow
if (mIsAttached) {
mLayout.dispatchDetachedFromWindow(this, mRecycler);
}
// 将 LayoutManager 与 RecyclerView 解除绑定
mLayout.setRecyclerView(null);
// 置空当前 LayoutManager
mLayout = null;
} else {
// 如果当前没有 LayoutManager,也清空 Recycler 的缓存视图
mRecycler.clear();
}
// 为了防止 ItemAnimator 出现问题,再次移除所有子视图(防御性操作)
mChildHelper.removeAllViewsUnfiltered();
// 将新的 LayoutManager 赋值给成员变量
mLayout = layout;
// 如果新 LayoutManager 不为 null
if (layout != null) {
// 检查该 LayoutManager 是否已经绑定到另一个 RecyclerView,如果是,抛出异常
if (layout.mRecyclerView != null) {
throw new IllegalArgumentException("LayoutManager " + layout
+ " is already attached to a RecyclerView:"
+ layout.mRecyclerView.exceptionLabel());
}
// 将该 LayoutManager 绑定到当前 RecyclerView
mLayout.setRecyclerView(this);
// 如果 RecyclerView 已经附加到 Window 上,通知 LayoutManager 附加事件
if (mIsAttached) {
mLayout.dispatchAttachedToWindow(this);
}
}
// 更新 Recycler 的视图缓存大小
mRecycler.updateViewCacheSize();
// 请求重新布局,触发 LayoutManager 重新布局子视图
requestLayout();
}
2. dispatchLayoutStep系列方法
理解measure,layout,draw流程之前需要先理解dispatchLayoutStep系列方法,这一系列方法分别对应 RecyclerView 在执行布局时的三个主要阶段。此处省略源码,只需要理解其核心作用。
- dispatchLayoutStep1()
预处理:表示进行预布局,适配器更新、动画运行、保存当前视图的信息等工作;
mState.mLayoutStep == State.STEP_START才会执行。
onMeasure,onLayout方法都可能会执行 - dispatchLayoutStep2()
核心阶段,测量和布局所有需要显示的 itemView,实际上调用的是onLayoutChildren方法。
onMeasure,onLayout方法都可能会执行 - dispatchLayoutStep3()
后处理:表示布局最后一步,保存和触发有关动画的信息,相关清理等工作。
onLayout中执行
3. onMeasure
- 源码理解
onMeasure()主要是RecyclerView宽高测量工作,主要有两种情况:
(1)当RecyclerView的宽高为match_parent或者精确值时,即measureSpecModeIsExactly = true,此时只需要测量自身的宽高就知道RecyclerView的宽高,测量方法结束(defaultOnMeasure);
(2)当RecyclerView的宽高为wrap_content时,即measureSpecModeIsExactly = false,会往下执行dispatchLayoutStep1()和dispatchLayoutStep2(),就是遍历测量ItemView的大小从而确定RecyclerView的宽高,这种情况真正的测量操作都是在dispatchLayoutStep2()中完成。
@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {//如果mLayout为空则采用默认测量,然后结束
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {//如果为自动测量,默认为true
final int widthMode = MeasureSpec.getMode(widthSpec);
final int heightMode = MeasureSpec.getMode(heightSpec);
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);//测量RecyclerView的宽高
//当前RecyclerView的宽高是否为精确值
final boolean measureSpecModeIsExactly =
widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
if (measureSpecModeIsExactly || mAdapter == null) {//如果RecyclerView的宽高为精确值或者mAdapter为空,则结束
return;
}
//RecyclerView的宽高为wrap_content时,即measureSpecModeIsExactly = false则进行测量
//因为RecyclerView的宽高为wrap_content时,需要先测量itemView的宽高才能知道RecyclerView的宽高
if (mState.mLayoutStep == State.STEP_START) {//还没测量过
dispatchLayoutStep1();//1.适配器更新、动画运行、保存当前视图的信息、运行预测布局
}
dispatchLayoutStep2();//2.最终实际的布局视图,如果有必要会多次运行
//根据itemView得到RecyclerView的宽高
mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
}
}
4. onLayout
- 源码理解
1、如果没在onMeasure阶段提前测量子ItemView,即RecyclerView宽高为match_parent或者精确值时,调用dispatchLayoutStep1()和dispatchLayoutStep2()测量itemView宽高;
2、如果在onMeasure阶段提前测量子ItemView,但是子视图发生了改变或者期望宽高和实际宽高不一致,则会调用dispatchLayoutStep2()重新测量;
3、最后都会执行dispatchLayoutStep3()方法。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
dispatchLayout(); //直接调用dispatchLayout()方法布局
TraceCompat.endSection();
mFirstLayoutComplete = true;
}
void dispatchLayout() {
······
mState.mIsMeasuring = false;//设置RecyclerView布局完成状态,前面已经设置预布局完成了。
if (mState.mLayoutStep == State.STEP_START) {//如果没在OnMeasure阶段提前测量子ItemView
dispatchLayoutStep1();//布局第一步:适配器更新、动画运行、保存当前视图的信息、运行预测布局
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();
} else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
|| mLayout.getHeight() != getHeight()) {//前两步完成测量,但是因为大小改变不得不再次运行下面的代码
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();//布局第二步:最终实际的布局视图,如果有必要会多次运行
} else {
mLayout.setExactMeasureSpecsFrom(this);
}
dispatchLayoutStep3();//布局第三步:最后一步的布局,保存视图动画、触发动画和不必要的清理。
}
5. onDraw
- 源码解析
在 RecyclerView 自身绘制完成后,依次调用所有添加的 ItemDecoration 的 onDraw() 方法,用于在 itemView 绘制之前绘制底层装饰内容。
ItemDecoration 是 RecyclerView 提供的一个抽象类,用于在 item 绘制前后添加装饰效果,比如:设置 item 的间隔,添加分组标题绘制边框、阴影等视觉效果
@Override
public void onDraw(Canvas c) {
super.onDraw(c);//所有itemView先绘制
//分别绘制ItemDecoration
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
6. 绘制流程总结

7. 缓存生命周期

绘制流程:LinearLayoutManager填充、测量、布局过程
itemView的真正布局时委托给各个的LayoutManager中处理,上面LinearLayoutManager可以知道dispatchLayoutStep2()是实际布局视图步骤,通过LayoutManager调用onLayoutChildren()方法进行布局itemView
1. onLayoutChildren()布局itemView
源码省略,只关注其核心逻辑
- 初始化布局锚点
确定布局的起始位置,比如从顶部开始往下布局(垂直方向),或者从左到右(水平方向)。这通常基于滚动状态或数据变化类型(如新增、删除等)来决定。 - 回收不可见的 View
调用 detachAndScrapAttachedViews 方法,将当前已经不可见的 item view 放入回收池(Recycler),以便后续复用。 - 确定布局方向和偏移量
根据当前滚动方向(如向上或向下)确定布局的方向和起始偏移量。例如,向下滚动时,新的 item 会从屏幕底部进入。 - 填充可见区域(fill 方法)
通过 fill() 方法不断添加新的 item view 到屏幕上,直到填满可见区域。 - 处理剩余空间或边界情况
布局完成后,如果还有剩余空间,可能需要做一些对齐处理(如居中、靠左/右等)。
2. fill()开始填充itemView
while()循环,循环执行layoutChunk()填充一个itemView到屏幕,同时返回这次填充的区域大小
首先根据屏幕还有多少剩余空间remainingSpace,根据这个数值减去子View所占的空间大小,小于0时布局子View结束,如果当前所有子View还没有超过remainingSpace时,调用layoutChunk()安排View的位置。
3. layoutChunk()对itemView创建、填充、测量、布局
- layoutState.next(recycler)从缓存中获取itemView,如果没有则创建itemView;
- 根据实际情况来添加itemView到RecyclerView中,最终调用的还是ViewGroup的**addView()**方法;
- measureChildWithMargins()测量itemView大小包括父视图的填充、项目装饰和子视图的边距;
- 根据计算好的left, top, right, bottom通过layoutDecoratedWithMargins()使用坐标在RecyclerView中布局给定的itemView。
4. 流程总结

RecyclerView 的回收复用缓存机制
1. item的五种状态

2. 多级缓存定义
RecylerView的缓存有Reclyer类管理,优先级顺序为:Scrap -> Cache -> Extension -> Pool -> 新创建。有如下几种缓存:
- Srcap缓存:mAttachedScrap
Srcap缓存不参与列表滚动时的回收复用,作为重新布局时的临时缓存,它的作用是,缓存当界面重新布局前和界面重新布局后都出现的ViewHolder,这些ViewHolder是无效、未移除、未标记的。
在这些无效、未移除、未标记的ViewHolder之中,mAttachedScrap负责保存其中没有改变的ViewHolder - Srcap缓存:mChangedScrap
保存那些数据发生变化(如调用 notifyItemChanged())的 ViewHolder。
这些 ViewHolder 仍可见,但其数据已经过时,需要重新绑定

- CacheView
用于RecyclerView列表位置产生变动时,对刚刚移出屏幕的view进行回收复用。
CacheView的最大容量为2,缓存一个新的ViewHolder时,如果超出了最大限制,那么会将CacheView缓存的第一个数据添加到RecycledViewPool后再移除掉,最后才会将新的ViewHolder添加进来。 - ViewCacheExtension
- ViewCacheExtension是缓存拓展的帮助类,额外提供了一层缓存池给开发者。几乎没有用到
- RecylerViewPool
ecycledViewPool是Recycler的终极回收站。数据结构:SparseArray嵌套一个ArraryList的形式。根据不同itemType来缓存ViewHolder,默认每个类型5个
3. 回收原理
调用链如下:
- onLayoutChildren方法:removeAndRecycleAllViews移除所有的子View,再通过detachAndScrapAttachedViews分离item
detachAndScrapAttachedViews()的作用就是把当前屏幕所有的item与屏幕分离,将他们从RecyclerView的布局中拿下来,保存到list中,在重新布局时,再将ViewHolder重新一个个放到新的位置上去
recycleViewHolderInternal:detachAndScrapAttachedViews满足特定条件就会走这个方法
根据item的状态放到不同的缓存中去(核心还是上边item状态和缓存)
4. 复用原理
调用链如下:
- onLayoutChildren
- fill()
- layoutChunk()
- layoutState.next(recycler)
- tryGetViewHolderForPositionByDeadline:核心还是根据上述item状态,缓存的定义,item的position去寻找对应的viewholder
5. 结合具体场景理解
-
Scrap缓存的理解

在一个手机屏幕中,将itemB删除,并且调用notifyItemRemoved()方法,如果item是无效并且被移除的就会回收到其他的缓存,否则都是缓存到Scrap中,那么mAttachedScrap和mChangedScrap会分别存储itemView,itemA没有任何的变化,存储到mAttachedScrap中,itemB虽然被移出了,但是还有效,也被存储到mAttachedScrap中(但是会被标记REMOVED,之后会移除);itemC和itemD发生了变化,位置往上移动了,会被存储到mAttachedScrap中。删除时,ABCD都会进入Scrap中;删除后,ACD都会回来,A没有任何变化,CD只是位置发生了变化,内容没有发生变化。 -
CacheView缓存的应用

CacheView缓存刚刚变为不可见的view,如果当前View再次进入屏幕中的时候,进行精准匹配,这个itemView还是 之前的itemView,那么会从CacheView中获取ViewHolder进行复用。如果一直向某一个方向滑动,那么CacheView将会不断替换缓存里面的ViewHolder(CacheView最多只能存储2个),将替换掉的ViewHolder先放到RecycledViewPool中。在CacheView中拿不到复用的ViewHolder,那么最后只能去RecycledViewPool中获取
6. 回收和复用流程总结

滑动原理
1. 普通滑动
2. 惯性滑动
3. 嵌套滑动
刷新数据的常见api
性能优化
补充个人理解核心问题:
1. onLayoutChildren如何理解这个方法?
onLayoutChildren 是 LayoutManager 的布局入口,相当于 ViewGroup 的 onLayout。
每次 RecyclerView 需要完整重新布局时回调。
第一步一般调用 detachAndScrapAttachedViews,剥离现有子 View 放入临时缓存。
然后遍历需要展示的 position,通过 recycler.getViewForPosition 拿到 view,完成测量,计算坐标,调用 layoutDecoratedWithMargins 摆放子 View。
2172



被折叠的 条评论
为什么被折叠?



