本文由 寒鸦飞尽 受权投稿https://juejin.im/post/5d6382cd6fb9a06b2262c0f8javascript
RecyclerView大概是Android开发者接触最多的一个控件了,官方对其作了很好的封装抽象,使得它既灵活又好用,可是你真的了解它么?在它简单的使用方式之下着实是不简单,首先咱们看一下官方对它的介绍:java
A flexible view for providing a limited window into a large data set.编程
很简单,就一句话「为大量数据集提供一个有限的展现窗口的灵活视图」怎么展现大量的数据是个技术活,这些数据伴随着滚动逐渐展现在咱们眼前,可是展现过的滚走的视图呢?它们是否还存在?我想你们确定知道它们是要被回收的,否者来个几百上千条数据那还不OOM了。那么咱们今天就围绕RecyclerView的视图回收机制来谈一谈,到底RecyclerView的回收机制是怎样的。缓存
咱们先了解下Recycler的缓存结构是怎样的,先了解两个专业词汇:app
Scrap (view):在布局期间进入临时分离状态的子视图。废弃视图能够重复使用,而不会与父级RecyclerView彻底分离,若是不须要从新绑定,则不进行修改,若是视图被视为脏,则由适配器修改。(这里的脏怎么理解呢?就是指那些在展现以前必须从新绑定的视图,好比一个视图原来展现的是“张三”,以后须要展现“李四”了,那么这个视图就是脏视图,须要从新绑定数据后再展现的。)异步
Recycle (view):先前用于显示适配器特定位置的数据的视图能够放置在高速缓存中以供稍后重用再次显示相同类型的数据。这能够经过跳过初始布局或构造来显着提升性能。ide
RecyclerView的缓存类型呢基本也就是上面的两种,这时可能有同窗要站出来讲我不对了,胡说,RecyclerView明明有四级缓存,怎么就两种了,骚年稍安勿躁,且听我来慢慢分解。首先咱们先看一个RV(RecyclerView在后文简称RV)的内部类Recycler。布局
public final class Recycler { final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>(); ArrayList<ViewHolder> mChangedScrap = null; final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>(); RecycledViewPool mRecyclerPool; private ViewCacheExtension mViewCacheExtension; …… 省略 …… }
就是介个类掌握着RV的缓存大权,从上面的代码片断咱们能够看到这个类声明了五个成员变量。咱们一个个的来讲一下:post
mAttachedScrap:咱们能够看到这个变量是个存放ViewHolder对象的ArrayList,这一级缓存是没有容量限制的,只要符合条件的我来者不拒,全收了。前面讲两个专业术语的时候提到了Scrap,这个就属于Scrap中的一种,这里的数据是不作修改的,不会从新走Adapter的绑定方法。性能
mChangedScrap:这个变量和上边的mAttachedScrap是同样的,惟一不一样的从名字也能够看出来,它存放的是发生了变化的ViewHolder,若是使用到了这里的缓存的ViewHolder是要从新走Adapter的绑定方法的。
mCachedViews:这个变量一样是一个存放ViewHolder对象的ArrayList,可是这个不一样于上面的两个里面存放的是dettach掉的视图,它里面存放的是已经remove掉的视图,已经和RV分离的关系的视图,可是它里面的ViewHolder依然保存着以前的信息,好比position、和绑定的数据等等。这一级缓存是有容量限制的,默认是2(不一样版本API可能会有差别,本文基于API26.1.0)。
mRecyclerPool:这个变量呢自己是一个类,跟上面三个都不同。这里面保存的ViewHolder不只仅是removed掉的视图,并且是恢复了出厂设置的视图,任何绑定过的痕迹都没有了,想用这里缓存的ViewHolder那是铁定要从新走Adapter的绑定方法了。并且咱们知道RV支持多布局,因此这里的缓存是按照itemType来分开存储的,咱们来大体的看一下它的结构:
public static class RecycledViewPool { private static final int DEFAULT_MAX_SCRAP = 5; static class ScrapData { ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; …… 省略 …… } SparseArray<ScrapData> 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从无到有是怎么显示出数据来的。你们因该知道一个视图的显示要通过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是能够经过滚动来展现大量数据的控件,那么由当前屏幕滚动而出的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 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<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap; if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) { return; } scrap.resetInternal(); scrapHeap.add(scrap); }
这个方法中一样对容量作了判断,跟mCachedViews不同,若是容量满了,就再也不继续缓存了。在缓存以前先调用了scrap.resetInternal()方法,这个方法顾名思义是个重置的方法,缓存以前把视图的信息都清除掉了,这也是为何这里缓存满了以后就再也不继续缓存了,而不是把老的缓存替换掉,由于它们重置后都同样了(这里指具备同种itemType的是同样的)。这就是滑动缓存的全过程,至此咱们知道了滚动出去的视图去哪了,那么滚动进来的视图哪来的呢?
和从无到有的过程同样,最后滚动也调用了fill方法,那最后必然是要走到前面分析的获取视图的5个流程。前面说过在布局完成以后,Scrap层的缓存就是空的了,那就只能从mCachedViews或者RecyclerPool中取了,都取不到最后就会走onCreateViewHolder建立视图。到这里滑动时的缓存以及取缓存就讲完了。
这块我就简单说一下结论,感兴趣的同窗能够自行查看源码。为何咱们在有数据刷新的时候推荐你们使用notifyItemChanged等方法而不使用notifyDataSetChanged方法呢?
在调用notifyDataSetChanged方法后,全部的子view会被标记,这个标记致使它们最后都被缓存到RecyclerPool中,而后从新绑定数据。而且因为RecyclerPool有容量限制,若是不够最后就要从新建立新的视图了。
可是使用notifyItemChanged等方法会将视图缓存到mChangedScrap和mAttachedScrap中,这两个缓存是没有容量限制的,因此基本不会从新建立新的视图,只是mChangedScrap中的视图须要从新绑定一下。
咱们从缓存的几个类型以及布局、滚动、刷新几个方面全方位的剖析了RV的缓存机制。
这么多层缓存是怎么工做的?何时用什么缓存?各个缓存之间有没有什么PY交易?若是让你本身写一个LayoutManager你能处理好缓存问题么?
我相信你已经有了本身的答案,后续会推出一篇关于自定义LayoutManager的文章,敬请期待。
推荐阅读作技术最怕埋头苦干探索Flutter异步消息的实现互联网 HR 黑话大全,太真实了!
编程·思惟·职场