最近做项目的时候突然想到一个问题,就是在项目里面使用了多种ViewHolder,但是在onBindView这个方法中,RecyclerView是如何知道我在哪个位置需要的是哪种ViewHolder呢?就这个问题趁机看了一下源码,终于找到了答案,原来RecyclerView的缓存机制是区分type的,也就是Recyclerview.Adapter.getItemViewType()这个方法的返回值来区分的。 那么从哪里开始看RecyclerView的源码呢? 因为View创建出来需要加到RecyclerView中,所以从view的measure、layout中去找,RecyclerView的布局都是交给LayoutManager,这里以LinearLayoutManager为例,从onLayoutChildren开始,调用链为
onLayoutChildren --> fill --> layoutChunk这里fill、layoutChunk都是布局的核心代码,在layoutChunk开头有这么一段:
View view = layoutState.next(recycler);这个地方返回一个view,传进去的参数为recycler,大概可以猜到这和缓存有关了,点进去看看:
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }果然,使用到了recycler.getViewForPosition,这个就是缓存的其实方法,最终看到了tryGetViewHolderForPositionByDeadline这个方法,这里就是缓存逻辑的核心代码,这里只挑重点看:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ....... //和动画有关系,而且scrapList是在LayoutManager内部的,和recyclerview关系不大 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) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); final int type = mAdapter.getItemViewType(offsetPosition); ...... //扩展的缓存 if (holder == null && mViewCacheExtension != null) { // We are NOT sending the offsetPosition because LayoutManager does not // know it. final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); ....... } if (holder == null) { // fallback to pool if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + position + ") fetching from shared pool"); } //从Recycler中获取缓存 holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { long start = getNanoTime(); if (deadlineNs != FOREVER_NS && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) { // abort - we have a deadline we can't meet return null; } //创建ViewHolder holder = mAdapter.createViewHolder(RecyclerView.this, type); ...... } return holder; }上面的流程其实就是按照这么四级缓存一步一步查找下去,分别是
mAttachedScrap , mCachedViews ,mViewCacheExtension, recycledViewPool这四级缓存都是由Recycler这个类来管理的 下面分别介绍这四种缓存的作用:
一级缓存: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; } }在滑动过程中一般不到这个缓存中去查找,因为此缓存是和layout相关的,在layout过程中,会首先将recyclerview的子view全部detach下来,放到这个数组里面,然后经过layout过程,再一个个的加回去,在LinearLayoutManager.onLayoutChildren中先调用detachAndScrapAttachedViews,然后再进行fill操作:
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i); scrapOrRecycleView(recycler, i, v); } }上面很明显就是把所有childView detach下来,具体的detach操作还是ChildViewHelper来进行的。 那为什么在layout之前进行detach操作呢? 我猜这是和recyclerview特性相关的,因为recyclerview事先并不知道自己能有多少个childView,这不仅取决于childView的大小还取决于layoutmanager的具体布局方式,也就是说measure和layout的过程是相关的,等到childview放满了recyclerview就不再添加childView了。而一般的ViewGroup的chilgview个数都是确定的,childview个数和measure、layout完全无关,只要全部测量完,然后挨个layout就行。 由于以上的性质,recyclerview的measure和layout过程和一般的viewgroup都不一样,recyclerview是先测量childview1,然后摆放childview1.接着测量childview2,然后摆放childview2.然后继续,直到摆满recyclerview。 在代码中这样体现:
@Override protected void onMeasure(int widthSpec, int heightSpec) { if (mLayout.isAutoMeasureEnabled()) { ...... mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec); final boolean measureSpecModeIsExactly = widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY; if (measureSpecModeIsExactly || mAdapter == null) { return; } if (mState.mLayoutStep == State.STEP_START) { dispatchLayoutStep1(); } // set dimensions in 2nd step. Pre-layout should happen with old dimensions for // consistency mLayout.setMeasureSpecs(widthSpec, heightSpec); mState.mIsMeasuring = true; dispatchLayoutStep2(); }可以看出在Recyclerview的onMeasure阶段,就开始进行layout了。
二级缓存:mCachedViews 这个缓存中也是精确匹配的,也就是需要位置一样才能复用,并且复用之后不用重新bind数据,因为位置一样,数据就是一样的,不用重新bind,这个缓存的大小为2,数据结构使用了arraylist。三级缓存:mViewCacheExtension 这个属于自定义缓存,一般不使用四级缓存:mRecyclerPool 这个缓存中是根据type来复用的,每个type缓存5个holder,可以使用map+arraylist的数据结构缓存,因为type为int类型,所以使用SparseArray来作为键值对查找,效率更高。所以是SparseArray+ArrayList结构: static class ScrapData { final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>(); int mMaxScrap = DEFAULT_MAX_SCRAP; long mCreateRunningAverageNs = 0; long mBindRunningAverageNs = 0; } SparseArray<ScrapData> mScrap = new SparseArray<>();基于上面的缓存说明,写了一个例子来验证: 这里recyclerview宽高为正方形的三倍,我们来计算在整个滑动过程中会创建几个viewHolder,首先一屏之内最多可以出现12个item,因此先创建12个,当第四行出现,第一行消失时,先将第一行的三个viewholder回收,那么这时缓存池中的情况是,mCachedViews=2,mRecyclerPool=1,因为mCachedViews是精确匹配的,因此只有复用mRecyclerPool中的一个,所以第四行只能有一个复用,剩下两个创建,因此总计创建14个ViewHolder,再滑动其实就不会创建viewholder了。上面的计算是没有预加载的情况下得到的,默认Recyclerview是有预加载的,可以禁止,使用layoutmanager.setItemPrefetchEnabled(false)禁止预加载,预加载是为了滑动更顺滑,不过在这里为了简化分析,所以禁止了预加载。 具体demo链接: https://github.com/whoami-I/RecyclerViewExample