这是 RecyclerView 缓存机制系列文章的第三篇, 系列文章的目录如下:
RecyclerView 缓存机制 (咋复用?)
RecyclerView 缓存机制 (回收去哪?)
上一篇文章讲述了 "从哪里获得回收的表项", 这一篇会结合实际回收场景分析下 "回收哪些表项?".
(ps: 下文中的 粗斜体字 表示引导源码阅读的内心戏)
回收场景
在众多回收场景中最显而易见的就是 "滚动列表时移出屏幕的表项被回收". 滚动是由 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 {
- ...
- @VisibleForTesting LayoutManager mLayout;
- ...
- 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 来处理, 于是移步到最熟悉的 LinearLayoutManager:
- 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) {
- if (getChildCount() == 0 || dy == 0) {
- return 0;
- }
- mLayoutState.mRecycle = true;
- ensureLayoutState();
- final int layoutDirection = dy> 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(dy);
- // 更新 LayoutState(这个函数对于 "回收哪些表项" 来说很关键, 待会会提到)
- updateLayoutState(layoutDirection, absDy, true, state);
- // 滚动时向列表中填充新的表项
- final int consumed = mLayoutState.mScrollingOffset
- + fill(recycler, mLayoutState, state, false);
- if (consumed <0) {
- if (DEBUG) {
- Log.d(TAG, "Don't have any more elements to scroll");
- }
- return 0;
- }
- final int scrolled = absDy> consumed ? layoutDirection * consumed : dy;
- mOrientationHelper.offsetChildren(-scrolled);
- if (DEBUG) {
- Log.d(TAG, "scroll req:" + dy + "scrolled:" + scrolled);
- }
- mLayoutState.mLastScrollDelta = scrolled;
- return scrolled;
- }
- ...
- }
沿着调用链往下找, 发现了一个上一篇中介绍过的函数 LinearLayoutManager.fill(), 原来列表滚动的同时也会不断的向其中填充表项 ( 想想也是, 不然怎么会不断有新的表项出现呢~ ). 上一遍只关注了其中填充的逻辑, 但其实里面还有回收逻辑:
- public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
- ...
- 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)) {
- layoutChunkResult.resetInternal();
- if (VERBOSE_TRACING) {
- TraceCompat.beginSection("LLM LayoutChunk");
- }
- // 填充新的表项
- layoutChunk(recycler, state, layoutState, layoutChunkResult);
- if (VERBOSE_TRACING) {
- TraceCompat.endSection();
- }
- if (layoutChunkResult.mFinished) {
- break;
- }
- layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;
- if (!layoutChunkResult.mIgnoreConsumed || mLayoutState.mScrapList != null
- || !state.isPreLayout()) {
- layoutState.mAvailable -= layoutChunkResult.mConsumed;
- // we keep a separate remaining space because mAvailable is important for recycling
- remainingSpace -= layoutChunkResult.mConsumed;
- }
- if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
- // 在当前滚动偏移量基础上追加因新表项插入增加的像素 (这句话对于 "回收哪些表项" 来说很关键, 待会会提到)
- layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
- if (layoutState.mAvailable <0) {
- layoutState.mScrollingOffset += layoutState.mAvailable;
- }
- // 回收表项
- recycleByLayoutState(recycler, layoutState);
- }
- if (stopOnFocusable && layoutChunkResult.mFocusable) {
- break;
- }
- ...
- }
- ...
- return start - layoutState.mAvailable;
- }
- }
在不断获取新表项用于填充的同时也在回收表项 (想想也是, 列表滚动的时候有表项插入的同时也有表项被移出), 移步到回收表项的函数:
- public class LinearLayoutManager extends RecyclerView.LayoutManager implements ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
- ...
- 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);
- }
- }
- ...
- /**
- * 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) {
- if (dt <0) {
- if (DEBUG) {
- Log.d(TAG, "Called recycle from start with a negative value. This might happen"
- + "during layout changes but may be sign of a bug");
- }
- return;
- }
- // 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;
- }
- }
- }
- }
- ...
- }
原来 RecyclerView 的回收分两个方向: 1. 从列表头回收 2. 从列表尾回收. 就以 "从列表头回收" 为研究对象分析下 RecyclerView 在滚动时到底是怎么判断 "哪些表项应该被回收?". ("从列表头回收表项" 所对应的场景是: 手指上滑, 列表向下滚动, 新的表项逐个插入到列表尾部, 列表头部的表项逐个被回收.)
回收哪些表项
要回答这个问题, 刚才那段代码中套在 recycleChildren(recycler, 0, i) 外面的判断逻辑是关键: mOrientationHelper.getDecoratedEnd(child)> limit.
- /**
- * Helper class for LayoutManagers to abstract measurements depending on the View's orientation.
- * 该类用于帮助 LayoutManger 抽象出基于视图方向的测量
- * <p>
- * It is developed to easily support vertical and horizontal orientations in a LayoutManager but
- * can also be used to abstract calls around view bounds and child measurements with margins and
- * decorations.
- *
- * @see #createHorizontalHelper(RecyclerView.LayoutManager)
- * @see #createVerticalHelper(RecyclerView.LayoutManager)
- */
- public abstract class OrientationHelper {
- ...
- /**
- * Returns the end of the view including its decoration and margin.
- * <p>
- * For example, for the horizontal helper, if a View's right is at pixel 200, has 2px right
- * decoration and 3px right margin, returned value will be 205.
- *
- * @param view The view element to check
- * @return The last pixel of the element
- * @see #getDecoratedStart(Android.view.View)
- */
- 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);
- // 直到表项底部纵坐标大于某个值后, 回收该表项以上的所有表项
- if (mOrientationHelper.getDecoratedEnd(child)> limit
- || mOrientationHelper.getTransformedEndWithDecoration(child)> limit) {
- // stop here
- // 回收索引为 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) {
- if (getChildCount() == 0 || dy == 0) {
- return 0;
- }
- mLayoutState.mRecycle = true;
- ensureLayoutState();
- final int layoutDirection = dy> 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
- final int absDy = Math.abs(dy);
- //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);
- // calculate how much we can scroll without adding new children (independent of layout)
- // 获得一个滚动偏移量, 如果只滚动了这个数值那不需要添加新的孩子
- scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
- - mOrientationHelper.getEndAfterPadding();
- } else {
- ...
- }
- ...
- // 对 mLayoutState.mScrollingOffset 赋值
- mLayoutState.mScrollingOffset = scrollingOffset;
- }
- }
一图胜千语:
关于 limit 等于 0 的 YY 破灭了, 其实 limit 是一根横谓语列表中间的横线, 它的值表示这一次滚动的总距离.(图中是一种理想情况, 即当滚动结束后新插入表项的底部正好和列表底部重叠) 其实 回收表项的时机是在滚动真正发生之前, 此时我们预先计算出滚动的偏移量, 根据偏移量筛选出滚动发生后应该被删除的表项. 即 limit 这根线也可以表述为: 当滚动发生后, 列表当前 limit 这个位置会成为列表的头部
分析完 "回收哪些表项" 后, 一不小心发现篇幅有点长了, 那关于回收去哪里? 将放到下一篇在讲.
来源: https://juejin.im/post/5c696c69e51d45403f2aa366