阅读 1727

RecyclerView缓存机制(回收些啥?)

这是RecyclerView缓存机制系列文章的第二篇,系列文章的目录如下:

  1. RecyclerView缓存机制(咋复用?)

  2. RecyclerView缓存机制(回收些啥?)

  3. RecyclerView缓存机制(回收去哪?)

  4. RecyclerView缓存机制(scrap view)

  5. 读源码长知识 | 更好的RecyclerView点击监听器

  6. 代理模式应用 | 每当为 RecyclerView 新增类型时就很抓狂

  7. 更好的 RecyclerView 表项子控件点击监听器

  8. 更高效地刷新 RecyclerView | DiffUtil二次封装

  9. 换一个思路,超简单的RecyclerView预加载

  10. RecyclerView 动画原理 | 换个姿势看源码(pre-layout)

  11. RecyclerView 动画原理 | pre-layout,post-layout 与 scrap 缓存的关系

如果想直接看结论可以移步到第四篇末尾(你会后悔的,过程更加精彩)。

上一篇文章讲述了“从哪里获得回收的表项”,这一篇会结合实际回收场景分析下“回收哪些表项?”。

回收场景

在众多回收场景中最显而易见的就是“滚动列表时移出屏幕的表项被回收”。滚动是由MotionEvent.ACTION_MOVE事件触发的,就以RecyclerView.onTouchEvent()为切入点寻觅“回收表项”的时机

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
    @Override
    public boolean onTouchEvent(MotionEvent e) {
            ...
            case MotionEvent.ACTION_MOVE: {
                    ...
                    // 内部滚动
                    if (scrollByInternal(
                            canScrollHorizontally ? dx : 0,
                            canScrollVertically ? dy : 0,
                            vtev)) {
                        getParent().requestDisallowInterceptTouchEvent(true);
                    }
                    ...
                }
            } break;
            ...
    }
}
复制代码

去掉了大量位移赋值逻辑后,一个处理滚动的函数出现在眼前:

public class RecyclerView extends ViewGroup implements ScrollingView, NestedScrollingChild2 {
   ...
   LayoutManager mLayout;// 处理滚动的LayoutManager
   ...
   boolean scrollByInternal(int x, int y, MotionEvent ev) {
        ...
        if (mAdapter != null) {
            ...
            if (x != 0) { // 水平滚动
                consumedX = mLayout.scrollHorizontallyBy(x, mRecycler, mState);
                unconsumedX = x - consumedX;
            }
            if (y != 0) { // 垂直滚动
                consumedY = mLayout.scrollVerticallyBy(y, mRecycler, mState);
                unconsumedY = y - consumedY;
            }
            ...
        }
        ...
}
复制代码

RecyclerView把滚动委托给LayoutManager来处理:

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {

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

    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
    	...
        //更新LayoutState(这个函数对于“回收哪些表项”来说很关键,待会会提到)
        updateLayoutState(layoutDirection, absDy, true, state);
        //滚动时向列表中填充新的表项
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
		...
        return scrolled;
    }
    ...
}
复制代码

沿着调用链往下找,发现了一个上一篇中介绍过的函数LinearLayoutManager.fill(),列表滚动的同时会不断的向其中填充表项。

上一遍只关注了其中填充的逻辑,里面还有回收逻辑:

public class LinearLayoutManager extends RecyclerView.LayoutManager {
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        ...
        int remainingSpace = layoutState.mAvailable + layoutState.mExtra;
        LayoutChunkResult layoutChunkResult = mLayoutChunkResult;
        //不断循环获取新的表项用于填充,直到没有填充空间
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            //填充新的表项
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
			...
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                //在当前滚动偏移量基础上追加因新表项插入增加的像素(这句话对于“回收哪些表项”来说很关键)
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                //回收表项
                recycleByLayoutState(recycler, layoutState);
            }
            ...
        }
        ...
        return start - layoutState.mAvailable;
    }
}
复制代码

在不断获取新表项用于填充的同时也在回收表项(列表滚动的时候有表项插入的同时也有表项被移出),移步到回收表项的函数:

public class LinearLayoutManager extends RecyclerView.LayoutManager {
    ...
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        if (!layoutState.mRecycle || layoutState.mInfinite) {
            return;
        }
        if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        	// 从列表头回收
            recycleViewsFromEnd(recycler, layoutState.mScrollingOffset);
        } else {
        	// 从列表尾回收
            recycleViewsFromStart(recycler, layoutState.mScrollingOffset);
        }
    }
    ...
    /**
     * 当向列表尾部滚动时回收滚出屏幕的表项
     * @param dt(该参数被用于检测滚出屏幕的表项)
     */
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int dt) {
        final int limit = dt;
        final int childCount = getChildCount();
		...
            //遍历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;
                }
            }
    }
    ...
}
复制代码

原来RecyclerView的回收分两个方向:1. 从列表头回收 2.从列表尾回收。

就以“从列表头回收”为研究对象分析下RecyclerView在滚动时到底是怎么判断“哪些表项应该被回收?”。 (“从列表头回收表项”所对应的场景是:手指上滑,列表向下滚动,新的表项逐个插入到列表尾部,列表头部的表项逐个被回收。)

回收哪些表项

要回答这个问题,刚才那段代码中套在recycleChildren(recycler, 0, i)外面的判断逻辑是关键:mOrientationHelper.getDecoratedEnd(child) > limit

// 屏蔽方向的抽象接口,用于减少关于方向的if-else
public abstract class OrientationHelper {
	// 获取当前表项相对于列表头部的坐标
    public abstract int getDecoratedEnd(View view);

	// 垂直布局的实现
    public static OrientationHelper createVerticalHelper(RecyclerView.LayoutManager layoutManager) {
        return new OrientationHelper(layoutManager) {
            @Override
            public int getDecoratedEnd(View view) {
                final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
                        view.getLayoutParams();
                return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
            }
}
复制代码

mOrientationHelper.getDecoratedEnd(child) 表示当前表项的尾部相对于列表头部的坐标,OrientationHelper这层抽象屏蔽了列表的方向,所以这句话在纵向列表中可以翻译成“当前表项的底部相对于列表顶部的纵坐标”。

判断条件mOrientationHelper.getDecoratedEnd(child) > limit中的limit又是什么意思?在纵向列表中,“表项底部纵坐标 > 某个值”意味着表项位于某条线的下方,回看一眼“回收表项”的逻辑:

//遍历LinearLayoutManager的孩子找出其中应该被回收的
for (int i = 0; i < childCount; i++) {
    View child = getChildAt(i);
    final int limit = mOrientationHelper.getEnd() - scrollingOffset + noRecycleSpace;
    //直到表项底部纵坐标大于某个值后,回收该表项以上的所有表项
    if (mOrientationHelper.getDecoratedEnd(child) > limit
            || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
        //回收索引为0到索引为i-1的表项
        recycleChildren(recycler, 0, i);
        return;
    }
}
复制代码

隐约觉得limit应该等于0,这样不正好是回收所有从列表头移出的表项吗? 不知道这样YY到底对不对,还是沿着调用链向上找一下limit被赋值的地方,调用链很长,就不全部罗列了,但其中有两个关键点:

public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {   
    ...
    int scrollBy(int dy, RecyclerView.Recycler recycler, RecyclerView.State state) {
        //1. 更新LayoutState
        updateLayoutState(layoutDirection, absDy, true, state);
        //滚动时向列表中填充新的表项
        final int consumed = mLayoutState.mScrollingOffset
                + fill(recycler, mLayoutState, state, false);
        ...
    }
    ...
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
            RecyclerView.State state, boolean stopOnFocusable) {
        ...
        //不断循环获取新的表项用于填充,直到没有填充空间
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                //2. 在当前滚动偏移量基础上追加因新表项插入增加的像素
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                if (layoutState.mAvailable < 0) {
                    layoutState.mScrollingOffset += layoutState.mAvailable;
                }
                //回收表项
                recycleByLayoutState(recycler, layoutState);
            }
            ...
        }
        ...
        return start - layoutState.mAvailable;
    }
    ...
    private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        int scrollingOffset;
        if (layoutDirection == LayoutState.LAYOUT_END) {
            mLayoutState.mExtra += mOrientationHelper.getEndPadding();
            //获得当前方向上里列表尾部最近的孩子(最后一个孩子)
            final View child = getChildClosestToEnd();
            // the direction in which we are traversing children
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // 获得一个滚动偏移量,如果只滚动了这个数值那不需要添加新的孩子
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
          ...
        }
        ...
        //对mLayoutState.mScrollingOffset赋值
        mLayoutState.mScrollingOffset = scrollingOffset;
    }
}
复制代码

一图胜千语: 屏幕快照 2019-02-16 下午7.23.51.png

关于limit等于0的YY破灭了,其实limit是一根位于列表中间的横线,它的值表示这一次滚动的总距离。(图中是一种理想情况,即当滚动结束后新插入表项的底部正好和列表底部重叠)其实 回收表项的时机是在滚动真正发生之前,此时我们预先计算出滚动的偏移量,根据偏移量筛选出滚动发生后应该被删除的表项。即 limit这根线也可以表述为:当滚动发生后,列表当前 limit这个位置会成为列表的头部

分析完“回收哪些表项”后,一不小心发现篇幅有点长了,那关于回收去哪里?将放到下一篇在讲。