真正带你搞懂-RecyclerView-的缓存机制,再也不怕面试被虐了(3)

static class ScrapData {
ArrayList mScrapHeap = new ArrayList<>();
int mMaxScrap = DEFAULT_MAX_SCRAP;
…… 省略 ……
}
SparseArray mScrap = new SparseArray<>();
…… 省略后面代码 ……
}

  • 首先我们看到一个常量‘DEFAULT_MAX_SCRAP’,这个就是缓存池定义的一个默认的缓存数,当然这个缓存数我们是可以自己设置的。而且这个缓存数量不是指整个缓存池只能缓存这么多,而是每个不同itemType的ViewHolder的缓存数量。

  • 接着往下看,我们看到一个静态内部类ScrapData,这里我们只看跟缓存相关的两个变量,先说mMaxScrap,前面的常量赋值给了它,这也就印证了我们前面说的这个缓存数量是对应每一种类型的ViewHolder的。再来看这个mScrapHeap变量,熟悉的一幕又来了,同样是一个缓存ViewHolder对象的ArrayList,它的容量默认是5.

  • 最后我们看到mScrap这个变量,它是一个存储我们上面提到的ScrapData类的对象的SparseArray,这样我们这个RecyclerPool就把不同itemType的ViewHolder按类型分类缓存了起来。

mViewCacheExtension:这一级缓存是留给开发者自由发挥的,官方并没有默认实现,它本身是null。

垃圾桶讲完了,哦不,是缓存层级讲完了。这里提一句,其实还有一层没有提到,因为它不在Recycler这个类中,它在ChildHelper类中,其中有个mHiddenViews,是个缓存被隐藏的ViewHolder的ArrayList。到这里我想大家对这几层缓存心里已经有个数了,但是还远远不够,这么多层缓存是怎么工作的?什么时候用什么缓存?各个缓存之间有没有什么PY交易?如果让你自己写一个LayoutManager你能处理好缓存问题么?就好比垃圾分类后,我们知道每种垃圾桶的定义和功能,但是面对大妈的灵魂拷问我依然分不清自己是什么垃圾,我太难了~相比之下,RV的几个垃圾桶简单多了,下面我们一起来看看,这些个缓存都咋用。

各缓存的使用

上面我们介绍了RV的各缓存层级,但是它们是怎么工作的呢?为什么要设计这些层级呢?别急,我们去源码中找找答案。一叶落而知天下秋,我们就从官方自带的最简单的布局管理者LinearLayoutManager入手,来看看到底如何使用这几级缓存写出一个合格的布局管理者。

RV从无到有的加载过程

首先我们看一下RV从无到有是怎么显示出数据来的。大家因该知道一个视图的显示要经过onMeasure、onLayout、onDraw三个方法,那么我们就先从第一个方法onMeasure入手,来看看里面做了什么。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
if (mLayout == null) {
defaultOnMeasure(widthSpec, heightSpec);
return;
}
if (mLayout.mAutoMeasure) {
if (mState.mLayoutStep == State.STEP_START) {
dispatchLayoutStep1();
}
dispatchLayoutStep2();
}
}

上面代码省略了一些无关代码,我们只看我们关心的,dispatchLayoutStep1和2方法,1方法中如果mState.mRunPredictiveAnimations为true会调用mLayout.onLayoutChildren(mRecycler, mState)这个方法,但是一般RV的预测动画都为false,所以我们看一下2方法,方法中同样调用了mLayout.onLayoutChildren(mRecycler, mState)方法,来看一下:

//已省略无关代码
private void dispatchLayoutStep2() {
eatRequestLayout();
onEnterLayoutOrScroll();

// Step 2: Run layout
mState.mInPreLayout = false;
mLayout.onLayoutChildren(mRecycler, mState);

mState.mLayoutStep = State.STEP_ANIMATIONS;
onExitLayoutOrScroll();
resumeRequestLayout(false);
}

这里onLayoutChildren方法是必走的,而mLayout是RV的成员变量,也就是LayoutManager,接下来我们去LinearLayoutManager里看看onLayoutChildren方法做了什么。

//已省略无关代码
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {

detachAndScrapAttachedViews(recycler);

if (mAnchorInfo.mLayoutFromEnd) {
// fill towards start
fill(recycler, mLayoutState, state, false);

// fill towards end
fill(recycler, mLayoutState, state, false);
endOffset = mLayoutState.mOffset;
} else {
// fill towards end
fill(recycler, mLayoutState, state, false);

// fill towards start
fill(recycler, mLayoutState, state, false);
startOffset = mLayoutState.mOffset;
}
}

这个方法挺长的,我们只看最关心的,来看下detachAndScrapAttachedViews(recycler)方法中做了什么。

public void detachAndScrapAttachedViews(Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i–) {
final View v = getChildAt(i);
scrapOrRecycleView(recycler, i, v);
}
}

如果有子view调用了scrapOrRecycleView(recycler, i, v)方法,继续追踪。

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 {
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}

正常开始布局的时候会进入else分支,首先是调用detachViewAt(index)来分离视图,然后调用了recycler.scrapView(view)方法。前面我们说过Recycler是RV的内部类,是管理RV缓存的核心类,然后我们继续追踪这个srapView方法,看看里面做了什么。

void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException(“……”);
}
holder.setScrapContainer(this, false);
mAttachedScrap.add(holder);
}
}

这里我们看到了熟悉的身影,「mAttachedScrap」,到此为止我们知道了,onLayoutChildren方法中调用detachAndScrapAttachedViews方法把存在的子view先分离然后缓存到了AttachedScrap中。我们回到onLayoutChildren方法中看看接下来做了什么,我们发现它先判断了方向,因为LinearLayoutManager有横纵两个方向,无论哪个方向最后都是调用fill方法,见名知意,这是个填充布局的方法,fill方法中又调用了layoutChunk这个方法,我们看一眼这个方法。

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
LayoutState layoutState, LayoutChunkResult result) {
View view = layoutState.next(recycler);
if (view == null) {
return;
}
if (layoutState.mScrapList == null) {
if (mShouldReverseLayout == (layoutState.mLayoutDirection
== LayoutState.LAYOUT_START)) {
addView(view);
} else {
addView(view, 0);
}
}
}

该方法中我们看到通过layoutState.next(recycler)方法来拿到视图,如果这个视图为null那么方法终止,否则就会调用addView方法将视图添加或者重新attach回来,这个我们不关心,我们看看是怎么拿到视图的。

View next(RecyclerView.Recycler recycler) {
if (mScrapList != null) {
return nextViewFromScrapList();
}
final View view = recycler.getViewForPosition(mCurrentPosition);
mCurrentPosition += mItemDirection;
return view;
}

首先我们看到如果mScrapList不为空会去其中取视图,mScrapList是什么呢?实际上它就是mAttachedScrap,但是它是只读的,而且只有在开启预测动画时才会被赋值,所以我们忽略它即可。重点关注下recycler.getViewForPosition(mCurrentPosition)方法,这个方法经过层层调用,最终是调用的Recycler类中的「tryGetViewHolderForPositionByDeadline(int position,boolean dryRun,long deadlineNs)」方法,接下来看一下这个方法做了哪些事。

@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
}
if (holder == null) {
// 2) Find from scrap/cache via stable ids, if exists
if (holder == null && mViewCacheExtension != null) {
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
}
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
}
if (holder == null) {
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
}
return holder;
}

这段代码着实做了不少事情,获取View和绑定View都是在这个方法中完成的,当然关于绑定和其它的无关代码这里就不贴了。我们一步步的看一下:

1. 第一步先从 getChangedScrapViewForPosition(position)方法中找需要的视图,但是有个条件mState.isPreLayout()要为true,这个一般在我们调用adapter的notifyItemChanged等方法时为true,其实也很好理解,数据发生了变化,viewholder被detach掉后缓存在mChangedScrap之中,在这里拿到的viewHolder后续需要重新绑定。

2. 第二步,如果没有找到视图则从getScrapOrHiddenOrCachedHolderForPosition这个方法中继续找。这个方法的代码就不贴了,简单说下这里的查找顺序:

  • 首先从mAttachedScrap中查找
  • 再次从前面略过的ChildHelper类中的mHiddenViews中查找
  • 最后是从mCachedViews中查找的

3. 第三步, mViewCacheExtension中查找,我们说过这个对象默认是null的,是由我们开发者自定义缓存策略的一层,所以如果你没有定义过,这里是找不到View的。

4. 第四步,从RecyclerPool中查找,前面我们介绍过RecyclerPool,先通过itemType从SparseArray类型的mscrap中拿到ScrapData,不为空继续拿到scrapHeap这个ArrayList,然后取到视图,这里拿到的视图需要重新绑定。

5. 第五步,如果前面几步都没有拿到视图,那么调用了mAdapter.createViewHolder(RecyclerView.this, type)方法,这个方法内部调用了一个抽象方法onCreateViewHolder,是不是很熟悉,没错,就是我们自己写一个Adapter要实现的方法之一。

到此为止我们获取一个视图的流程就讲完了,获取到视图之后就是怎么摆放视图并添加到RV之中,然后最终展示到我们面前。细心的小伙伴可能发现这个流程貌似有点问题啊?第一次进入onLayoutChildren时还没有任何子view,在fill方法前等于没有缓存子view,所有的子View都是第五步onCreateViewHolder创建而来的。实际上这里的设计是有道理的,除了一些特殊情况onLayoutChildren方法会被多次调用外,一个View从无到有展示在我们面前要至少经过两次onMeasure,一次onLayout,一次onDraw方法(为什么是这样的呢,感兴趣的小伙伴可以去ViewRootImpl中找找答案)。所以这里需要做个缓存,而不至于每次都重新创建新的视图。整个过程大致如图:

这里提一下,在RV展示成功后,Scrap这层的缓存就为空了,在从Scrap中取视图的同时就被移出了缓存。在onLayout这里最终会调用到dispatchLayoutStep3方法,没错,除了1和2还有3,在3中,如果Scrap还有缓存,那么缓存会被清空,清空的缓存会被添加到mCachedViews或者RecyclerPool中。

RV滑动时的缓存过程

RV是可以通过滚动来展示大量数据的控件,那么由当前屏幕滚动而出的View去哪了?滚动而入的View哪来的?同样的,我们去源码中找找答案。

scrollHorizontallyBy,scrollVerticallyBy

  • 一个LayoutManager如果可以滑动,那么上面的两个方法要返回非0值,分别代表可以横向滚动和纵向滚动。最终两个方法都会调用scrollBy方法,然后scrollby方法调用了fill方法,这个fill我们已经见过了,现在再看一下。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
RecyclerView.State state, boolean stopOnFocusable) {
final int start = layoutState.mAvailable;
if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
// TODO ugly bug fix. should not happen

建议

当我们出去找工作,或者准备找工作的时候,我们一定要想,我面试的目标是什么,我自己的技术栈有哪些,近期能掌握的有哪些,我的哪些短板 ,列出来,有计划的去完成,别看前两天掘金一些大佬在驳来驳去 ,他们的观点是他们的,不要因为他们的观点,膨胀了自己,影响自己的学习节奏。基础很大程度决定你自己技术层次的厚度,你再熟练框架也好,也会比你便宜的,性价比高的替代,很现实的问题但也要有危机意识,当我们年级大了,有哪些亮点,与比我们经历更旺盛的年轻小工程师,竞争。

  • 无论你现在水平怎么样一定要 持续学习 没有鸡汤,别人看起来的毫不费力,其实费了很大力,这四个字就是我的建议!!!!!!!!!

  • 准备想说怎么样写简历,想象算了,我觉得,技术就是你最好的简历

  • 我希望每一个努力生活的it工程师,都会得到自己想要的,因为我们很辛苦,我们应得的。

  • 有什么问题想交流,欢迎给我私信,欢迎评论

【附】相关架构及资料

Android高级技术大纲

面试资料整理

内含往期Android高级架构资料、源码、笔记、视频。高级UI、性能优化、架构师课程、NDK、混合式开发(ReactNative+Weex)微信小程序、Flutter全方面的Android进阶实践技术

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 4
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值