在前一篇文章 RecyclerView 源码分析(一) —— 绘制流程解析 介绍了 RecyclerView 的绘制流程,RecyclerView 经过将绘制流程从 View 中抽取出来,放到 LayoutManager 中,使得 RecyclerView 在不一样的 LayoutManager 中,拥有不一样的样式,使得 RecyclerView 异常灵活,大大增强了 RecyclerView 使用场景。html
固然,RecyclerView 的缓存机制也是它特有的一个优势,减小了对内存的占用以及重复的绘制工做,所以,本文意在介绍和学习 RecyclerView 的缓存设计思想。数组
当咱们在讨论混存的时候,必定会经历建立-缓存-复用的过程。所以对于 RecyclerView 的缓存机制也是按照以下的步骤进行。缓存
在讲到对子 itemView 测量的时候,layoutChunk 方法中会先得到每个 itemView,在获取后,在将其添加到 RecyclerView 中。因此咱们先来看看建立的过程:微信
View next(RecyclerView.Recycler recycler) { if (mScrapList != null) { return nextViewFromScrapList(); } final View view = recycler.getViewForPosition(mCurrentPosition); mCurrentPosition += mItemDirection; return view; }
next
就是调用 RecyclerView
的 getViewForPosition
方法来获取一个 View
的。而 getViewForPosition
方法最终会调用到 RecyclerView
的tryGetViewHolderForPositionByDeadline
方法。数据结构
这个方法很长,可是其实逻辑很简单,整个过程前面部分是先从缓存尝试获取 VH,若是找不到,就会建立新的 VH,而后绑定数据,最后将再将 VH 绑定到 LayoutParams (LP) 上。架构
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { if (position < 0 || position >= mState.getItemCount()) { throw new IndexOutOfBoundsException("Invalid item position " + position + "(" + position + "). Item count:" + mState.getItemCount() + exceptionLabel()); } boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; // 省略从缓存查找 VH 的逻辑,下面是若是仍是没找到,就会建立一个新的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; }
// 建立 VH holder = mAdapter.createViewHolder(RecyclerView.this, type); if (ALLOW_THREAD_GAP_WORK) { // only bother finding nested RV if prefetching RecyclerView innerView = findNestedRecyclerView(holder.itemView); if (innerView != null) { holder.mNestedRecyclerView = new WeakReference<>(innerView); } } long end = getNanoTime(); mRecyclerPool.factorInCreateTime(type, end - start); if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder"); } } } // This is very ugly but the only place we can grab this information // before the View is rebound and returned to the LayoutManager for post layout ops. // We don't need this in pre-layout since the VH is not updated by the LM. if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) { holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST); if (mState.mRunSimpleAnimations) { int changeFlags = ItemAnimator .buildAdapterChangeFlagsForAnimations(holder); changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT; final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState, holder, changeFlags, holder.getUnmodifiedPayloads()); recordAnimationInfoIfBouncedHiddenView(holder, info); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { // do not update unless we absolutely have to. holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { if (DEBUG && holder.isRemoved()) { throw new IllegalStateException("Removed holder should be bound and it should" + " come here only in pre-layout. Holder: " + holder + exceptionLabel()); } final int offsetPosition = mAdapterHelper.findPositionOffset(position);
// 进行数据绑定 bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams;
// 下面逻辑就是将 VH 绑定到 LP, LP 又设置到 ItemView 上 if (lp == null) { rvLayoutParams = (LayoutParams) generateDefaultLayoutParams(); holder.itemView.setLayoutParams(rvLayoutParams); } else if (!checkLayoutParams(lp)) { rvLayoutParams = (LayoutParams) generateLayoutParams(lp); holder.itemView.setLayoutParams(rvLayoutParams); } else { rvLayoutParams = (LayoutParams) lp; } rvLayoutParams.mViewHolder = holder; rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound; return holder; }
即便省略了中间从缓存查找 VH 的逻辑,剩下部分的代码仍是很长。那我再归纳下 tryGetViewHolderForPositionByDeadline 方法所作的事:app
从缓存查找 VH ;less
缓存没有,那么就建立一个 VH;函数
判断 VH 需不须要更新数据,若是须要就会调用 tryBindViewHolderByDeadline 绑定数据;源码分析
将 VH 绑定到 LP, LP 又设置到 ItemView 上,互相依赖;
到这里关于建立 VH 的逻辑就讲完了。
在介绍添加到缓存的逻辑时,仍是须要介绍缓存相关的类和变量。
Recycler 是 RecyclerView 的一个内部类。咱们来看一下它的主要的成员变量。
mAttachedScrap 缓存屏幕中可见范围的 ViewHolder
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
mChangedScrap 缓存滑动时即将与 RecyclerView 分离的ViewHolder,按子View的position或id缓存,默认最多存放2个
ArrayList<ViewHolder> mChangedScrap = null;
mCachedViews ViewHolder 缓存列表,其大小由 mViewCacheMax 决定,默认 DEFAULT_CACHE_SIZE 为 2,可动态设置。
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
ViewCacheExtension 开发者可自定义的一层缓存,是虚拟类 ViewCacheExtension 的一个实例,开发者可实现方法 getViewForPositionAndType(Recycler recycler, int position, int type) 来实现本身的缓存。
private ViewCacheExtension mViewCacheExtension;
RecycledViewPool ViewHolder 缓存池,在有限的 mCachedViews 中若是存不下 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。
RecycledViewPool mRecyclerPool;
VH 被建立以后,是要被缓存,而后重复利用的,那么他们是何时被添加到缓存的呢?此处仍是以 LinearLayoutManager 举例说明。在 RecyclerView 源码分析(一) —— 绘制流程解析 一文中曾提到一个方法:
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { // ... detachAndScrapAttachedViews(recycler); // ... }
onLayoutChildren 是对子 view 进行绘制。在对子 view 会先调用 detachAndScrapAttachedViews 方法,下面来看看这个方法。
下面来看下这个方法:
// recyclerview public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { final int childCount = getChildCount(); for (int i = childCount - 1; i >= 0; i--) { final View v = getChildAt(i);
// 每一个 view 都会放到里面 scrapOrRecycleView(recycler, i, v); } } private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); if (viewHolder.shouldIgnore()) { if (DEBUG) { Log.d(TAG, "ignoring view " + viewHolder); } return; }
// 若是 VH 无效,而且已经被移除了,就会走另外一个逻辑 if (viewHolder.isInvalid() && !viewHolder.isRemoved() && !mRecyclerView.mAdapter.hasStableIds()) { removeViewAt(index); recycler.recycleViewHolderInternal(viewHolder); } else {
// 先 detch 掉,而后放入缓存中 detachViewAt(index); recycler.scrapView(view); mRecyclerView.mViewInfoStore.onViewDetached(viewHolder); } }
也就是在上面的逻辑里,被放到缓存中。这里就能够看到
若是是 remove,会执行 recycleViewHolderInternal(viewHolder)
方法,而这个方法最终会将 ViewHolder 加入 CacheView 和 Pool 中,
而当是 Detach,会将 View 加入到 ScrapViews 中
须要指出的一点是:须要区分两个概念,Detach 和 Remove
detach: 在 ViewGroup 中的实现很简单,只是将 ChildView 从 ParentView 的 ChildView 数组中移除,ChildView 的 mParent 设置为 null, 能够理解为轻量级的临时 remove, 由于 View此时和 View 树仍是藕断丝连, 这个函数被常常用来改变 ChildView 在 ChildView 数组中的次序。View 被 detach 通常是临时的,在后面会被从新 attach。
remove: 真正的移除,不光被从 ChildView 数组中除名,其余和 View 树各项联系也会被完全斩断(不考虑 Animation/LayoutTransition 这种特殊状况), 好比焦点被清除,从TouchTarget 中被移除等。
下面来看 Recycler 两个的具体逻辑方法:
/** * internal implementation checks if view is scrapped or attached and throws an exception * if so. * Public version un-scraps before calling recycle. */ void recycleViewHolderInternal(ViewHolder holder) {
// ...省略前面的代码,前面都是在作检验 final boolean transientStatePreventsRecycling = holder .doesTransientStatePreventRecycling(); @SuppressWarnings("unchecked") final boolean forceRecycle = mAdapter != null && transientStatePreventsRecycling && mAdapter.onFailedToRecycleView(holder); boolean cached = false; boolean recycled = false; if (DEBUG && mCachedViews.contains(holder)) { throw new IllegalArgumentException("cached view received recycle internal? " + holder + exceptionLabel()); } if (forceRecycle || holder.isRecyclable()) { if (mViewCacheMax > 0 && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_UPDATE | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) { // Retire oldest cached view 若是缓存数量超了,就会先移除最早加入的 int cachedViewSize = mCachedViews.size(); if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { recycleCachedViewAt(0); cachedViewSize--; } int targetCacheIndex = cachedViewSize; if (ALLOW_THREAD_GAP_WORK && cachedViewSize > 0 && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) { // when adding the view, skip past most recently prefetched views int cacheIndex = cachedViewSize - 1; while (cacheIndex >= 0) { int cachedPos = mCachedViews.get(cacheIndex).mPosition; if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) { break; } cacheIndex--; } targetCacheIndex = cacheIndex + 1; }
// 添加到缓存 mCachedViews.add(targetCacheIndex, holder); cached = true; } if (!cached) { addViewHolderToRecycledViewPool(holder, true); recycled = true; } } else { } // even if the holder is not removed, we still call this method so that it is removed // from view holder lists. mViewInfoStore.removeViewHolder(holder); if (!cached && !recycled && transientStatePreventsRecycling) { holder.mOwnerRecyclerView = null; } }
该方法所作的事具体以下:
检验该 VH 的有效性,确保已经再也不被使用;
判断缓存的容量,超了就会进行移除,而后找一个合适的位置进行添加。
mCachedViews 对应的数据结构也是 ArrayList 可是该缓存对集合的大小是有限制的,默认是 2。该缓存中 ViewHolder 的特性和 mAttachedScrap 中的特性是同样的,只要 position或者 itemId 对应上了,那么它就是干净的,无需从新绑定数据。开发者能够调用 setItemViewCacheSize(size) 方法来改变缓存的大小。该层级缓存触发的一个常见的场景是滑动 RV。固然 notifyXXX 也会触发该缓存。该缓存和 mAttachedScrap 同样特别高效。
RecyclerViewPool 缓存能够针对多ItemType,设置缓存大小。默认每一个 ItemType 的缓存个数是 5。并且该缓存能够给多个 RecyclerView 共享。因为默认缓存个数为 5,假设某个新闻 App,每屏幕能够展现 10 条新闻,那么必然会致使缓存命中失败,频繁致使建立 ViewHolder 影响性能。因此须要扩大缓存size。
接下去看 scrapView 这个方法:
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("Called scrap view with an invalid view." + " Invalid views cannot be reused from scrap, they should rebound from" + " recycler pool." + exceptionLabel()); } holder.setScrapContainer(this, false); // 这里的 false mAttachedScrap.add(holder); } else { if (mChangedScrap == null) { mChangedScrap = new ArrayList<ViewHolder>(); } holder.setScrapContainer(this, true); // 这里是 true mChangedScrap.add(holder); } }
该方法就比较简单了,没有那么多须要检验的逻辑。这里根据条件,有两种缓存类型能够选择,具体就不展开了,你们均可以看懂。这里讲解下两个 scrapView 的缓存。
mAttachedScrap 的对应数据结构是ArrayList,在 LayoutManager#onLayoutChildren 方法中,对 views 进行布局时,会将 RecyclerView 上的 Views 所有暂存到该集合中,以备后续使用,该缓存中的 ViewHolder 的特性是,若是和 RV 上的 position 或者 itemId 匹配上了,那么认为是干净的 ViewHolder,是能够直接拿出来使用的,无需调用 onBindViewHolder 方法。该 ArrayList 的大小是没有限制的,屏幕上有多少个 View,就会建立多大的集合。
触发该层级缓存的场景通常是调用 notifyItemXXX 方法。调用 notifyDataSetChanged 方法,只有当 Adapter hasStableIds 返回 true,会触发该层级的缓存使用。
mChangedScrap 和 mAttachedScrap 是同一级的缓存,他们是平等的。可是mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到 mChangedScrap 中。mChangedScrap缓存中的ViewHolder是须要调用onBindViewHolder方法从新绑定数据的。那么此时就有个问题了,为何同一级别的缓存须要设计两个不一样的缓存?
在 dispatchLayoutStep2 阶段 LayoutManager onLayoutChildren方法中最终会调用 layoutForPredictiveAnimations 方法,把 mAttachedScrap 中剩余的 ViewHolder 填充到屏幕上,因此他们的区别就是,mChangedScrap 中的 ViewHolder 在 RV 填充满的状况下,是不会强行填充到 RV 上的。那么有办法可让发生改变的 ViewHolder 进入 mAttachedScrap 缓存吗?固然能够。调用 notifyItemChanged(int position, Object payload) 方法能够,实现局部刷新功能,payload 不为空,那么发生改变的 ViewHolder 是会被分离到 mAttachedScrap 中的。
下面进入到最后一节,使用缓存。这个在以前绘制篇幅也有提到,下面直接看对应的方法:
//根据传入的position获取ViewHolder ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ---------省略---------- boolean fromScrapOrHiddenOrCache = false; ViewHolder holder = null; //预布局 属于特殊状况 从mChangedScrap中获取ViewHolder if (mState.isPreLayout()) { holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } if (holder == null) { //一、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder //二、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); ----------省略---------- } if (holder == null) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); ---------省略---------- final int type = mAdapter.getItemViewType(offsetPosition); //若是Adapter中声明了Id,尝试从id中获取,这里不属于缓存 if (mAdapter.hasStableIds()) { holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); } if (holder == null && mViewCacheExtension != null) { 3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存须要开发者实现 final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); if (view != null) { holder = getChildViewHolder(view); } } if (holder == null) { // fallback to pool //四、从缓存池mRecyclerPool中尝试获取ViewHolder holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { //若是获取成功,会重置ViewHolder状态,因此须要从新执行Adapter#onBindViewHolder绑定数据 holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { ---------省略---------- //五、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder建立一个 holder = mAdapter.createViewHolder(RecyclerView.this, type); } } boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { holder.mPreLayoutPosition = position; } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { final int offsetPosition = mAdapterHelper.findPositionOffset(position); //六、若是须要绑定数据,会调用Adapter#onBindViewHolder来绑定数据 bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); } ----------省略---------- return holder; }
上述逻辑用流程图表示:
总结一下上述流程:经过 mAttachedScrap、mCachedViews 及 mViewCacheExtension 获取的 ViewHolder 不须要从新建立布局及绑定数据;经过缓存池 mRecyclerPool 获取的 ViewHolder不须要从新建立布局,可是须要从新绑定数据;若是上述缓存中都没有获取到目标 ViewHolder,那么就会回调 Adapter#onCreateViewHolder 建立布局,以及回调 Adapter#onBindViewHolder来绑定数据。
咱们已经知道 ViewCacheExtension 属于第三级缓存,须要开发者自行实现,那么 ViewCacheExtension 在什么场景下使用?又是如何实现的呢?
首先咱们要明确一点,那就是 Recycler
自己已经设置了好几级缓存了,为何还要留个接口让开发者去自行实现缓存呢?
关于这一点,来看看 Recycler
中的其余缓存:
mAttachedScrap
用来处理可见屏幕的缓存;
mCachedViews
里存储的数据虽然是根据 position
来缓存,可是里面的数据随时可能会被替换的;
mRecyclerPool
里按 viewType
去存储 ArrayList< ViewHolder>
,因此 mRecyclerPool
并不能按 position
去存储 ViewHolder
,并且从 mRecyclerPool
取出的 View
每次都要去走 Adapter#onBindViewHolder
去从新绑定数据。
假如我如今须要在一个特定的位置(好比 position=0 位置)一直展现某个 View,且里面的内容是不变的,那么最好的状况就是在特定位置时,既不须要每次从新建立 View,也不须要每次都去从新绑定数据,上面的几种缓存显然都是不适用的,这种状况该怎么办呢?能够经过自定义缓存 ViewCacheExtension
实现上述需求。
结论援引自:Android ListView 与 RecyclerView 对比浅析--缓存机制
ListView和RecyclerView缓存机制基本一致:
mActiveViews 和 mAttachedScrap 功能类似,意义在于快速重用屏幕上可见的列表项ItemView,而不须要从新 createView 和 bindView;
mScrapView 和 mCachedViews + mReyclerViewPool功能类似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用.
RecyclerView 的优点在于
mCacheViews 的使用,能够作到屏幕外的列表项 ItemView 进入屏幕内时也无须bindView快速重用;
mRecyclerPool 能够供多个 RecyclerView 共同使用,在特定场景下,如 viewpaper+ 多个列表页下有优点.客观来讲,RecyclerView 在特定场景下对 ListView 的缓存机制作了补强和完善。
不一样使用场景:列表页展现界面,须要支持动画,或者频繁更新,局部刷新,建议使用 RecyclerView,更增强大完善,易扩展;其它状况(如微信卡包列表页)二者都OK,但ListView在使用上会更加方便,快捷。
https://www.jianshu.com/p/2b19e9bcda84
https://www.jianshu.com/p/6e6bf58b7f0d
https://www.jianshu.com/p/e1b257484961