其他分享
首页 > 其他分享> > 真正带你搞懂-RecyclerView-的缓存机制,再也不怕面试被虐了

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

作者:互联网

// 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这个方法中继续找。这个方法的代码就不贴了,简单说下这里的查找顺序:

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这里最终会调用到dispatchLa
youtStep3方法,没错,除了1和2还有3,在3中,如果Scrap还有缓存,那么缓存会被清空,清空的缓存会被添加到mCachedViews或者RecyclerPool中。

RV滑动时的缓存过程

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

scrollHorizontallyBy,scrollVerticallyBy

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
if (layoutState.mAvailable < 0) {
layoutState.mScrollingOffset += layoutState.mAvailable;
}
recycleByLayoutState(recycler, layoutState);
}
}

这这段代码中判断了当前是否是滚动触发的fill方法,如果是调用recycleByLayoutState(recycler, layoutState)方法。这个方法几经周转会调用到removeAndRecycleViewAt方法:

public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}

这里注意先把视图remove掉了,而不是detach掉。然后调用Recycler中的recycleView方法,这个方法最后会调用recycleViewHolderInternal方法,方法如下:

void recycleViewHolderInternal(ViewHolder holder) {

if (forceRecycle || holder.isRecyclable()) {
if (省略) {
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize–;
}
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
}
}

删除不相关代码后逻辑很清晰。前面我们说过mCachedViews是有容量限制的,默认为2。那么如果符合放到mCachedViews中的条件,首先会判断mCachedViews是否已经满了,如果满了会通过recycleCachedViewAt(0)方法把最老得那个缓存放进RecyclerPool,然后在把新的视图放进mCachedViews中。如果这个视图不符合条件会直接被放进RecyclerPool中。我们注意到,在缓存进mCachedViews之前,我们的视图只是被remove掉了,绑定的数据等信息都还在,这意味着从mCachedViews取出的视图如果符合需要的目标视图是可以直接展示的,而不需要重新绑定。而放进RecyclerPool最终是要调用putRecycledView方法的。

public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
scrap.resetInternal();
scrapHeap.add(scrap);
}

这个方法中同样对容量做了判断,跟mCachedViews不一样,如果容量满了,就不再继续缓存了。在缓存之前先调用了scrap.resetInternal()方法,这个方法顾名思义是个重置的方法,缓存之前把视图的信息都清除掉了,这也是为什么这里缓存满了之后就不再继续缓存了,而不是把老的缓存替换掉,因为它们重置后都一样了(这里指具有同种itemType的是一样的)。这就是滑动缓存的全过程,至此我们知道了滚动出去的视图去哪了,那么滚动进来的视图哪来的呢?

数据更新时的缓存过程

这块我就简单说一下结论,感兴趣的同学可以自行查看源码。为什么我们在有数据刷新的时候推荐大家使用notifyItemChanged等方法而不使用notifyDataSetChanged方法呢?

总结

我们从缓存的几个类型以及布局、滚动、刷新几个方面全方位的剖析了RV的缓存机制。
。并且由于RecyclerPool有容量限制,如果不够最后就要重新创建新的视图了。

总结

我们从缓存的几个类型以及布局、滚动、刷新几个方面全方位的剖析了RV的缓存机制。

标签:缓存,recycler,方法,视图,RecyclerView,搞懂,holder,view
来源: https://blog.csdn.net/m0_65321931/article/details/122607079