这是RecyclerView
缓存机制系列文章的第三篇,系列文章的目录如下:
上一篇以列表滑动事件为起点沿着调用链一直往下寻找,验证了“滑出屏幕的表项”会被回收。那它们被回收去哪里了?沿着上一篇的调用链继续往下探究:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
...
/**
* Recycles views that went out of bounds after scrolling towards the end of the layout.
* 当向列表尾部滚动时回收滚出屏幕的表项
* <p>
* Checks both layout position and visible position to guarantee that the view is not visible.
*
* @param recycler Recycler instance of {@link android.support.v7.widget.RecyclerView}
* @param dt This can be used to add additional padding to the visible area. This is used
* to detect children that will go out of bounds after scrolling, without
* actually moving them.(该参数被用于检测滚出屏幕的表项)
*/
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
...
// ignore padding, ViewGroup may not clip children.
final int limit = dt;
final int childCount = getChildCount();
if (mShouldReverseLayout) {
for (int i = childCount - 1; i >= 0; i--) {
View child = getChildAt(i);
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
recycleChildren(recycler, childCount - 1, i);
return;
}
}
} else {
//遍历LinearLayoutManager的孩子找出其中应该被回收的
for (int i = 0; i < childCount; i++) {
View child = getChildAt(i);
//直到表项底部纵坐标大于某个值后,回收该表项以上的所有表项
if (mOrientationHelper.getDecoratedEnd(child) > limit
|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
// stop here
//回收索引为0到i-1的表项
recycleChildren(recycler, 0, i);
return;
}
}
}
}
...
}
recycleViewsFromStart()
通过遍历找到滑出屏幕的表项,然后调用了recycleChildren()
回收他们:
public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
/**
* Recycles children between given indices.
* 回收孩子
*
* @param startIndex inclusive
* @param endIndex exclusive
*/
private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
if (startIndex == endIndex) {
return;
}
if (DEBUG) {
Log.d(TAG, "Recycling " + Math.abs(startIndex - endIndex) + " items");
}
if (endIndex > startIndex) {
for (int i = endIndex - 1; i >= startIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
} else {
for (int i = startIndex; i > endIndex; i--) {
removeAndRecycleViewAt(i, recycler);
}
}
}
}
最终调用了父类LayoutManager.removeAndRecycleViewAt()
:
public abstract static class LayoutManager {
/**
* Remove a child view and recycle it using the given Recycler.
*
* @param index Index of child to remove and recycle
* @param recycler Recycler to use to recycle child
*/
public void removeAndRecycleViewAt(int index, Recycler recycler) {
final View view = getChildAt(index);
removeViewAt(index);
recycler.recycleView(view);
}
}
先从LayoutManager
中删除表项,然后调用Recycler.recycleView()
回收表项:
public final class Recycler {
/**
* Recycle a detached view. The specified view will be added to a pool of views
* for later rebinding and reuse.
*
* <p>A view must be fully detached (removed from parent) before it may be recycled. If the
* View is scrapped, it will be removed from scrap list.</p>
*
* @param view Removed view for recycling
* @see LayoutManager#removeAndRecycleView(View, Recycler)
*/
public void recycleView(View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
}
}
通过表项视图拿到了对应ViewHolder
,然后把其传入Recycler.recycleViewHolderInternal()
,现在就可以更准地回答上一篇的那个问题“回收些啥?”:回收的是滑出屏幕表项对应的ViewHolder
。
public final class Recycler {
...
int mViewCacheMax = DEFAULT_CACHE_SIZE;
static final int DEFAULT_CACHE_SIZE = 2;
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
...
/**
* 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) {
...
if (forceRecycle || holder.isRecyclable()) {
//先存在mCachedViews里面
//这里的判断条件决定了复用mViewCacheMax中的ViewHolder时不需要重新绑定数据
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
//如果mCachedViews大小超限了,则删掉最老的被缓存的ViewHolder
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;
}
//ViewHolder加到缓存中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
//若ViewHolder没有入缓存则存入回收池
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
} else {
...
}
...
}
- 通过
cached
这个布尔值,实现互斥,即ViewHolder
要么存入mCachedViews
,要么存入pool
-
mCachedViews
有大小限制,默认只能存2个ViewHolder
,当第三个ViewHolder
存入时会把第一个移除掉,代码如下:
public final class Recycler {
...
void recycleCachedViewAt(int cachedViewIndex) {
if (DEBUG) {
Log.d(TAG, "Recycling cached view at index " + cachedViewIndex);
}
ViewHolder viewHolder = mCachedViews.get(cachedViewIndex);
if (DEBUG) {
Log.d(TAG, "CachedViewHolder to be recycled: " + viewHolder);
}
//将ViewHolder加入到回收池
addViewHolderToRecycledViewPool(viewHolder, true);
//将ViewHolder从cache中移除
mCachedViews.remove(cachedViewIndex);
}
...
}
从mCachedViews
移除掉的ViewHolder
会加入到回收池中。 mCachedViews
有点像“回收池预备队列”,即总是先回收到mCachedViews
,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder
存入回收池 :
public final class Recycler {
/**
* Prepares the ViewHolder to be removed/recycled, and inserts it into the RecycledViewPool.
* 将viewHolder存入回收池
*
* Pass false to dispatchRecycled for views that have not been bound.
*
* @param holder Holder to be added to the pool.
* @param dispatchRecycled True to dispatch View recycled callbacks.
*/
void addViewHolderToRecycledViewPool(ViewHolder holder, boolean dispatchRecycled) {
clearNestedRecyclerViewIfNotNested(holder);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE)) {
holder.setFlags(0, ViewHolder.FLAG_SET_A11Y_ITEM_DELEGATE);
ViewCompat.setAccessibilityDelegate(holder.itemView, null);
}
if (dispatchRecycled) {
dispatchViewRecycled(holder);
}
holder.mOwnerRecyclerView = null;
getRecycledViewPool().putRecycledView(holder);
}
}
public static class RecycledViewPool {
static class ScrapData {
ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
//每种类型的ViewHolder最多存5个
int mMaxScrap = DEFAULT_MAX_SCRAP;
long mCreateRunningAverageNs = 0;
long mBindRunningAverageNs = 0;
}
//以viewType为键,ScrapData为值,作为回收池中ViewHolder的容器
SparseArray<ScrapData> mScrap = new SparseArray<>();
//ViewHolder入池 按viewType分类入池,相同的ViewType存放在List中
public void putRecycledView(ViewHolder scrap) {
final int viewType = scrap.getItemViewType();
final ArrayList<ViewHolder> 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");
}
//入回收池之前重置ViewHolder
scrap.resetInternal();
scrapHeap.add(scrap);
}
}
ViewHolder
会按viewType
分类存入回收池,最终存储在ScrapData
的ArrayList
中,回收池数据结构分析详见RecyclerView缓存机制(咋复用?)。
缓存优先级
还记得RecyclerView缓存机制(咋复用?)中得出的结论吗?这里再引用一下:
- 虽然为了获取
ViewHolder
做了5次尝试(共从6个地方获取),先排除3种特殊情况,即从mChangedScrap
获取、通过id获取、从自定义缓存获取,正常流程中只剩下3种获取方式,优先级从高到低依次是:
- 从
mAttachedScrap
获取- 从
mCachedViews
获取- 从
mRecyclerPool
获取
- 这样的缓存优先级是不是意味着,对应的复用性能也是从高到低?(复用性能越好意味着所做的昂贵操作越少)
- 最坏情况:重新创建
ViewHodler
并重新绑定数据- 次好情况:复用
ViewHolder
但重新绑定数据- 最好情况:复用
ViewHolder
且不重新绑定数据
当时分析了mAttachedScrap
和mRecyclerPool
的复用性能,即 从mRecyclerPool
中复用的ViewHolder
需要重新绑定数据,从mAttachedScrap
中复用的ViewHolder
不要重新出创建也不需要重新绑定数据
把存入mCachedViews
的代码和复用时绑定数据的代码结合起来看一下:
void recycleViewHolderInternal(ViewHolder holder) {
...
//满足这个条件才能存入mCachedViews
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
}
...
}
ViewHolder tryGetViewHolderForPositionByDeadline(int position,boolean dryRun, long deadlineNs) {
...
//满足这个条件就需要重新绑定数据
if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()){
}
...
重新绑定数据的三个条件中,holder.needsUpdate()
和holder.isInvalid()
都是false
时才能存入mCachedViews
,而!holder.isBound()
对于mCachedViews
中的ViewHolder
来说必然为false
,因为只有当调用ViewHolder.resetInternal()
重置ViewHolder
后,才会将其设置为未绑定状态,而只有存入回收池时才会重置ViewHolder
。所以 从mCachedViews
中复用的ViewHolder
不需要重新绑定数据
总结
- 滑出屏幕表项对应的
ViewHolder
会被回收到mCachedViews
+mRecyclerPool
结构中,mCachedViews
是ArrayList
,默认存储最多2个ViewHolder
,当它放不下的时候,会通过将旧的ViewHolder
挪到mRecyclerPool
的方式来腾出空间。mRecyclerPool
是SparseArray
,它会按viewType
分类存储ViewHolder
,默认每种类型最多存5个。 - 从
mRecyclerPool
中复用的ViewHolder
需要重新绑定数据 - 从
mCachedViews
中复用的ViewHolder
不需要重新绑定数据