RecyclerView緩存機制(回收些啥邢隧?)

這是RecyclerView緩存機制系列文章的第三篇,系列文章的目錄如下:

  1. RecyclerView緩存機制(咋復用冈在?)
  2. RecyclerView緩存機制(回收些啥倒慧?)
  3. RecyclerView緩存機制(回收去哪?)
  4. RecyclerView緩存機制(scrap view)

上一篇文章講述了“從哪里獲得回收的表項”,這一篇會結合實際回收場景分析下“回收哪些表項纫谅?”炫贤。
(ps: 下文中的 粗斜體字 表示引導源碼閱讀的內心戲)

回收場景


在眾多回收場景中最顯而易見的就是“滾動列表時移出屏幕的表項被回收”。滾動是由MotionEvent.ACTION_MOVE事件觸發(fā)的付秕,就以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;
            ...
    }
}

去掉了大量位移賦值邏輯后兰珍,一個處理滾動的函數(shù)出現(xiàn)在眼前:

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(這個函數(shù)對于“回收哪些表項”來說很關鍵询吴,待會會提到)
        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;
    }
    ...
}

沿著調用鏈往下找掠河,發(fā)現(xiàn)了一個上一篇中介紹過的函數(shù)LinearLayoutManager.fill(),原來列表滾動的同時也會不斷的向其中填充表項(想想也是猛计,不然怎么會不斷有新的表項出現(xiàn)呢~)唠摹。上一遍只關注了其中填充的邏輯,但其實里面還有回收邏輯:

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;
        //不斷循環(huán)獲取新的表項用于填充奉瘤,直到沒有填充空間
        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;
    }
}

在不斷獲取新表項用于填充的同時也在回收表項(想想也是,列表滾動的時候有表項插入的同時也有表項被移出)盗温,移步到回收表項的函數(shù):

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.(該參數(shù)被用于檢測滾出屏幕的表項)
     */
    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;
            }
            ...
}

結合注釋和該方法的實現(xiàn)风钻,原來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被賦值的地方吧~澄惊,調用鏈很長唆途,就不全部羅列了富雅,但其中有兩個關鍵點,其實我在上面的代碼中埋了伏筆肛搬,現(xiàn)在再羅列一下:

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(這個函數(shù)對于“回收哪些表項”來說很關鍵没佑,待會會提到)
        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) {
        ...
        //不斷循環(huán)獲取新的表項用于填充,直到沒有填充空間
        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)
            // 獲得一個滾動偏移量蛤奢,如果只滾動了這個數(shù)值那不需要添加新的孩子
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
          ...
        }
        ...
        //對mLayoutState.mScrollingOffset賦值
        mLayoutState.mScrollingOffset = scrollingOffset;
    }
}

一圖勝千語:


屏幕快照 2019-02-16 下午7.23.51.png

關于limit等于0的YY破滅了,其實limit是一根橫謂語列表中間的橫線陶贼,它的值表示這一次滾動的總距離啤贩。(圖中是一種理想情況,即當滾動結束后新插入表項的底部正好和列表底部重疊)其實回收表項的時機是在滾動真正發(fā)生之前拜秧,此時我們預先計算出滾動的偏移量痹屹,根據(jù)偏移量篩選出滾動發(fā)生后應該被刪除的表項。即 limit這根線也可以表述為:當滾動發(fā)生后腹纳,列表當前 limit這個位置會成為列表的頭部

分析完“回收哪些表項”后痢掠,一不小心發(fā)現(xiàn)篇幅有點長了驱犹,那關于回收去哪里嘲恍?將放到下一篇在講。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末雄驹,一起剝皮案震驚了整個濱河市佃牛,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌医舆,老刑警劉巖俘侠,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蔬将,居然都是意外死亡爷速,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進店門霞怀,熙熙樓的掌柜王于貴愁眉苦臉地迎上來惫东,“玉大人,你說我怎么就攤上這事毙石×冢” “怎么了?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵徐矩,是天一觀的道長滞时。 經常有香客問我,道長滤灯,這世上最難降的妖魔是什么坪稽? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任曼玩,我火速辦了婚禮,結果婚禮上刽漂,老公的妹妹穿的比我還像新娘演训。我一直安慰自己,他們只是感情好贝咙,可當我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布样悟。 她就那樣靜靜地躺著,像睡著了一般庭猩。 火紅的嫁衣襯著肌膚如雪窟她。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天蔼水,我揣著相機與錄音震糖,去河邊找鬼。 笑死趴腋,一個胖子當著我的面吹牛吊说,可吹牛的內容都是我干的。 我是一名探鬼主播优炬,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼颁井,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蠢护?” 一聲冷哼從身側響起雅宾,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎葵硕,沒想到半個月后眉抬,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡懈凹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年蜀变,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片介评。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡库北,死狀恐怖,靈堂內的尸體忽然破棺而出威沫,到底是詐尸還是另有隱情贤惯,我是刑警寧澤,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布棒掠,位于F島的核電站孵构,受9級特大地震影響,放射性物質發(fā)生泄漏烟很。R本人自食惡果不足惜颈墅,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一蜡镶、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧恤筛,春花似錦官还、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至煎殷,卻和暖如春屯伞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背豪直。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工劣摇, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人弓乙。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓末融,卻偏偏與公主長得像,于是被迫代替她去往敵國和親暇韧。 傳聞我的和親對象是個殘疾皇子勾习,可洞房花燭夜當晚...
    茶點故事閱讀 44,577評論 2 353