读源码涨知识---RecyclerView-预布局-,后布局与-scrap-缓存的关系

文章讲述了RecyclerView在填充表项前的清空、detach、scrap过程,以及如何利用mAttachedScrap缓存复用ViewHolder。RecyclerView为了实现表项动画,需要进行预布局和后布局,scrap机制确保了动画前后数据的一致性。
摘要由CSDN通过智能技术生成

在每次向RecyclerView填充表项之前都会先清空现存表项。

目前看来,detach viewremove view差不多,它们都会将子控件从父控件的孩子列表中删除,唯一的区别是detach更轻量,不会触发重绘。而且detach是短暂的,被detach的 View 最终必须被彻底 remove 或者重新 attach。(下面就会马上把他们重新 attach)

scrap 表项

scrap 表项的意思是回收表项并将其存入mAttachedScrap列表,它是回收器Recycler中的成员变量:

public class RecyclerView {
public final class Recycler {
// scrap 列表
final ArrayList mAttachedScrap = new ArrayList<>();
}
}

mAttachedScrap是一个 ArrayList 结构,用于存储ViewHolder实例。

RecyclerView 填充表项前,除了会 detach 所有可见表项外,还会同时 scrap 它们:

public class RecyclerView {
public abstract static class LayoutManager {
// 回收表项
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);

// detach 表项
detachViewAt(index);
// scrap 表项
recycler.scrapView(view);

}
}
}

scrapView()是回收器Recycler的方法,正是这个方法将表项回收到了mAttachedScrap列表中:

public class RecyclerView {
public final class Recycler {
void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
// 表项不需要更新,或被移除,或者表项索引无效时,将被会收到 mAttachedScrap
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
holder.setScrapContainer(this, false);
// 将表项回收到 mAttachedScrap 结构中
mAttachedScrap.add(holder);
} else {
// 只有当表项没有被移除且有效且需要更新时才会被回收到 mChangedScrap
if (mChangedScrap == null) {
mChangedScrap = new ArrayList();
}
holder.setScrapContainer(this, true);
mChangedScrap.add(holder);
}
}
}
}

scrapView()中根据ViewHolder状态将其会收到不同的结构中,同样地,硬看源码很难快速判断执行了那个分支,继续断点调试,Demo 场景中所有的表项都会被回收到mAttachedScrap结构中。(关于 mAttachedScrap 和 mChangedScrap 的区别会在后续文章分析)

分析至此,进一步细化刚才得到的结论:

在每次向RecyclerView填充表项之前都会先清空 LayoutManager 中现存表项,将它们 detach 并同时缓存入 mAttachedScrap列表中。

将结论应用在 Demo 的场景,即是:RecyclerView 在预布局阶段准备向列表中填充表项前,会清空现有的表项 1、2,把它们都 detach 并回收对应的 ViewHolder 到 mAttachedScrap列表中。

从缓存拿填充表项

预布局与 scrap 缓存的关系

缓存定是为了复用,啥时候用呢?紧接着的“填充表项”中就立马会用到:

public class LinearLayoutManager {
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

// detach 表项
detachAndScrapAttachedViews(recycler);

// 填充表项
fill()
}

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
// 计算剩余空间
int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
// 不停的往列表中填充表项,直到没有剩余空间
while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
// 填充单个表项
layoutChunk(recycler, state, layoutState, layoutChunkResult);

}
}

// 填充单个表项
void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
// 获取下一个被填充的视图
View view = layoutState.next(recycler);

// 填充视图
addView(view);

}
}

填充表项时,通过layoutState.next(recycler)获取下一个该被填充的表项视图:

public class LinearLayoutManager {
static class LayoutState {
View next(RecyclerView.Recycler recycler) {

// 委托 Recycler 获取下一个该填充的表项
final View view = recycler.getViewForPosition(mCurrentPosition);

return view;
}
}
}

public class RecyclerView {
public final class Recycler {
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
}

View getViewForPosition(int position, boolean dryRun) {
// 调用链最终传递到 tryGetViewHolderForPositionByDeadline()
return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}
}

沿着调用链一直往下,最终走到了Recycler.tryGetViewHolderForPositionByDeadline(),在RecyclerView缓存机制(咋复用?)中对其做过详细介绍,援引结论如下:

  1. 在 RecyclerView 中,并不是每次绘制表项,都会重新创建 ViewHolder 对象,也不是每次都会重新绑定 ViewHolder 数据。
  2. RecyclerView 填充表项前,会通过Recycler获取表项的 ViewHolder 实例。
  3. RecyclertryGetViewHolderForPositionByDeadline()方法中,前后尝试 5 次,从不同缓存中获取可复用的 ViewHolder 实例,其中第一优先级的缓存即是scrap结构。
  4. scrap缓存获取的表项不需要重新构建,也不需要重新绑定数据。

从 scrap 结构获取 ViewHolder 的源码如下:

public class RecyclerView {
public final class Recycler {
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
ViewHolder holder = null;

// 从 scrap 结构中获取指定 position 的 ViewHolder 实例
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);

}

}

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 遍历 mAttachedScrap 列表中所有的 ViewHolder 实例
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
// 校验 ViewHolder 是否满足条件,若满足,则缓存命中
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}

}
}
}

mAttachedScrap列表中获取的ViewHolder实例后,得进行校验。校验的内容很多,其中最重要的的是:ViewHolder索引值和当前填充表项的位置值是否相等,即:

scrap 结构缓存的 ViewHolder 实例,只能复用于和它回收时相同位置的表项。

也就是说,若当前列表正准备填充 Demo 中的表项 2(position == 1),即使 scrap 结构中有相同类型 ViewHolder,只要viewHolder.getLayoutPosition()的值不为 1,缓存不会命中。

分析至此,可以把上面得到的结论进一步拓展:

在每次向RecyclerView填充表项之前都会先清空 LayoutManager 中现存表项,将它们 detach 并同时缓存入 mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach 的表项并重新 attach 它们。

(弱弱地问一句,这样折腾意义何在?可能接着往下看就知道了。。)

将结论应用在 Demo 的场景,即是:RecyclerView 在预布局阶段准备向列表中填充表项前,会清空现有的表项 1、2,把它们都 detach 并回收对应的 ViewHolder 到 mAttachedScrap 列表中。然后又在填充表项阶段从 mAttachedScrap 中重新获取了表项 1、2 并填入列表。

上一篇的结论说“Demo 场景中,预布局阶段还会额外加载列表第三个位置的表项 3”,但mAttachedScrap只缓存了表项 1、2。所以在填充表项 3 时,scrap 缓存未命中。不仅如此,因表项 3 是从未被加载过的表项,遂所有的缓存都不会命中,最后只能沦落到重新构建表项并绑定数据

public class RecyclerView {
public final class Recycler {
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
if (holder == null) {

// 构建 ViewHolder
holder = mAdapter.createViewHolder(RecyclerView.this, type);

}
// 获取表项偏移的位置
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 绑定 ViewHolder 数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
}
}
}

沿着上述代码的调用链往下走查,就能找到熟悉的onCreateViewHolder()onBindViewHolder()

在绑定 ViewHolder 数据之前,先调用了mAdapterHelper.findPositionOffset(position)获取了“偏移位置”。断点调试告诉我,此时它会返回 1,即表项 2 被移除后,表项 3 在列表中的位置。

AdapterHelper将所有对表项的操作都抽象成UpdateOp并保存在列表中,当获取表项 3 偏移位置时,它发现有一个表项 2 的删除操作,所以表项 3 的位置会 -1。(有关 AdapterHelper 的内容就不展开了~)

至此,预布局阶段的填充表项结束了,LayoutManager 中现有表项 1、2、3,形成了第一张快照(1,2,3)。

后布局与 scrap 缓存的关系

再次援引上一篇的结论:

  1. RecyclerView 为了实现表项动画,进行了 2 次布局,第一次预布局,第二次后布局,在源码上表现为 LayoutManager.onLayoutChildren() 被调用 2 次。

  2. 预布局的过程始于 RecyclerView.dispatchLayoutStep1(),终于 RecyclerView.dispatchLayoutStep2()。

在紧接着执行的dispatchLayoutStep2()中,开始了后布局

public class RecyclerView {
void dispatchLayout() {

dispatchLayoutStep1();// 预布局
mLayout.setExactMeasureSpecsFrom(this);
dispatchLayoutStep2();// 后布局

}

private void dispatchLayoutStep2() {
mState.mInPreLayout = false;// 预布局结束
mLayout.onLayoutChildren(mRecycler, mState); // 第二次 onLayoutChildren()
}

布局子表项的老花样要再来一遍,即先 detach 并 scrap 现有表项,然后再填充。

但这次会有一些不同:

  1. 因为 LayoutManager 中现有表项 1、2、3,所以 scrap 完成后,mAttachedScrap中存有表项1、2、3 的 ViewHolder 实例(position 依次为 0、0、1,被移除表项的 position 会被置 0)。
  2. 因为第二次执行onLayoutChildren()已不属于预布局阶段,所以不会加载额外的表项,即LinearLayoutManager.layoutChunk()只会执行 2 次,分别填充位置为 0 和 1 的表项。
  3. mAttachedScrap缓存的 ViewHolder 中,有 2 个 position 为 0,1 个 position 为 1。毫无疑问,填充列表位置 1 的表项时,表项 3 必会命中(因为 position 相等)。但填充列表位置 0 的表项时,是表项 1 还是 表项 2 命中?(它们的 position 都为 0)再回看一遍,缓存命中前的校验逻辑:

public class RecyclerView {
public final class Recycler {
// 从 缓存中获取 ViewHolder 实例
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// 遍历 mAttachedScrap
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
if (!holder.wasReturnedFromScrap()
&& holder.getLayoutPosition() == position // 位置相等
&& !holder.isInvalid()
&& (mState.mInPreLayout || !holder.isRemoved()) // 在预布局阶段 或 表项未被移除
) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}
}
}

当遍历到mAttachedScrap的表项 2 时,虽然它的位置满足了要求,但校验的最后一个条件把它排除了,因为现在已经不再是预布局阶段,且表项 2 是被移除的。所以列表的位置 0 只能被剩下的表项 1 填充。

分别用表项 1、3 填充了列表的位置 0、1 ,后布局的填充表项也结束了。

此时就形成第二张快照(1,3),和预布局形成的快照(1,2,3)比对之后,就知道表项 2 需要做消失动画,而表项 3 需要做移入动画。那动画具体是怎么实现的?限于篇幅,下次再析。

总结

回到篇中的那个问题:“何必这样折腾?即先 detach 并 缓存表项到 scrap 结构中,然后紧接着又在填充表项时从中取出?”

因为 RecyclerView 要做表项动画,

为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,

为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),

为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),

但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),

Recycler 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。

Android 开发相关源码精编解析

随着Android开发行业逐渐饱和,对Android开发者的面试要求也越来越高,是否掌握底层源码,便是考验一名Android开发者的重要一环。面试被问到源码问题答不出来,会掉身价、砍薪资尚且不谈,甚至连面试都过不了!

网上各类源码解析的文章博客五花八门、良莠不齐。杂乱、要么内容质量太浅,零散、碎片化,总看着看着就衔接不上了。

所以特意将我在疫情期间花了4个月整理出来的《Android 开发相关源码精编解析》分享出来大家

由于内容较多,避免影响到大家的阅读体验,在此只截图展示目录部分,487详细完整版的《Android 开发相关源码精编解析》电子书文档领取方式:**点赞+关注,然后私信关键词 【666】**即可加我的个人微信私发给你(无偿)。也欢迎大家找我探讨Android技术问题~

**目录:一共18节,487页PDF,**包括MMKV 源码,ARouter 源码,AsyncTask 源码,Volley 源码,Retrofit 源码,OkHttp 源码,ButterKnife 源码,Okio 源码,SharedPreferences 源码,EventBus 源码,Android 自定义注解初探,View 的工作机制源码分析,Android 触摸事件分发机制源码分析,Android 按键事件分发机制源码分析,深入解析 Handler 源码,深入解析 Binder 源码,深入解析 JNI 源码,深入解析 Glide 源码。

《Android 开发相关源码精编解析》电子书文档资料已经上传在我的GitHub,或者关注后简信我【666】即可领取(无偿)。
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

写在最后

对程序员来说,很多技术的学习都是“防御性”的。也就是说,我们是在为未来学习。我们学习新技术的目的,或是为了在新项目中应用,或仅仅是为了将来的面试。但不管怎样,一定不能“止步不前”,不能荒废掉。

![
[]


文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

ttps://upload-images.jianshu.io/upload_images/22459598-3e1bbd9b84cc0ef9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

[外链图片转存中…(img-fraWoXvh-1712053271246)]
[外链图片转存中…(img-eraGQ8rz-1712053271246)]

文章以下内容会给出阿里与美团的面试题(答案+解析)、面试题库、Java核心知识点梳理等

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值