RecyclerView 回收复用逻辑整理

1,468 阅读4分钟

参考:

https://juejin.cn/post/6844904146684870669

https://blog.csdn.net/c10WTiybQ1Ye3/article/details/107193802

RecyclerView 版本

androidx.recyclerview:recyclerview:1.1.0-alpha05

从RecyclerView的onLayout入手

RecyclerView.onLayout(...)
->RecyclerView.dispatchLayout()
// dispatchLayoutStep1方法等同于pre layout;
//源码注释:这里决定哪个动画运行,保存当前View信息等;
///dispatchLayoutStep2方法处理真正布局的地方
->RecyclerView.dispatchLayoutStep1() 
->RecyclerView.dispatchLayoutStep2()
//mLayout是LayoutManager的实例
->mLayout.onLayoutChildren(mRecycler, mState);
//此处查看LinearLayoutManager.fill() 注释:填充给定Layout
->LinearLayoutManager.fill(recycler, mLayoutState, state, false);
//循环调用,每次返回一个
->LinearLayoutManager.layoutChunk(recycler, layoutState) 
->LinearLayoutManager.LayoutState.next()
//通过 Recycler 获取指定位置的 ItemView   
->RecyclerView.recycler.getViewForPosition(int position)
//获取ViewHolder 返回ViewHolder中的ItemView
->RecyclerView.tryGetViewHolderForPositionByDeadline(***)

获取ViewHolder流程

代码展示

1 如果是预布局,尝试从mChangedScrap 中获取ViewHolder

// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    ...
}

2 尝试从mAttachedScrap/mHiddenViews/mCachedViews 中获取ViewHolder

// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    ...
}

3 如果存在StableId 尝试使用ID从mAttachedScrap中获取ViewHolder

// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
            type, dryRun);
    ...
}

3.1 如果用户有自定义缓存,尝试从mViewCacheExtension中获取ViewHolder,一般不会自定义

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) {
        holder = getChildViewHolder(view);
        ...
    }
}

4 尝试从mRecyclerPool中获取ViewHolder

if (holder == null) { // fallback to pool
    ...
    holder = getRecycledViewPool().getRecycledView(type);
    ...
}

5 如果以上方法均未获取到则创建一个ViewHolder

if (holder == null) {
    ...
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    ...
}

关于Pre-layout(预布局)

当adapter调用notifyItemChanged()或者notifyItemRangeChanged()的时候,onLayoutChildren()会调用两次,一次是预布局,一次是实际布局。通过对比两次布局的不同,RecyclerView可以完成预测动画。

好比当前有A,B,C三个Item,当前A,B Item显示在屏幕上,这时候我们做一个将B移除C展示的操作。这时候为了更好地用户体验需要增加一个C平缓进入B原先位置的动画。如果只布局一次的话,我们只知道C的最终位置(也就是B原先的位置),但是并不知道C的起始位置(也就是不知道该从哪个坐标开始启动动画,因为滑动方向不确定,每个LayoutManager滑动方式也不确定)、而如果有预布局的话,可以在预布局的时候将ABC三个Item都布局出来,然后通过与实际布局对比,就能知道动画开始的坐标从而开启动画

关于StableID

stableID 作用在于调用notifyDataSetChanged方法后,LayoutManager重新布局的的时候将ViewHolder回收到何处

没有设置StableId,viewHolder被回收到RecyclerViewPool 如果设置了StableId,viewHolder被回收到Scrap中

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    ...
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        removeViewAt(index);
        //回收到RecyclerViewPool
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        detachViewAt(index);
        //回收到Scrap中
        recycler.scrapView(view);
        ...
    }
}

关于Scrap

mChangedScrap 和 mAttachedScrap 是RecyclerView最先查找ViewHolder的地方, 只在布局阶段使用,布局完成后这两个地方的ViewHolder会移到mCachedViews 或者mRecyclerPool中。

当LayoutManager开始布局的时候(预布局或者真正布局),当前布局中所有ViewHolder都会被回收到Scrap中,然后LayoutManager挨个的取回ViewHolder,除非View发生了变化,否则它会立马从Scrap中回到原来位置。

这样做是为了能让RecyclerView和LayoutManager更好地分离,LayoutManager不需要去关心那个View需要应该保留哪个View需要移到RecyclerPool中,这些都是RecyclerView的职责,他只需要去Scrap中取ViewHolder就好了。

//LinearLayoutManager.onLayoutChildren()
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler,
 RecyclerView.State state) {
    ...
    //回收到Scrap中
    detachAndScrapAttachedViews(recycler);
    ...
}

mChangedScrap 和 mAttachedScrap区别

1、添加时机不同,只有在Item发生了变化(notifyItemChanged或者notifyItemRangeChanged被调用),并且ItemAnimator调用canReuseUpdatedViewHolder()返回false时才会添加到mAttachedScrap,否则添加到mChangedScrap中

CanReuseUpdateViewHolder返回‘false’表示要使用不同的ViewHolder来完成动画,true表示使用相同的ViewHolder完成动画例如淡入淡出

2、mChangedScrap 只在预布局的时候会使用到,mAttachedScrap在整个布局中都可以使用

if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED 
| ViewHolder.FLAG_INVALID)|| !holder.isUpdated() || 
canReuseUpdatedViewHolder(holder)) {
    ...
    holder.setScrapContainer(this, false);
    //被同时标记为Removed和Invalid,或者没有更新的,或者ItemAnimator为null,
    //或者ItemAnimator.canReuseUpdatedViewHolder返回true时
    //添加到mChangedScrap 中
    mAttachedScrap.add(holder);
} else {
    if (mChangedScrap == null) {
        mChangedScrap = new ArrayList();
    }
    holder.setScrapContainer(this, true);
     //添加到mChangedScrap 中
    mChangedScrap.add(holder);
}

关于mHiddenViews

找到mHiddenViews添加的地方,发现在addAnimatingView(ViewHolder viewHolder) 方法中为调用逻辑处。

注释明确的说明将视图添加到animatingViews列表中纯粹是出于动画目的,他们与常规的视图区分管理,并且对LayoutManager是不可见的

/**
 * Adds a view to the animatingViews list.
 * mAnimatingViews holds the child views that are currently being kept around
 * purely for the purpose of being animated out of view. They are drawn as a regular
 * part of the child list of the RecyclerView, but they are invisible to the LayoutManager
 * as they are managed separately from the regular child views.
 * @param viewHolder The ViewHolder to be removed
 */
private void addAnimatingView(ViewHolder viewHolder) {
    final View view = viewHolder.itemView;
    final boolean alreadyParented = view.getParent() == this;
    mRecycler.unscrapView(getChildViewHolder(view));
    if (viewHolder.isTmpDetached()) {
        // re-attach
        mChildHelper.attachViewToParent(view, -1, view.getLayoutParams(), true);
    } else if (!alreadyParented) {
        mChildHelper.addView(view, true);
    } else {
        mChildHelper.hide(view);
    }
}

性能优化方向