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

}
}
}
}

果不其然,在填充表项之前会遍历所有子表项,并逐个回收它们:

public class RecyclerView {
public abstract static class LayoutManager {
// 回收表项
private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.isInvalid() && !viewHolder.isRemoved()&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
// detach 表项
detachViewAt(index);
// scrap 表项
recycler.scrapView(view);

}
}
}
}

回收表项时,根据viewHolder的不同状态执行不同分支。硬看源码很难快速判断会走哪个分支,果断运行 Demo,断点调试一把。在上述场景中,所有表项都走了第二个分支,即在布局表项之前,对现有表项做了两个关键的操作:

  1. detach 表项detachViewAt(index)
  2. scrap 表项recycler.scrapView(view)

detach 表项

先看看 detach 表项是个什么操作:

public class RecyclerView {
public abstract static class LayoutManager {
ChildHelper mChildHelper;
// detach 指定索引的表项
public void detachViewAt(int index) {
detachViewInternal(index, getChildAt(index));
}

// detach 指定索引的表项
private void detachViewInternal(int index, @NonNull View view) {

// 将 detach 委托给 ChildHelper
mChildHelper.detachViewFromParent(index);
}
}
}

// RecyclerView 子表项管理类
class ChildHelper {
// 将指定位置的表项从 RecyclerView detach
void detachViewFromParent(int index) {
final int offset = getOffset(index);
mBucket.remove(offset);
// 最终实现 detach 操作的回调
mCallback.detachViewFromParent(offset);
}
}

LayoutManager会将 detach 任务委托给ChildHelperChildHelper再执行detachViewFromParent()回调,它在初始化ChildHelper时被实现:

public class RecyclerView {
// 初始化 ChildHelper
private void initChildrenHelper() {
// 构建 ChildHelper 实例
mChildHelper = new ChildHelper(new ChildHelper.Callback() {
@Override
public void detachViewFromParent(int offset) {
final View view = getChildAt(offset);

// 调用 ViewGroup.detachViewFromParent()
RecyclerView.this.detachViewFromParent(offset);
}

}
}
}

RecyclerView detach 表项的最后一步调用了ViewGroup.detachViewFromParent()

public abstract class ViewGroup {
// detach 子控件
protected void detachViewFromParent(int index) {
removeFromArray(index);
}

// 删除子控件的最后一步
private void removeFromArray(int index) {
final View[] children = mChildren;
// 将子控件持有的父控件引用置空
if (!(mTransitioningViews != null && mTransitioningViews.contains(children[index]))) {
children[index].mParent = null;
}
final int count = mChildrenCount;
// 将父控件持有的子控件引用置空
if (index == count - 1) {
children[–mChildrenCount] = null;
} else if (index >= 0 && index < count) {
System.arraycopy(children, index + 1, children, index, count - index - 1);
children[–mChildrenCount] = null;
}

}
}

ViewGroup.removeFromArray()是容器控件移除子控件的最后一步(ViewGroup.removeView()也会调用这个方法)。

至此可以得出结论:

在每次向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();// 后布局

更多Android高级工程师进阶学习资料

进阶学习视频

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

[外链图片转存中…(img-DvmrYopB-1715396098949)]

附上:我们之前因为秋招收集的二十套一二线互联网公司Android面试真题(含BAT、小米、华为、美团、滴滴)和我自己整理Android复习笔记(包含Android基础知识点、Android扩展知识点、Android源码解析、设计模式汇总、Gradle知识点、常见算法题汇总。)

[外链图片转存中…(img-fzUGAKRi-1715396098950)]

里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中…
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值