注册

RecyclerView 缓存机制 | 如何复用表项?(2)

RecyclerView 缓存机制 | 如何复用表项?(1)

第四次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
{
...
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 (view != null) {
//获得view对应的ViewHolder
holder = getChildViewHolder(view);
if (holder == null) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view which does not have a ViewHolder"
+ exceptionLabel());
} else if (holder.shouldIgnore()) {
throw new IllegalArgumentException("getViewForPositionAndType returned"
+ " a view that is ignored. You must call stopIgnoring before"
+ " returning this view." + exceptionLabel());
}
}
}
...
}
复制代码

经过从mAttachedScrapmCachedViews获取ViewHolder未果后,继续尝试通过ViewCacheExtension 获取:



/**
* ViewCacheExtension is a helper class to provide an additional layer of view caching that can
* be controlled by the developer.
* ViewCacheExtension提供了额外的表项缓存层,用户帮助开发者自己控制表项缓存
*


* When {@link Recycler#getViewForPosition(int)} is called, Recycler checks attached scrap and
* first level cache to find a matching View. If it cannot find a suitable View, Recycler will
* call the {@link #getViewForPositionAndType(Recycler, int, int)} before checking
* {@link RecycledViewPool}.
* 当Recycler从attached scrap和first level cache中未能找到匹配的表项时,它会在去RecycledViewPool中查找之前,先尝试从自定义缓存中查找
*


*/

public abstract static class ViewCacheExtension {

/**
* Returns a View that can be binded to the given Adapter position.
*


* This method should
not create a new View. Instead, it is expected to return
* an already created View that can be re-used for the given type and position.
* If the View is marked as ignored, it should first call
* {@link LayoutManager#stopIgnoringView(View)} before returning the View.
*


* RecyclerView will re-bind the returned View to the position if necessary.
*/

public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
}
复制代码


注释揭露了很多信息:ViewCacheExtension用于开发者自定义表项缓存,且这层缓存的访问顺序位于mAttachedScrapmCachedViews之后,RecycledViewPool 之前。这和Recycler. tryGetViewHolderForPositionByDeadline()中的代码逻辑一致,那接下来的第五次尝试,应该是从 RecycledViewPool 中获取 ViewHolder


第五次尝试


ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
{
...
if (holder == null) {
...
//从回收池中获取ViewHolder对象
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
...
}
复制代码

前四次尝试都未果,最后从RecycledViewPool 中获取ViewHolder稍等片刻!相对于从mAttachedScrap mCachedViews 中获取 ViewHolder,此处并没有严格的检验逻辑。为啥要区别对待不同的缓存? 大大的问号悬在头顶,但现在暂时无法解答,还是接着看RecycledViewPool 的结构吧~


public final class Recycler {
...
RecycledViewPool mRecyclerPool;
//获得RecycledViewPool实例
RecycledViewPool getRecycledViewPool() {
if (mRecyclerPool == null) {
mRecyclerPool = new RecycledViewPool();
}
return mRecyclerPool;
}
...
}
public static class RecycledViewPool {
...
//从回收池中获取ViewHolder对象
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList scrapHeap = scrapData.mScrapHeap;
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
...
}
复制代码

函数中只要访问了类成员变量,它的复杂度就提高了,因为类成员变量的作用于超出了函数体,使得函数就和类中其他函数耦合,所以不得不进行阅读更多以帮助理解该函数:


    public static class RecycledViewPool {
//同类ViewHolder缓存个数上限
private static final int DEFAULT_MAX_SCRAP = 5;

/**
* Tracks both pooled holders, as well as create/bind timing metadata for the given type.
* 回收池中存放单个类型ViewHolder的容器
*/

static class ScrapData {
//同类ViewHolder存储在ArrayList中
ArrayList mScrapHeap = new ArrayList<>();
//每种类型的ViewHolder最多存5个
int mMaxScrap = DEFAULT_MAX_SCRAP;
}
//回收池中存放所有类型ViewHolder的容器
SparseArray mScrap = new SparseArray<>();
...
//ViewHolder入池 按viewType分类入池,一个类型的ViewType存放在一个ScrapData中
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
//如果超限了,则放弃入池
if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
return;
}
if (DEBUG && scrapHeap.contains(scrap)) {
throw new IllegalArgumentException("this scrap item already exists");
}
scrap.resetInternal();
//回收时,ViewHolder从列表尾部插入
scrapHeap.add(scrap);
}
//从回收池中获取ViewHolder对象
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList scrapHeap = scrapData.mScrapHeap;
//复用时,从列表尾部获取ViewHolder(优先复用刚入池的ViewHoler)
return scrapHeap.remove(scrapHeap.size() - 1);
}
return null;
}
}
复制代码


  • 上述代码列出了RecycledViewPool 中最关键的一个成员变量和两个函数。至此可以得出结论:RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储(即是Adapter.getItemViewType()的返回值),同一类型的ViewHolder存放在ArrayList 中,且默认最多存储5个。
  • 相比较于mCachedViews,从mRecyclerPool中成功获取ViewHolder对象后并没有做合法性和表项位置校验,只检验viewType是否一致。所以 mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项

创建ViewHolder并绑定数据


ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
//所有缓存都没有命中,只能创建ViewHolder
if (holder == null) {
...
holder = mAdapter.createViewHolder(RecyclerView.this, type);
...
}
...
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);
}
}
复制代码


  • 再进行了上述所有尝试后,如果依然没有获得ViewHolder,只能重新创建并绑定数据。沿着调用链往下,就会找到熟悉的onCreateViewHolder()onBindViewHolder()
  • 绑定数据的逻辑嵌套在一个大大的if中(原来并不是每次都要绑定数据,只有满足特定条件时才需要绑定。
  • 那什么情况下需要绑定,什么情况下不需要呢?这就要引出“缓存优先级”这个概念。

缓存优先级




  • 缓存有优先级一说,在使用图片二级缓存(内存+磁盘)时,会先尝试去优先级高的内存中获取,若未命中再去磁盘中获取。优先级越高意味着性能越好。RecyclerView的缓存机制中是否也能套用“缓存优先级”这一逻辑?




  • 虽然为了获取ViewHolder做了5次尝试(共从6个地方获取),先排除3种特殊情况,即从mChangedScrap获取、通过id获取、从自定义缓存获取,正常流程中只剩下3种获取方式,优先级从高到低依次是:



    1. mAttachedScrap获取
    2. mCachedViews获取
    3. mRecyclerPool 获取



  • 这样的缓存优先级是不是意味着,对应的复用性能也是从高到低?(复用性能越好意味着所做的昂贵操作越少)



    1. 最坏情况:重新创建ViewHodler并重新绑定数据
    2. 次好情况:复用ViewHolder但重新绑定数据
    3. 最好情况:复用ViewHolder且不重新绑定数据

    毫无疑问,所有缓存都未命中的情况下会发生最坏情况。剩下的两种情况应该由3种获取方式来分摊,猜测优先级最低的 mRecyclerPool 方式应该命中次好情况,而优先级最高的 mAttachedScrap应该命中最好情况,去源码中验证一下:




ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();

// Try first for an exact, non-invalid match from scrap.
//1.从attached scrap回收集合中
for (int i = 0; i < scrapCount; i++) {
final ViewHolder holder = mAttachedScrap.get(i);
//只有当holder是有效时才返回
if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
&& !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
return holder;
}
}
}

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs)
{
...
if (holder == null) {
...
//从回收池中获取ViewHolder对象
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
//重置ViewHolder
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
...
//如果表项没有绑定过数据 或 表项需要更新 或 表项无效 且表项没有被移除时绑定表项数据
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);
}
...
}

public abstract static class ViewHolder {
/**
* This ViewHolder has been bound to a position; mPosition, mItemId and mItemViewType
* are all valid.
* 绑定标志位
*/

static final int FLAG_BOUND = 1 << 0;
/**
* This ViewHolder’s data is invalid. The identity implied by mPosition and mItemId
* are not to be trusted and may no longer match the item view type.
* This ViewHolder must be fully rebound to different data.
* 无效标志位
*/

static final int FLAG_INVALID = 1 << 2;
//判断ViewHolder是否无效
boolean isInvalid() {
//将当前ViewHolder对象的flag和无效标志位做位与操作
return (mFlags & FLAG_INVALID) != 0;
}
//判断ViewHolder是否被绑定
boolean isBound() {
//将当前ViewHolder对象的flag和绑定标志位做位与操作
return (mFlags & FLAG_BOUND) != 0;
}
/**
* 将ViewHolder重置
*/

void resetInternal() {
//将ViewHolder的flag置0
mFlags = 0;
mPosition = NO_POSITION;
mOldPosition = NO_POSITION;
mItemId = NO_ID;
mPreLayoutPosition = NO_POSITION;
mIsRecyclableCount = 0;
mShadowedHolder = null;
mShadowingHolder = null;
clearPayload();
mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
mPendingAccessibilityState = PENDING_ACCESSIBILITY_STATE_NOT_SET;
clearNestedRecyclerViewIfNotNested(this);
}
}
复制代码

温故知新,回看 mRecyclerPool 复用逻辑时,发现在成功获得ViewHolder对象后,立即对其重置(将flag置0)。这样就满足了绑定数据的判断条件(因为0和非0位与之后必然为0)。
同样的,在才mAttachedScrap中获取ViewHolder时,只有当其是有效的才会返回。所以猜测成立:mRecyclerPool中复用的ViewHolder需要重新绑定数据,从mAttachedScrap 中复用的ViewHolder不要重新出创建也不需要重新绑定数据


总结



  1. 在 RecyclerView 中,并不是每次绘制表项,都会重新创建 ViewHolder 对象,也不是每次都会重新绑定 ViewHolder 数据。
  2. RecyclerView 通过Recycler获得下一个待绘制表项。
  3. Recycler有4个层次用于缓存 ViewHolder 对象,优先级从高到底依次为ArrayList mAttachedScrapArrayList mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定 ViewHolder 对象。
  4. RecycledViewPool 对 ViewHolder 按viewType分类存储(通过SparseArray),同类 ViewHolder 存储在默认大小为5的ArrayList中。
  5. mRecyclerPool中复用的 ViewHolder 需要重新绑定数据,从mAttachedScrap 中复用的 ViewHolder 不需要重新创建也不需要重新绑定数据。
  6. mRecyclerPool中复用的ViewHolder ,只能复用于viewType相同的表项,从mCachedViews中复用的 ViewHolder ,只能复用于指定位置的表项。
  7. mCachedViews用于缓存指定位置的 ViewHolder ,只有“列表回滚”这一种场景(刚滚出屏幕的表项再次进入屏幕),才有可能命中该缓存。该缓存存放在默认大小为 2 的ArrayList中。

这篇文章粗略的回答了关于“复用”的4个问题,即“复用什么?”、“从哪里获得复用?”、“什么时候复用?”、“复用优先级”。读到这里,可能会有很多疑问:



  1. scrap view是什么?
  2. changed scrap viewattached scrap view有什么区别?
  3. 复用的 ViewHolder 是在什么时候被缓存的?
  4. 为什么要4层缓存?它们的用途有什么区别?

分析完“复用”,后续文章会进一步分析“回收”,希望到时候这些问题都能迎刃而解。


作者:唐子玄
链接:https://juejin.cn/post/6844903778303344647
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册