RecyclerView源码解析

复用和回收

复用的好处:
避免为表项视图绑定数据,创建表项视图。

子item的绘制交给LayoutManager去处理。

fill

LinearLayoutManager#fill
作用:回收和复用。

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    ...
    // 当前的方向上是否还有多余的空间填充item
    int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
    // 当剩余空间> 0时,继续填充更多表项
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // 通过View循环,来对条目进行一条条复用,填充剩余空间
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            // 从剩余空间中扣除新表项占用像素值
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                // 在limit上追加新表项所占像素值
                // 回收哪些项目是根据limit线走的,手指向上滑,底部填充元素,limit线会下移,在这根线上面的条目会被回收。
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            // 回收
            recycleByLayoutState(recycler, layoutState);
        }
    }
    return start - layoutState.mAvailable;
}

回收

LinearLayoutManager#recycleByLayoutState

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
         // 从列表头回收
        recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
    } else {
        // 从列表尾回收
        recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
    }
}

LinearLayoutManager#recycleViewsFromStart

private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
    //从头开始遍历 LinearLayoutManager,以找出应该会回收的表项
    final int childCount = getChildCount();
    // 是否反转布局,就是布局上从上往下填充还是从下往上填充
    if (mShouldReverseLayout) {
        for (int i = childCount - 1; i >= 0; i--) {
            View child = getChildAt(i);
            // 当某表项底部位于limit隐形线之后时,回收它以上的所有表项
            // limit是列表中隐形的线
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                //回收索引为末尾到i-1的表项
                recycleChildren(recycler, childCount - 1, i);
                return;
            }
        }
    } else {
         //回收索引为0到i-1的表项
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}

“从列表头回收表项”所对应的场景是:手指上滑,列表向上滚动,新的表项逐个插入到列表尾部,列表头部的表项逐个被回收。

复用

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 1. 通过缓存池中获取下个条目
    View view = layoutState.next(recycler);
   
    // 2. 将列表中的一项添加进RecyclerView
    RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();
    // 3. 测量该视图
    measureChildWithMargins(view, 0, 0);
    // 4. 获取填充视图需要消耗的像素值
    result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
    // 5. 布局表项
    // 确定表项上下左右四个点相对于RecyclerView的位置
    layoutDecoratedWithMargins(view, left, top, right, bottom);
}

LinearLayoutManager#next

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

RecyclerView#tryGetViewHolderForPositionByDeadline

复用机制代码

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there
    // 复用的对象是ViewHolder
    // 在布局之前
    if (mState.isPreLayout()) {
        // 1. 通过id或者position从mChangedScrap缓存找到对应的缓存
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    if (holder == null) {
        // 2. 通过position从mAttachedScrap或二级回收缓存中获取ViewHolder
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                if (!dryRun) {
                    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;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);

        final int type = mAdapter.getItemViewType(offsetPosition);
        // 3. 通过id从mAttachedScrap或二级回收缓存中获取ViewHolder
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        if (holder == null && mViewCacheExtension != null) {
            // 4. 从自定义缓存中获取ViewHolder
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
            }
        }
        if (holder == null) { // fallback to pool
            // 5. 从缓存池中取ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        if (holder == null) {
            long start = getNanoTime();
            // 6.所有缓存都没命中,就需要创建ViewHolder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
            }
        }
    }

    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        //获得ViewHolder后,绑定视图数据
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }
    ...
    return holder;
}

总结:
RecyclerView在滚动发生之前,会根据预计滚动位移大小来决定需要向列表中填充多少新的表项。在填充表项的同时,也会回收表项,回收的依据是limit隐形线。
limit隐形线是RecyclerView在滚动发生之前根据滚动位移计算出来的一条线,它是决定哪些表项该被回收的重要依据。它可以理解为:隐形线当前所在位置,在滚动完成后会和列表顶部重叠。
limit隐形线的初始值=列表当前可见表项的底部到列表底部的距离,即列表在不填充新表项时,可以滑动的最大距离。每一个新填充表项消耗的像素值都会被追加到limit值之上,即limit隐形线会随着新表项的填充而不断地下移。
触发回收逻辑时,会遍历当前所有表项,若某表项的底部位于limit隐形线下方,则该表项上方的所有表项都会被回收。

四级缓存

// detach调用
// 复用的时候不需要调用bindViewHolder重新绑定数据,状态和数据不会被重置的
// 保存原封不动的ViewHolder
// 生命周期两次布局
// 位置一致才能复用
final ArrayList mAttachedScrap = new ArrayList<>();
// 发生变化的ViewHolder
// 生命周期只有预布局
ArrayList mChangedScrap = null;

// remove调用
// 可通过setItemCacheSize调整,默认大小为2
// 上下滑动,被滑出去的ViewHolder缓存
// 如果超过限制,会把最老的item移除到RecycledViewPool中。

// mCachedViews中缓存的ViewHolder只能复用于指定位置,不需要调用bindViewHolder重新绑定数据
// 应用场景列表回滚
final ArrayList mCachedViews = new ArrayList();

// 自定义拓展View缓存
private ViewCacheExtension mViewCacheExtension;

// RecycledViewPool中的ViewHolder存储在SparseArray中,并且按viewType分类存储
// 同一类型的ViewHolder存放在ArrayList 中,且默认最多存储5个。
// mCachedViews缓存放不下的时候,才会把缓存放进mRecyclerPool,里面的缓存都是需要重新绑定数据的。
// 从mRecyclerPool中取出的ViewHolder只能复用于相同viewType的表项。
RecycledViewPool mRecyclerPool;

最差情况:重新创建ViewHolder绑定数据
次好情况:复用ViewHolder需要重新绑定数据
最好情况:复用ViewHolder不需要重新绑定数据

谈谈mChangedScrap

生命周期只有预布局的时候。
mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到mChangedScrap中。
mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。

浅谈几种更新RecyclerView的区别

notifyItemInserted


需要重新布局,A、B、C都可以从mAttachedScrap缓存拿出来直接使用,不需要绑定,a需要创建对应的ViewHolder重新绑定,添加进一级缓存。

注意如果是A,B,C,D移除B,B还是在mAttachedScrap缓存,只不过FLAG是REMOVE。

notifyDataSetChanged


代表数据全面发生变化,屏幕上的内容标为无效,屏幕上的元素全部缓存到四级缓存RecycledViewPool,屏幕上的元素都需要重新绑定。

notifyItemChanged


A被添加了FLAG_UPDATE,在scrapView(View view)中不满足!holder.isUpdated()所以会被放入到mChangedScrap,然后在缓存复用时B、C、D都可以直接使用,A因为被修改了所以需要重新绑定一下。
也就是说:notifyItemChanged将屏幕上的元素保存到一级缓存中,有更改的保存到mChangedScrap中并且需要重新绑定,没有变化的保存到mAttachedScrap中。

重点类

Recycler:管理复用
LayoutManager:管理布局

detach和remove

一个View只是暂时被清除掉,稍后立刻就要用到,使用detach。它会被缓存进scrapCache的区域。
一个View不再显示在屏幕上,需要被清除掉,并且下次再显示它的时机目前未知,使用remove。它会被以viewType分组,缓存进RecyclerViewPool里。

scrap view的生命周期

在将表项一个个填充到列表之前会先将其先回收到mAttachedScrap中,回收数据的来源是LayoutManager的孩子,而LayoutManager的孩子都是屏幕上可见的或即将可见的表项。
RecyclerView布局的最后一步,清除scrap view。
mAttachedScrap生命周期起始于RecyclerView布局开始,终止于RecyclerView布局结束。

RecyclerView的动画

列表中有两个表项(1、2),删除2,此时3会从屏幕底部平滑地移入并占据原来2的位置。
为了实现该效果,RecyclerView的策略是:为动画前的表项先执行一次pre-layout,将不可见的表项3也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项3的位置,就知道表项3该如何做动画了,表项2做消失动画,当动画结束后,item2的ViewHolder会被回收。

RecyclerView为了实现表项动画,进行了2次布局(预布局+后布局),在源码上表现为LayoutManager.onLayoutChildren()被调用2次。
预布局的过程始于RecyclerView.dispatchLayoutStep1(),终于RecyclerView.dispatchLayoutStep2()。

在每次向RecyclerView填充表项之前都会先清空LayoutManager中现存表项,将它们detach并同时缓存入mAttachedScrap列表中。在紧接着的填充表项阶段,就立马从mAttachedScrap中取出刚被 detach的表项并重新attach它们。

pre-layout
LinearLayoutManager#onLayoutChildren

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ...
    // 在填充表项之前会遍历所有子表项,并逐个回收
    detachAndScrapAttachedViews(recycler);
    ...
    // 填充表项
    fill()
}

post-layout
因为LayoutManager中现有表项1、2、3,所以scrap完成后,mAttachedScrap中存有表项1、2、3的ViewHolder实例(position依次为0、0、1,被移除表项的position会被置0)。分别填充position位置为0和1的表项。为2的位置缓存就不会命中。
缓存命中规则:position相同,并且表项没被移除。

为什么这么设计:
为了确定动画的种类和起终点,需要比对动画前和动画后的两张“表项快照”,不然只知道最终位置不知道起始位置。
为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),
为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),
但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),
RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在scrap结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。

整体总结

  • Recycler有4个层次用于缓存ViewHolder对象,优先级从高到底依次为ArrayList mAttachedScrapArrayList mCachedViewsViewCacheExtension mViewCacheExtensionRecycledViewPool mRecyclerPool。如果四层缓存都未命中,则重新创建并绑定ViewHolder对象。
  • 缓存性能:
    都不需要重新创建ViewHolder,只有RecycledViewPool,mChangedScrap需要重新绑定数据。
  • 缓存容量:
    mAttachedScrap:没有大小限制,但最多包含屏幕可见表项。
    mCachedViews:默认大小限制为2,放不下时,按照先进先出原则将最先进入的ViewHolder存入回收池以腾出空间。
    mRecyclerPool:对ViewHolder按viewType分类存储(通过SparseArray),同类ViewHolder存储在默认大小为5的ArrayList中。
  • 缓存用途:
    mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用。
    mCachedViews:用于移出屏幕表项的回收和复用,且只能用于指定位置的表项,有点像“回收池预备队列”,即总是先回收到mCachedViews,当它放不下的时候,按照先进先出原则将最先进入的ViewHolder存入回收池。
    mRecyclerPool:用于移出屏幕表项的回收和复用,且只能用于指定viewType的表项
  • 缓存结构:
    mAttachedScrap:ArrayList
    mCachedViews:ArrayList
    mRecyclerPool:对ViewHolder按viewType分类存储在SparseArray中,同类ViewHolder存储在ScrapData中的ArrayList中

参考
RecyclerView 源码分析2-缓存机制图解
RecyclerView 面试题 | 哪些情况下表项会被回收到缓存池?

你可能感兴趣的:(RecyclerView源码解析)