RecyclerView源码剖析: 滑动,子View回收再利用

3,104 阅读14分钟

本文紧接着上一篇文章来分析RecyclerView的滚动,从中我们可以窥探到RecyclerView缓存的回收与再利用。

分析源码,不能太盲目,否则目标过大容易迷失自己,因此我还是选取上篇文章的例子,作为分析的目标

mRecyclerView.setLayoutManager(new LinearLayoutManager(this));
mRecyclerView.setAdapter(new RvAdapter());

为了分析RecyclerView滚动,现在假设手指从下往上滑动,后面将以此为根基进行分析。

  1. 与前面文章约定的习惯一致,我将使用RV表示RecyclerView,用LM表示LayoutManager,用LLM表示LinearLayoutManager。
  2. 阅读本文需要提前了解RecyclerView源码剖析: 基本显示

事件处理

根据事件分发的原理可知,RV的滚动由onTouchEvent()完成,精简代码如下

public boolean onTouchEvent(MotionEvent e) {
    // 通过LayoutManager判断是否可以水平或者垂直滚动
    final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
    final boolean canScrollVertically = mLayout.canScrollVertically();

    switch (action) {
        case MotionEvent.ACTION_MOVE: {
            int dx = mLastTouchX - x;
            // 1. 获取手指移动的距离
            // dy大于0,代表手指向上滑动
            int dy = mLastTouchY - y;
                
            // ... 省略判断是否能执行滚动的代码
                
            if (mScrollState == SCROLL_STATE_DRAGGING) {
                // ...省略nested scroll的代码
                    
                // 2. 执行滚动
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {
                    // 成功完成一次滚动,就请求父View不再截断后续事件
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
            }
        } break;
    }
    return true;
}

例子中使用的LLM支持的是垂直滚动,并且手指从下往上滑动,因此dx值为0,dy值大于0(注意,不是小于0)。

计算完滑动距离后,调用scrollByInternal()来完成滚动。

这里省略了事件处理的代码,大家可以自行分析。

滚动实现

boolean scrollByInternal(int x, int y, MotionEvent ev) {
    int consumedX = 0;
    int consumedY = 0;

    if (mAdapter != null) {
        mReusableIntPair[0] = 0;
        mReusableIntPair[1] = 0;
        // 执行滚动,并把滚动消耗的距离放到第二个参数中。
        scrollStep(x, y, mReusableIntPair);
        // 计算滚动消耗的距离
        consumedX = mReusableIntPair[0];
        consumedY = mReusableIntPair[1];
        // ...
    }
        
    // 如果有ItemDecoration立即刷新
    if (!mItemDecorations.isEmpty()) {
        invalidate();
    }

    // ...省略nested scroll和over scroll的代码
        
    // 通知监听RV滑动的监听者
    if (consumedX != 0 || consumedY != 0) {
        dispatchOnScrolled(consumedX, consumedY);
    }
        
    // 如果没有ItemDecoration,也需要刷新
    if (!awakenScrollBars()) {
        invalidate();
    }
        
    // 只要有滑动,就返回true
    return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

这里的逻辑很清楚,执行滚动,通知监听器,刷新界面。因此我们把目光集中在scrollStep()即可

void scrollStep(int dx, int dy, @Nullable int[] consumed) {
    // ...

    int consumedX = 0;
    int consumedY = 0;
    // 滚动交给LayoutManager执行
    if (dx != 0) {
        consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
    }
    if (dy != 0) {
        consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
    }

    // ...
        
    // 保存滚动的距离到第二个参数中
    if (consumed != null) {
        consumed[0] = consumedX;
        consumed[1] = consumedY;
    }
}

原来RV把滚动的逻辑交给了LM,例子中使用的是LLM,而且支持的是垂直滚动,因此我们来分析LLM的scrollVerticallyBy()方法。

LLM的垂直滚动实现

public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,
        RecyclerView.State state) {
    if (mOrientation == HORIZONTAL) {
        return 0;
    }
    return scrollBy(dy, recycler, state);
}

LLM是通过scrollBy()实现滚动的,由于代码逻辑跨度比较大,因此我将分布讲解。

更新布局信息

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    if (getChildCount() == 0 || delta == 0) {
        return 0;
    }
    ensureLayoutState();
    // 表示子View可被回收
    mLayoutState.mRecycle = true;
    // 手指从下往上滑动,delta大于0,取值LayoutState.LAYOUT_END
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
        
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // ...
    return scrolled;
}

根据前面文章可知,updateLayoutState()为子View布局更新mLayouState信息

private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
    // 大部分情况都为false
    mLayoutState.mInfinite = resolveIsInfinite();
    // 保存布局的方向
    mLayoutState.mLayoutDirection = layoutDirection;
        
    mReusableIntPair[0] = 0;
    mReusableIntPair[1] = 0;
    // 计算额外的布局空间,如果不是smooth scroll,一般为0
    calculateExtraLayoutSpace(state, mReusableIntPair);
    int extraForStart = Math.max(0, mReusableIntPair[0]);
    int extraForEnd = Math.max(0, mReusableIntPair[1]);
        
    // 根据例子分析,值为true
    boolean layoutToEnd = layoutDirection == LayoutState.LAYOUT_END;
    // 代表布局可用的额外空间
    mLayoutState.mExtraFillSpace = layoutToEnd ? extraForEnd : extraForStart;
    // 代表不需要回收的空间
    mLayoutState.mNoRecycleSpace = layoutToEnd ? extraForStart : extraForEnd;
        
    int scrollingOffset;
    if (layoutToEnd) {
        // 增加RV底部的padding
        mLayoutState.mExtraFillSpace += mOrientationHelper.getEndPadding();
        // 获取最底部的一个子View
        final View child = getChildClosestToEnd();
        // Adapter数据遍历方向,这里取ITEM_DIRECTION_TAIL,表示从前往后遍历
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
        // 计算获取的下一个View,在Adapter中的position
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        // 计算布局的起始坐标
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);

        // 计算在不同添加子View的情况下,RV可以滚动的距离
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    } else {

    }
        
    // requiredSpace值为dy
    // mLayoutState.mAvailable代表布局的可用空间
    mLayoutState.mAvailable = requiredSpace;
        
    // 此时分析的情况canUseExistingSpace为true
    if (canUseExistingSpace) {
        // 个人认为,这段代码放在这里是无意义的
        mLayoutState.mAvailable -= scrollingOffset;
    }
        
        
    mLayoutState.mScrollingOffset = scrollingOffset;
}

这里有很多变量是在前面文章中介绍过的,但是重点要关注scrollingOffset变量,用一副图解释下

scrollingOffset

由图可知,滑动距离还没达到scrollingOffset时,RV是不需要填充子View的。

子View的回收与填充

更新完信息后,接下来就要决定此次的滑动是否需要回收不可见子View,以及是否需要填充新View。

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // 2. 根据情况回收,创建子View,
    int added = fill(recycler, mLayoutState, state, false);
        
    // ...
    return scrolled;
}

fill()方法的命名不好,它不仅仅完成填充子View的任务,还完成了子View的回收任务

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    final int start = layoutState.mAvailable;
        
    // 1. 在添加子View前,回收那些预计不可见子View
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // 个人觉得,将刚才更新mLayout时,计算layoutState.mAvailable的代码移动到这里比较恰当
        // TODO ugly bug fix. should not happen
        if (layoutState.mAvailable < 0) {
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
            recycleByLayoutState(recycler, layoutState);
    }
        
    // 可用空间还是要包括计算出来的额外空间
    int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
    // 用来保存布局的结果
    LayoutChunkResult layoutChunkResult = mLayoutChunkResult;

    // 2. 如果有可用空间,并且还有子View可以填充,那么就填充子View,并计算是否会输不可见子View    
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();

        // 2.1 添加子View
        layoutChunk(recycler, state, layoutState, layoutChunkResult);

        // 这里处理的是所有子View添加完毕的情况
        if (layoutChunkResult.mFinished) {
            break;
        }

        // 计算下次布局的坐标偏移量
        layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
            
        // 添加子View后,再次计算可用空间
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
            
        // 2.2 添加子View后,在执行滚动前,回收那些预计不可见的子View
        if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            if (layoutState.mAvailable < 0) {
                layoutState.mScrollingOffset += layoutState.mAvailable;
            }
            recycleByLayoutState(recycler, layoutState);
        }

    }
    // 表示添加子View使用了多少空间,如果没有添加子View,值就是0
    return start - layoutState.mAvailable;
}

这段代码把View的回收以及创建,混合在一起。好在前面的文章已经分析了填充子View的过程,那么接下来我挑一种情形来分析子View的回收过程。这个情形如下图

recyle

如图所示,dy代表手指向上滑动的距离差,很明显dy是小于scrollingOffset(不添加子View可以滚动的距离),但是却大于第一个子View的底部距离。

在这种情况下,我们完全可以预计,即将到来的滚动,肯定会让第一个子View不可见,因此我们可以提前回收这个子View。

回收子View

我把需要的代码结合在一起来分析提前回收子View的过程,代码如下

private void updateLayoutState(int layoutDirection, int requiredSpace,
        boolean canUseExistingSpace, RecyclerView.State state) {
    // requiredSpace的值就是dy
    mLayoutState.mAvailable = requiredSpace;
    // canUseExistingSpace为true
    if (canUseExistingSpace) {
        // dy小于scrollingOffset时,mLayoutState.mAvailable为负值
        mLayoutState.mAvailable -= scrollingOffset;
    }
}

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // 在添加子View前,回收那些预计不可见子View
    if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
        // 处理负值的情况
        if (layoutState.mAvailable < 0) {
            // layoutState.mScrollingOffset重新计算后值为dy
            layoutState.mScrollingOffset += layoutState.mAvailable;
        }
        recycleByLayoutState(recycler, layoutState);
    }
        
    // ...
}

根据前面图片展示的情况,dy是小于mScrollingOffset的,代码中最终计算计算出的layoutState.mScrollingOffset的值就为dy,后面的代码将会比较顶部的子View在滚动dy的情况下,是否不可见,看下recycleByLayoutState()如何实现的

private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
    // 此时值为dy
    int scrollingOffset = layoutState.mScrollingOffset;
    // 不是smooth scroll,值为0
    int noRecycleSpace = layoutState.mNoRecycleSpace;
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {

    } else {
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}
    
private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,
        int noRecycleSpace) {
    // 此时值为dy
    final int limit = scrollingOffset - noRecycleSpace;
        
    final int childCount = getChildCount();
    if (mShouldReverseLayout) {

    } else {
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 遍历获取第一个底部坐标大于dy的子View
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                // 如果找到这个子View,就删除前面的所有子View,因为它们都不可见
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}    

根据前面的情况来分析这段代码,可以计算出limit的值就是dy,因此通过遍历获取的第一个底部坐标大于limit值的子View,就是前面图片上的第二个子View,因此调用recycleChildren(recycler, 0, i),回收第二个子View前面的所有子View,也就是回收第一个子View,因为它马上不可见。

recycleChildren()通过removeAndRecycleViewAt()方法逐个回收子View

public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
    // 获取子View
    final View view = getChildAt(index);
    // 1. 从RV中移除子View
    removeViewAt(index);
    // 2. 使用Recycler回收子View
    recycler.recycleView(view);
}

回收分为两步,首先从RV中移除子View,这是一个ViewGroup移除子View的操作。然后利用RecyclerView.Recycler进行回收子View。

Reycler回收

现在我们把目光聚焦到Recycler是如何回收子View的!!!

此时此刻真是令人激动的时候,因为我们可以开始窥探Recycler的缓存。

public void recycleView(@NonNull View view) {
    // 通过布局参数获取ViewHolder
    ViewHolder holder = getChildViewHolderInt(view);
            
    // ...
            
    // 回收
    recycleViewHolderInternal(holder);
}

首先获取了View的ViewHolder,然后调用recycleViewHolderInternal()来回收ViewHolder

回收的是ViewHolder而不是View,有意思!

void recycleViewHolderInternal(ViewHolder holder) {
    // ... 省略状态判断的代码
            
    // ViewHolder没有设置FLAG_NOT_RECYCLABLE,并且View处于transient state时,这个变量值为true
    final boolean transientStatePreventsRecycling = holder
            .doesTransientStatePreventRecycling();
    // Adapter#onFailedToRecycleView()做清理工作后,代表可强制刷新
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder);
                    
    boolean cached = false;
    boolean recycled = false;

    if (forceRecycle || holder.isRecyclable()) {
        // 1. 缓存最大空间大于0并且ViewHolder状态正常,就把它添加到缓存中
        // mViewCacheMax代表缓存空间大小,默认为2
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    
            int cachedViewSize = mCachedViews.size();
            // 如果缓存空间已满,就清理空间
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                // 移除第一个缓存的ViewHolder,并使用Recycler进行回收
                recycleCachedViewAt(0);
                cachedViewSize--;
            }

            int targetCacheIndex = cachedViewSize;
                    
            // ... 省略预获取信息的操作
                    
            // 缓存空间足够,就缓存ViewHolder
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
                
        // 2. 无法使用缓存,就使用Recycler回收ViewHolder
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {

    }
    // ...
}

我们来分析下这里使用的缓存回收策略

  1. ArrayList mCachedViews最大空间大于0,并且ViewHolder状态正常(没设置过FLAG_INVALID, FLAG_REMOVED, FLAG_UPDATE, FLAG_ADAPTER_POSITION_UNKNOWN)。
    • 如果达到缓存最大空间,就把index为0的项,交给Recycler回收。
    • 如果缓存空间足够,就直接添加到缓存中。
  2. 如果缓存最大空间不够,或者ViewHolder状态不正常,那么就用RecyclerPool进行回收。

这里用到两个缓存,有什么区别呢,后面揭晓。

现在来看下addViewHolderToRecycledViewPool()如何回收ViewHolder

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
            clearNestedRecyclerViewIfNotNested(holder);
    // ...省略Accessbility
    
    // 通知监听者ViewHolder被回收        
    if (dispatchRecycled) {
        dispatchViewRecycled(holder);
    }
    
    // 被回收前,设置ViewHolder的mOwnerRecyclerView为null
    holder.mOwnerRecyclerView = null;
    
    // 使用RecylerPool回收ViewHolder
    getRecycledViewPool().putRecycledView(holder);
}

原来使用RecyclerPool进行回收的

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    // 根据类型,获取回收池中的集合
    final ArrayList<ViewHolder> scrapHeap = getScrapDataForType(viewType).mScrapHeap;
    // 如果集合已满,就不进行此次回收(mMaxScrap默认为5)
    if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
        return;
    }
    scrap.resetInternal();
    // 添加需要回收的ViewHolder
    scrapHeap.add(scrap);
}

回收池是为了节省内存而生,但是它也不能盲目的无限回收,RecyclerPool对每种类型的ViewHolder回收上限是5。如果达到上限,那么提交给回收池的ViewHolder就会被忽略掉。

如果没有达到回收池的上限,首先根据需要被回收的ViewHolder的类型获取一个集合,然后把需要回收的ViewHolder放到这个集合中。

Recycler再利用

现在我们已经了解了Recycler回收子View的方式,那么现在我们来看看如何再利用回收的ViewHolder。

根据前面文章的分析可知,获取子View是在layoutChunk()中发生的

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    // 1. 获取子View
    View view = layoutState.next(recycler);
    // 2. 添加子View
    // 3. 测量子View
    // 4. 布局子View

    // ...
}

获取子View是从Recycler中获取的

ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // ...
            
    ViewHolder holder = null;
 
    // 1. 从mAttachedScrap,hidden(正在消失的View), mCachedViews中获取
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            // 对于不是pre-layout过程,这里主要就是检查ViewHolder类型是否匹配
            if (!validateViewHolderForOffsetPosition(holder)) {
                        
            } else {
                romScrapOrHiddenOrCache = true;
            }
        }
    }
            
            
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2. 如果Adapter支持stable ids,就根据stable id从mAttachedScrap, mCachedViews中获取
        if (mAdapter.hasStableIds()) {

        }
                
        // 3. 从自定义的缓存mViewCacheExtension中获取
        if (holder == null && mViewCacheExtension != null) {

        }
        if (holder == null) { // fallback to pool
            // 4. 从RecylerPool中获取
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
                
        // 5. 如果都获取不到,只能调用Adapter创建
        if (holder == null) {
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
        }
    }

    // ...

    boolean bound = false;
    if (mState.isPreLayout() && holder.isBound()) {

    } 
    // 6. 根据情况决定是否重新绑定
    else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 7. 更新布局参数
    // ....
}

第二步,涉及到Adapterstable id功能,大部分情况下用不到,所以暂时不考虑。

第三步使用的是自定义缓存,目前不分析,大家可以自己去研究下自定义缓存如何使用。

第一步,从mAttachedScraphidden view(正在消失的View),mCachedViews中获取ViewHolder。其中与我们相关的就是缓存mCachedViews,代码如下

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

    // 1. 从mAttachedScrap获取

    // 2. 从hidden views中获取

    // 3. 从mCachedViews中获取
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // 获取ViewHolder的条件
        // 1. ViewHolder没设置过FLAG_INVALID
        // 2. ViewHolder的布局位置要和添加的位置一样
        // 3. 没有被过渡动画所使用
        if (!holder.isInvalid() && holder.getLayoutPosition() == position
                && !holder.isAttachedToTransitionOverlay()) {
            // 本次分析中dryRun为false,代表获取ViewHolder之前,需要从缓存中移除
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        }
    }
    return null;
}

我们发现从mCachedViews获取ViewHolder还是有条件的

  1. ViewHolder有效,也就是没设置过FLAG_INVALID。
  2. ViewHolder的布局位置要和添加的位置一样。
  3. 没有被过渡动画所使用。

第一个和第三个条件,对于此时的分析是满足的。第二个条件很显然不一定满足,它是处理这样的情况,假如手指向上滑动导致第一个View被回收,此时手指马上向下滑动,这个时候就需要让刚被回收的第一个View再显示出来,因此就可以再利用刚回收的ViewHolder

从这里我们可以明白,mCachedViews是用来处理刚刚被回收的子View,然后又要让这个子View再显示的情况。

从前面回收ViewHolder的分析可知,回收还用到了RecyclerPool,因此这里我们还要分析从RecyclerPool获取ViewHolder的情况,也就是第四步,它调用的是RecyclerPool#getRecyclerView()方法

public ViewHolder getRecycledView(int viewType) {
    // 根据类型获取缓存的集合
    final ScrapData scrapData = mScrap.get(viewType);
    if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
        final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
        for (int i = scrapHeap.size() - 1; i >= 0; i--) {
            // ViewHolder没有被过渡动画使用,那么默认返回的就是第一个ViewHolder
            if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
                return scrapHeap.remove(i);
            }
        }
    }
    return null;
}

这里与前面分析RecyclerPool的缓存原理一致,首先根据类型获取缓存集合,然后返回一个没有被过渡动画使用的ViewHolder。但是这个ViewHolder中保存的数据是不可靠的,因此需要重新绑定一次,也就是要调用Adapter#bindViewHolder()方法,从而会调用子类AdapteronBindViewHolder()方法。

我们来总结下mCachedViewsRecyclerPool两个缓存的的特点

  1. mCachedViews适用于子View刚被回收,然后又马上要显示的情况。从这个缓存中获取的ViewHolder不需要再绑定。
  2. RecyclerPool是为了节约内存。从这个缓存中获取的ViewHolder是需要重新绑定的。

那么如果缓存中获取不到ViewHolder呢?自然需要通过Adapter创建了,这个过程在前面的文章已经分析过了。

实现RV的滚动

现在,我们已经明白了子View如何回收再利用,那么现在我们来进行最后一步,分析RV如何实现滚动

可能你已经忘记从哪里分析了,没关系,已经分析到了LLM的scrollBy()中的第三步

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
    // ...
    // 1. 更新mLayoutState信息
    updateLayoutState(layoutDirection, absDelta, true, state);
        
    // 2. 根据情况回收,创建子View,
    int added = fill(recycler, mLayoutState, state, false);
        
    // 3. 计算实际需要滚动的距离
    final int consumed = mLayoutState.mScrollingOffset + added;
    if (consumed < 0) {
        return 0;
    }
    final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;
        
    // 4. 执行RV的滚动
    mOrientationHelper.offsetChildren(-scrolled);
        
    return scrolled;
}

第三步,计算实际需要滚动的距离。这里分多种情况,请大家自行分析。

第四步,实现RV的滚动,这里是通过基类LayoutManager#offsetChildrenVertical(),再由RecyclerView#offsetChildrenVertical()实现的

public void offsetChildrenVertical(@Px int dy) {
    final int childCount = mChildHelper.getChildCount();
    for (int i = 0; i < childCount; i++) {
        mChildHelper.getChildAt(i).offsetTopAndBottom(dy);
    }
}

原来是通过位移RV的每一个子View完成来完成RV的整体滚动。

感想

RecyclerView每一个过程的分析,都需要极大的耐心和毅力,为写这篇文章,我酝酿了每一个细节,只希望能讲解明白。但是限于篇幅,文章中很多小细节只给出了注释,需要大家在源码中自己去分析去体会,才能真正做到融会贯通。