作者:opLW
参考:
1.启舰大神的文章
2.RecyclerView 源码分析(三) - RecyclerView的缓存机制
3.基于滑动场景解析RecyclerView的回收复用机制原理
最近RecyclerView用的很多,打算写一系列相关的文章,做一个总结,加深理解。? 有什么不对还望指出。(RV代表RecyclerView, LM代表LayoutManager,VH代表ViewHolder,RR代表Recycler)
目录
前言: 缓存机制是RV的一大特色,将分为上下两篇介绍RV的回收,复用以及结合场景具体分析。多级缓存使得RV即使展示大量的数据,也不出现OOM。RV的缓存机制主要涉及到ViewHolder,Recycler等。下面仅对正常滑动过程进行理解,对于动画模块暂不做研究?,实在太多太杂了。
1.概述
- 1) 下面根据个人理解,画出LayoutManager,Recycler,以及ViewHolder之间的关系。
- 2) 整体来说RV内部分工明确,LM负责布局(不熟悉布局过程的可以先看看RecyclerView (一)-- 绘制流程),RR负责管理回收复用VH,VH内部也会记录一些自身的相关信息。所以我主要研究RR内部对于回收和复用的原理。
2.RR的复用
- 1) RR的四级缓存 在深入查看回收和复用之前,我们先大概了解一下RR中的四级缓存。
级别 | 缓存的名字 | 作用 |
---|---|---|
第一级 | mAttachedScrap和mChangedScrap | mAttachedScrap中存放的是因布局需要,暂时从屏幕上移除下来的VH,马上又可以重新布局放回去的,不会真正被回收。比如刷新时,使用detachAndScrapAttachedViews(recycler)暂时取下VH,然后重新计算放回VH。mChangedScrap 主要存放的是数据有更新的VH,需要重新绑定数据。 |
第二级 | mCachedViews | 主要存放刚刚被移除的VH,默认大小为2,可以通过RV的setItemViewCacheSize设置。与mAttachedScrap不同的是,这个缓存里面存放的VH,是可能不会再被用到的,属于被回收的。比如一直向下滚动,那么一定不会用到;又如来回滚动,那么刚刚被移除的VH,又可以马上被用到。 |
第三级 | mViewCacheExtension | 这个是RV留给我们自己扩展的缓存,一般情况下不会用到 |
第四级 | mRecyclerPool | 最低的一级缓存,这里面的VH需要重新绑定数据。RecycledViewPool默认会为每种ViewType各存放5个需要重新绑定数据的VH。详情看下面 |
- 2) 布局过程中如何通过RR来获取VH呢? 三大LM中代码特别深奥耐人寻味,想深究的可自行查看源码(应该很爽?)。这里开门见山,布局过程最终调用RR的
public View getViewForPosition(int position)
方法来获取目标VH。下面深入查看获取VH的逻辑。 - 3) 先看看大体的源码:(先查看整体,后面再分段探讨)最终是在tryGetViewHolderForPositionByDeadline里面进行判断并返回一个非null的VH,从而获取它的itemView。
@NonNull public View getViewForPosition(int position) { return getViewForPosition(position, false); } View getViewForPosition(int position, boolean dryRun) { return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView; } ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ViewHolder holder = null; // 省略大部分判断和获取的代码 return holder; }
- 4)下面分段讨论省略的判断和获取代码。
- A 直接根据position获取
// 0) If there is a changed scrap, try to find from there if (mState.isPreLayout()) { ==1== holder = getChangedScrapViewForPosition(position); fromScrapOrHiddenOrCache = holder != null; } // 1) Find by position from scrap/hidden list/cache if (holder == null) { ==2== holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); if (holder != null) { if (!validateViewHolderForOffsetPosition(holder)) { ==3== // recycle holder (and unscrap if relevant) since it can't be used if (!dryRun) { // we would like to recycle this but need to make sure it is not used by // animation logic etc. holder.addFlags(ViewHolder.FLAG_INVALID); if (holder.isScrap()) { removeDetachedView(holder.itemView, false); holder.unScrap(); } else if (holder.wasReturnedFromScrap()) { holder.clearReturnedFromScrapFlag(); } recycleViewHolderInternal(holder); } holder = null; } else { fromScrapOrHiddenOrCache = true; } } }
- 1 从mChangedScrap获取 这里查阅了源码和其他文章,得出一个结论是:preLayout状态是跟动画处理相关的,这里我们暂不做深究。
- 2 从mAttachedScrap, mCachedViews获取 下面看看getScrapOrHiddenOrCachedHolderForPosition方法。
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); for (int i = 0; i < scrapCount; i++) { ==2.1== 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; } } // 省略部分代码 // Search in our first-level recycled view cache. final int cacheSize = mCachedViews.size(); ==2.2== for (int i = 0; i < cacheSize; i++) { final ViewHolder holder = mCachedViews.get(i); // invalid view holders may be in cache if adapter has stable ids as they can be // retrieved via getScrapOrCachedViewForId if (!holder.isInvalid() && holder.getLayoutPosition() == position) { if (!dryRun) { mCachedViews.remove(i); } if (DEBUG) { Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position + ") found match in cache: " + holder); } return holder; } } return null; }
- 2.1 从mAttachedScrap中,寻找有效且位置对应的VH,找到则直接返回。
- 2.2 从mCachedViews中,寻找有效且位置对应的VH,找到则直接返回。
- 3 大致看得出,是对2中取到的VH进行一些判断,如果不符合还要进行回收,重新放回缓存中。
- 总结 这个阶段会根据position去获取VH,主要是从第一级和第二级中获取,因为这两级的VH存放的VH都是最新被移除的,此时VH对应的position信息可能还有效,所以可以使用position去获取。
- B 根据ViewType和重新计算后的offsetPosition获取
if (holder == null) { // 根据原先的position去获取一个偏移之后的offsetPosition final int type = mAdapter.getItemViewType(offsetPosition); // 2) Find from scrap/cache via stable ids, if exists if (mAdapter.hasStableIds()) { ==4== holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun); if (holder != null) { // update position holder.mPosition = offsetPosition; fromScrapOrHiddenOrCache = true; } } if (holder == null && mViewCacheExtension != null) { ==5== // We are NOT sending the offsetPosition because LayoutManager does not // know it. final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); // 省略一些判断。 } if (holder == null) { ==6== if (DEBUG) { Log.d(TAG, "tryGetViewHolderForPositionByDeadline(" + position + ") fetching from shared pool"); } holder = getRecycledViewPool().getRecycledView(type); if (holder != null) { holder.resetInternal(); if (FORCE_INVALIDATE_DISPLAY_LIST) { invalidateDisplayListInt(holder); } } } if (holder == null) { ==7== // 省略部分代码 holder = mAdapter.createViewHolder(RecyclerView.this, type); // 省略部分代码 } } ==8==
- 4 如果我们的RV的adapter有设置stableId的话,这里根据viewType和offsetPosition再次去一级和二级中获取,这里不展开讨论。(还不是很理解stableId的用处 ?)
- 5 如果有自定义扩展的mViewCacheExtension,那么还会从mViewCacheExtension再看看有没有可以使用的,这里也不展开讨论。
- 6 从第四级mRecyclerPool获取 RecycledViewPool是RV的一个内部类,其内部通过一个SparseArray类型的mScrap来存放ScrapData,而ScrapData又是一个含有默认大小为5的ArrayList。总结 RecycledViewPool默认会为每种ViewType各存放5个需要重新绑定数据的VH。下面看看源码理解一哈 ?
holder = getRecycledViewPool().getRecycledView(type); // RecycledViewPool#getRecycledView public ViewHolder getRecycledView(int viewType) { final ScrapData scrapData = mScrap.get(viewType); if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) { final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap; return scrapHeap.remove(scrapHeap.size() - 1); } return null; } // RecycledViewPool#ScrapData 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<>(); private static final int DEFAULT_MAX_SCRAP = 5;
- 7 没有获取到合适的VH,则调用
mAdapter.createViewHolder(RecyclerView.this, type)
重新create一个。 - 总结 这一部分主要从更低级的缓存获取,甚至重新构建一个。
- C 8 注意 最后省略部分代码,主要是对前面两个步骤获取到的VH进行统一检查。检查数据是否需要重新绑定,要则调用adapter.bindViewHolder。检查VH的itemView是否含有LayoutParams,没有则调用LM的generateDefaultLayoutParams产生默认的LayoutParams。
- A 直接根据position获取
3.总结
- 1)总结 在复用时从上到下主要分三部分。第一部分根据position在一级和二级中寻找,没找到则进入第二部分;第二部分则根据重新计算后的offsetPosition和ViewType,如果有stableId则在所有级别中查找,不含有则在最后两级查找,最后还是没有则直接构建新的VH。第三部分主要进行统一的检查。
- 2)借用腾讯Bugly的一张图片总结一下
万水千山总是情,麻烦手下别留情。
如若讲得有不妥,文末留言告知我,
如若觉得还可以,收藏点赞要一起。
opLW原创七言律诗,转载请注明出处