为了账号安全,请及时绑定邮箱和手机立即绑定

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

标签:
Android

上一篇文章讲述了“从哪里获得回收的表项”,这一篇会结合实际回收场景分析下“回收哪些表项?”。
(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;
    }
}

一图胜千语:


webp



作者:唐子玄
链接:https://www.jianshu.com/p/6eabcb7743b1


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消