上一篇文章分析RecyclerView刷新机制知道
LayoutManager
在布局子View
时会向Recycler
索要一个ViewHolder
。但从Recycler
中获取一个ViewHolder
的前提是Recycler
中要有ViewHolder
。那Recycler
中是如何有ViewHolder
的呢?
本文会分析两个问题:
RecyclerView
的View
是在什么时候放入到Recycler
中的。以及在Recycler
中是如何保存的。LayoutManager
在向Recycler
获取ViewHolder
时,Recycler
寻找ViewHolder
的逻辑是什么。
即何时存、怎么存
和何时取、怎么取
的问题。何时取
已经很明显了:LayoutManager
在布局子View
时会从Recycler
中获取子View
。 所以本文要理清的是其他3个问题。在文章继续之前要知道Recycler
管理的基本单元是ViewHolder
,LayoutManager
操作的基本单元是View
,即ViewHolder
的itemview
。本文不会分析RecyclerView
动画时view
的复用逻辑。
为了接下来的内容更容易理解,先回顾一下Recycler
的组成结构:
Recycler的组成.png
mChangedScrap
: 用来保存RecyclerView
做动画时,被detach的ViewHolder
。mAttachedScrap
: 用来保存RecyclerView
做数据刷新(notify
),被detach的ViewHolder
mCacheViews
:Recycler
的一级ViewHolder
缓存。RecyclerViewPool
:mCacheViews
集合中装满时,会放到这里。
先看一下如何从Recycler
中取一个ViewHolder
来复用。
从Recycler中获取一个ViewHolder的逻辑
LayoutManager
会调用Recycler.getViewForPosition(pos)
来获取一个指定位置(这个位置是子View布局所在的位置)的view
。getViewForPosition()
会调用tryGetViewHolderForPositionByDeadline(position...)
, 这个方法是从Recycler
中获取一个View
的核心方法。它就是如何从Recycler中获取一个ViewHolder
的逻辑,即怎么取
。方法太长, 我做了很多裁剪:
ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) { ... 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); //从 attach 和 mCacheViews 中获取 if (holder != null) { ... //校验这个holder是否可用 } } if (holder == null) { ... final int type = mAdapter.getItemViewType(offsetPosition); //获取这个位置的数据的类型。 子Adapter复写的方法 // 2) Find from scrap/cache via stable ids, if exists if (mAdapter.hasStableIds()) { //stable id 就是标识一个viewholder的唯一性, 即使它做动画改变了位置 holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), //根据 stable id 从 scrap 和 mCacheViews中获取 type, dryRun); .... } if (holder == null && mViewCacheExtension != null) { // 从用户自定义的缓存集合中获取 final View view = mViewCacheExtension .getViewForPositionAndType(this, position, type); //你返回的View要是RecyclerView.LayoutParams属性的 if (view != null) { holder = getChildViewHolder(view); //把它包装成一个ViewHolder ... } } if (holder == null) { // 从 RecyclerViewPool中获取 holder = getRecycledViewPool().getRecycledView(type); ... } if (holder == null) { ... //实在没有就会创建 holder = mAdapter.createViewHolder(RecyclerView.this, type); ... } } ... boolean bound = false; if (mState.isPreLayout() && holder.isBound()) { //动画时不会想去调用 onBindData ... } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) { ... final int offsetPosition = mAdapterHelper.findPositionOffset(position); bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs); //调用 bindData 方法 } final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams(); final LayoutParams rvLayoutParams; ...调整LayoutParams return holder; }
即大致步骤是:
如果执行了
RecyclerView
动画的话,尝试根据position
从mChangedScrap集合
中寻找一个ViewHolder
尝试
根据position
从scrap集合
、hide的view集合
、mCacheViews(一级缓存)
中寻找一个ViewHolder
根据
LayoutManager
的position
更新到对应的Adapter
的position
。 (这两个position
在大部分情况下都是相等的,不过在子view删除或移动
时可能产生不对应的情况)根据
Adapter position
,调用Adapter.getItemViewType()
来获取ViewType
根据
stable id(用来表示ViewHolder的唯一,即使位置变化了)
从scrap集合
和mCacheViews(一级缓存)
中寻找一个ViewHolder
根据
position和viewType
尝试从用户自定义的mViewCacheExtension
中获取一个ViewHolder
根据
ViewType
尝试从RecyclerViewPool
中获取一个ViewHolder
调用
mAdapter.createViewHolder()
来创建一个ViewHolder
如果需要的话调用
mAdapter.bindViewHolder
来设置ViewHolder
。调整
ViewHolder.itemview
的布局参数为Recycler.LayoutPrams
,并返回Holder
虽然步骤很多,逻辑还是很简单的,即从几个缓存集合中获取ViewHolder
,如果实在没有就创建。但比较疑惑的可能就是上述ViewHolder缓存集合
中什么时候会保存ViewHolder
。接下来分几个RecyclerView
的具体情形,来一点一点弄明白这些ViewHolder缓存集合
的问题。
情形一 : 由无到有
即一开始RecyclerView
中没有任何数据,添加数据源后adapter.notifyXXX
。状态变化如下图:
State由无到有.png
很明显在这种情形下Recycler
中是不会存在任何可复用的ViewHolder
。所以所有的ViewHolder
都是新创建的。即会调用Adapter.createViewHolder()和Adapter.bindViewHolder()
。那这些创建的ViewHolder
会缓存起来吗?
这时候新创建的这些ViewHolder
是不会被缓存起来的。 即在这种情形下: Recycler只会通过Adapter创建ViewHolder,并且不会缓存这些新创建的ViewHolder
情形二 : 在原有数据的情况下进行整体刷新
就是下面这种状态:
State由有到有.png
其实就是相当于用户在feed中做了下拉刷新。实现中的伪代码如下:
dataSource.clear()dataSource.addAll(newList)adapter.notifyDatasetChanged()
在这种情形下猜想Recycler
肯定复用了老的卡片(卡片的类型不变),那么问题是 : 在用户刷新时旧ViewHolder
保存在哪里? 如何调用旧ViewHolder
的Adapter.bindViewHolder()
来重新设置数据的?
其实在上一篇文章Recycler刷新机制
中,LinearLayoutManager
在确定好布局锚点View
之后就会把当前attach
在RecyclerView
上的子View
全部设置为scrap状态
:
void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { ... onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection); // RecyclerView指定锚点,要准备正式布局了 detachAndScrapAttachedViews(recycler); // 在开始布局时,把所有的View都设置为 scrap 状态 ... }
什么是scrap状态呢? 在前面的文章其实已经解释过: ViewHolder被标记为FLAG_TMP_DETACHED
状态,并且其itemview
的parent
被设置为null
。
detachAndScrapAttachedViews
就是把所有的view保存到Recycler
的mAttachedScrap
集合中:
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) { for (int i = getChildCount() - 1; i >= 0; i--) { final View v = getChildAt(i); scrapOrRecycleView(recycler, i, v); } }private void scrapOrRecycleView(Recycler recycler, int index, View view) { final ViewHolder viewHolder = getChildViewHolderInt(view); ...删去了一些判断逻辑 detachViewAt(index); //设置RecyclerView这个位置的view的parent为null, 并标记ViewHolder为FLAG_TMP_DETACHED recycler.scrapView(view); //添加到mAttachedScrap集合中 ... }
所以在这种情形下LinearLayoutManager
在真正摆放子View
之前,会把所有旧的子View
按顺序保存到Recycler
的mAttachedScrap集合
中
接下来继续看,LinearLayoutManager
在布局时如何复用mAttachedScrap集合
中的ViewHolder
。
前面已经说了LinearLayoutManager
会当前布局子View的位置向Recycler
要一个子View,即调用到tryGetViewHolderForPositionByDeadline(position..)
。我们上面已经列出了这个方法的逻辑,其实在前面的第二步:
尝试根据position
从scrap集合
、hide的view集合
、mCacheViews(一级缓存)
中寻找一个ViewHolder
即从mAttachedScrap
中就可以获得一个ViewHolder
:
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) { final int scrapCount = mAttachedScrap.size(); 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; } } ... }
即如果mAttachedScrap中holder
的位置和入参position
相等,并且holder
是有效的话这个holder
就是可以复用的。所以综上所述,在情形二下所有的ViewHolder
几乎都是复用Recycler中mAttachedScrap集合
中的。
并且重新布局完毕后Recycler
中是不存在可复用的ViewHolder
的。
情形三 : 滚动复用
这个情形分析是在情形二
的基础上向下滑动时ViewHolder
的复用情况以及Recycler
中ViewHolder
的保存情况, 如下图:
State滚动复用.png
在这种情况下滚出屏幕的View会优先保存到mCacheViews
, 如果mCacheViews
中保存满了,就会保存到RecyclerViewPool
中。
在前一篇文章RecyclerView刷新机制
中分析过,RecyclerView
在滑动时会调用LinearLayoutManager.fill()
方法来根据滚动的距离来向RecyclerView
填充子View,其实在个方法在填充完子View之后就会把滚动出屏幕的View做回收:
int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) { ... int remainingSpace = layoutState.mAvailable + layoutState.mExtra; ... while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) { ... layoutChunk(recycler, state, layoutState, layoutChunkResult); //填充一个子View if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) { layoutState.mScrollingOffset += layoutChunkResult.mConsumed; if (layoutState.mAvailable < 0) { layoutState.mScrollingOffset += layoutState.mAvailable; } recycleByLayoutState(recycler, layoutState); //根据滚动的距离来回收View } } }
即fill
每填充一个子View
都会调用recycleByLayoutState()
来回收一个旧的子View
,这个方法在层层调用之后会调用到Recycler.recycleViewHolderInternal()
。这个方法是ViewHolder
回收的核心方法,不过逻辑很简单:
检查
mCacheViews集合
中是否还有空位,如果有空位,则直接放到mCacheViews集合
如果没有的话就把
mCacheViews集合
中最前面的ViewHolder
拿出来放到RecyclerViewPool
中,然后再把最新的这个ViewHolder放到mCacheViews集合
如果没有成功缓存到
mCacheViews集合
中,就直接放到RecyclerViewPool
mCacheViews集合
为什么要这样缓存? 看一下下面这张图 :
mCacheViews的缓存逻辑.png
我是这样认为的,如上图,往上滑动一段距离,被滑动出去的ViewHolder
会被缓存在mCacheViews集合
,并且位置是被记录的。如果用户此时再下滑的话,可以参考文章开头的从Recycler
中获取ViewHolder的逻辑:
先按照位置从
mCacheViews集合
中获取按照
viewType
从mCacheViews集合
中获取
上面对于mCacheViews集合
两步操作,其实第一步就已经命中了缓存的ViewHolder
。并且这时候都不需要调用Adapter.bindViewHolder()
方法的。即是十分高效的。
所以在普通的滚动复用的情况下,ViewHolder
的复用主要来自于mCacheViews集合
, 旧的ViewHolder
会被放到mCacheViews集合
, mCacheViews集合
挤出来的更老的ViewHolder
放到了RecyclerViewPool
中
到这里基本的复用情形都覆盖了,其他的就涉及到RecyclerView动画
了。这些点在下一篇文章继续看。
作者:susion哒哒
链接:https://www.jianshu.com/p/aeb9ccf6a5a4
共同学习,写下你的评论
评论加载中...
作者其他优质文章