Android高工面試:RecyclerView 滾動時,新表項是如何一個個被填充進來的?舊錶項是如何一個個被回收的?

今天給大家分享一道阿里P6級面試:關於 RecyclerView 面試真題:“RecyclerView 滾動時,新表項是如何一個個被填充進來的?舊錶項是如何一個個被回收的?”這篇以走讀源碼的方式,解答這個問題。

作者:唐子玄

觸發滾動的源頭

手指在屏幕滑動,列表隨之滾動,觸發滾動的源頭必然在觸摸事件中:

public class RecyclerView {
    // RecyclerView 重載 onTouchEvent()
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        switch (action) {
            // RecyclerView 對滑動事件的處理
            case MotionEvent.ACTION_MOVE: {
                ...
                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        e)) {...}
            }
        }
    }
}

RecyclerView 在處理滑動事件時調用了scrollByInternal(),並且把滾動位移作爲參數傳入:

public class RecyclerView {
    boolean scrollByInternal(int x, int y, MotionEvent ev) {
        ...
        scrollStep(x, y, mReusableIntPair);
        ...
        // 真正地實現滾動
        dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,TYPE_TOUCH, mReusableIntPair);
        ...
    }
}

在真正實現滾動之前,調用了scrollStep(),位移繼續作爲參數傳遞:

public class RecyclerView {
    LayoutManager mLayout;
    void scrollStep(int dx, int dy, @Nullable int[] consumed) {
        ...
        if (dx != 0) {
            consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);
        }
        if (dy != 0) {
            consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);
        }
        ...
    }
}

scrollStep()分別處理了兩個方向上的滾動,並將其委託給了LayoutManager,以LinearLayoutManager中的垂直滾動爲例:

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

垂直方向的位移作爲參數傳入,並傳遞給scrollBy():

public class LinearLayoutManager {
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // 填充表項
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
        ...
    }
}

發現了一個關鍵方法fill(),看名字有“填充”的意思,難道列表滾動之前會把即將出現的表項先填充進來?

填充表項

帶着疑問,點開fill()

public class LinearLayoutManager {
    // 根據剩餘空間填充表項
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 計算剩餘空間 = 可用空間 + 額外空間(=0)
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 循環,當剩餘空間 > 0 時,繼續填充更多表項
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            // 填充單個表項
            layoutChunk(recycler, state, layoutState, layoutChunkResult)
            ...
        }
    }
}

填充表項是一個while循環,循環結束條件是“列表剩餘空間是否 > 0”,每次循環調用layoutChunk()將單個表項填充到列表中:

public class LinearLayoutManager {
    // 填充單個表項
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 1.獲取下一個該被填充的表項視圖
        View view = layoutState.next(recycler);
        // 2.使表項成爲 RecyclerView 的子視圖
        addView(view);
        ...
        // 3.測量表項視圖(把 RecyclerView 內邊距和表項裝飾考慮在內)
        measureChildWithMargins(view, 0, 0);
        // 獲取填充表項視圖需要消耗的像素值
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        ...
        // 4.佈局表項
        layoutDecoratedWithMargins(view, left, top, right, bottom);
    }
}

layoutChunk()先從緩存池中獲取下一個該被填充新表項的視圖,之所以稱之爲新表項,是因爲在滾動發生之前,這些表項還未顯示在屏幕上。(關於複用的詳細分析可以移步RecyclerView 緩存機制 | 如何複用表項?)。

緊接着調用了addView()使表項視圖成爲 RecyclerView 的子視圖,調用鏈如下:

public class RecyclerView {
    ChildHelper mChildHelper;
    public abstract static class LayoutManager {
        public void addView(View child) {
            addView(child, -1);
        }

        public void addView(View child, int index) {
            addViewInt(child, index, false);
        }

        private void addViewInt(View child, int index, boolean disappearing) {
            ...
            // 委託給 ChildHelper
            mChildHelper.attachViewToParent(child, index, child.getLayoutParams(), false);
            ...
        }
    }
}

class ChildHelper {
    final Callback mCallback;
    void attachViewToParent(View child, int index, ViewGroup.LayoutParams layoutParams,boolean hidden) {
        ...
        mCallback.attachViewToParent(child, offset, layoutParams);
    }
}

調用鏈從RecyclerViewLayoutManager再到ChildHelper,最後又回到了RecyclerView

public class RecyclerView {
    ChildHelper mChildHelper;
    private void initChildrenHelper() {
        mChildHelper = new ChildHelper(new ChildHelper.Callback() {
            @Override
            public void attachViewToParent(View child, int index,ViewGroup.LayoutParams layoutParams) {
                ...
                RecyclerView.this.attachViewToParent(child, index, layoutParams);
            }
            ...
        }
    }
}

addView()的最終落腳點是ViewGroup.attachViewToParent()

public abstract class ViewGroup {
    protected void attachViewToParent(View child, int index, LayoutParams params) {
        ...
        // 將子視圖添加到數組中
        addInArray(child, index);
        // 子視圖和父親關聯
        child.mParent = this;
        ...
    }
}

attachViewToParent()中包含了“添加子視圖”最具標誌性的兩個動作:1. 將子視圖添加到數組中 2. 子視圖和父親關聯。

表項成爲 RecyclerView 子視圖之後,對其進行了測量:

public class LinearLayoutManager {
    // 填充單個表項
    void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {
        // 1.獲取下一個該被填充的表項視圖
        View view = layoutState.next(recycler);
        // 2.使表項成爲 RecyclerView 的子視圖
        addView(view);
        ...
        // 3.測量表項視圖(把 RecyclerView 內邊距和表項裝飾考慮在內)
        measureChildWithMargins(view, 0, 0);
        // 獲取填充表項視圖需要消耗的像素值
        result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
        ...
        // 4.佈局表項
        layoutDecoratedWithMargins(view, left, top, right, bottom);
    }
}

測量得到子視圖的尺寸,就可以知道填充該表項會消耗掉多少像素值,將該數值存儲在LayoutChunkResult.mConsumed中。

有了尺寸後,也就可以佈局表項了,即確定表項上下左右四個點相對於 RecyclerView 的位置:

public class RecyclerView {
    public abstract static class LayoutManager {
        public void layoutDecoratedWithMargins(View child, int left, int top, int right,int bottom) {
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            final Rect insets = lp.mDecorInsets;
            // 定位子表項
            child.layout(left + insets.left + lp.leftMargin, top + insets.top + lp.topMargin,
                    right - insets.right - lp.rightMargin,
                    bottom - insets.bottom - lp.bottomMargin);
        }
    }
}

調用控件的layout()方法即是爲控件定位,關於定位子控件的詳細介紹可以移步Android自定義控件 | View繪製原理(畫在哪?)

填充完一個表項後,會從remainingSpace中扣除它所佔用的空間(這樣 while 循環才能結束)

public class LinearLayoutManager {
    // 根據剩餘空間填充表項
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 計算剩餘空間 = 可用空間 + 額外空間(=0)
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 循環,當剩餘空間 > 0 時,繼續填充更多表項
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            ...
            // 填充單個表項
            layoutChunk(recycler, state, layoutState, layoutChunkResult)
            ...
            // 從剩餘空間中扣除新表項佔用像素值
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            remainingSpace -= layoutChunkResult.mConsumed;
            ...
        }
    }
}

至此可以得出結論:

  1. RecyclerView 在滾動發生之前,會有一個填充新表項的動作,填充的是當前還未顯示的表項。
  1. RecyclerView 填充表項是通過while循環實現的,當列表沒有剩餘空間時,填充表項也就結束了。

那到底要填充幾個新表項?回看一眼while循環的退出條件:

public class LinearLayoutManager {
    // 根據剩餘空間填充表項
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 計算剩餘空間 = 可用空間 + 額外空間(=0)
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        // 循環,當剩餘空間 > 0 時,繼續填充更多表項
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {...}
    }
}

填充表項的個數取決於remainingSpace的大小,它的值有兩個變量相加所得,其中layoutState.mExtraFillSpace的值爲 0(斷點調試告訴我的),而layoutState.mAvailable是由傳入參數layoutState決定的,沿着調用鏈網上搜索它被賦值的地方:

public class LinearLayoutManager {
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // 取滑動位移絕對值
        final int absDelta = Math.abs(delta);
        // 更新 LayoutState (將位移絕對值傳入)
        updateLayoutState(layoutDirection, absDelta, true, state);
        // 填充表項
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
        ...
    }

    private void updateLayoutState(int layoutDirection, int requiredSpace, boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        mLayoutState.mAvailable = requiredSpace;
        ...
    }
}

在填充表項之前,mLayoutState.mAvailable的值被置爲滾動位移的絕對值。

至此可以進一步細化之前的結論:

RecyclerView 在滾動發生之前,會根據滾動位移大小來決定需要向列表中填充多少新的表項。

回收表項

有新表項被填充到列表,就有舊錶項被回收,就好比隨着滾動,新表項移入屏幕,舊錶項移出屏幕。

那如何決定回收哪些表項呢?

RecyclerView 通過Recycler.recycleView()回收表項,以它爲切入點,向上查找調用鏈中是否存在和滾動相關的地方:

public class RecyclerView {
    public final class Recycler {
        // 0
        public void recycleView(@NonNull View view) {...}
    }

    public abstract static class LayoutManager {
        public void removeAndRecycleViewAt(int index, @NonNull Recycler recycler) {
            final View view = getChildAt(index);
            removeViewAt(index);
            // 1
            recycler.recycleView(view);
        }
    }
}

public class LinearLayoutManager {
    private void recycleChildren(RecyclerView.Recycler recycler, int startIndex, int endIndex) {
        // 2:回收索引值爲 endIndex -1 到 startIndex 的表項
        for (int i = endIndex - 1; i >= startIndex; i--) {
            removeAndRecycleViewAt(i, recycler);
        }
    }

    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        ...
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            if (mOrientationHelper.getDecoratedEnd(child) > limit|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                // 3
                recycleChildren(recycler, 0, i);
            }
        }
    }

    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        // 4
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }

    int fill(RecyclerView.Recycler recycler, LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 循環填充表項
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // 填充單個表項
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
            if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
                layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
                // 5:回收表項
                recycleByLayoutState(recycler, layoutState);
            }
            ...
        }
    }
}

沿着調用鏈一直往上查找(註釋中的0-5),居然在填充表項的fill()方法中找到了回收表項的操作。而且是在每次循環填充一個新表項之後,立馬執行了回收操作。

那到底回收哪些表項呢?

要回答這個問題,剛纔那段代碼中套在recycleChildren(recycler, 0, i)外面的判斷邏輯是關鍵:

public class LinearLayoutManager {
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        ...
        // 遍歷列表中當前所有表項
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 若該子表項滿足某個條件,則回收索引從 0 到 i-1 的表項
            if (mOrientationHelper.getDecoratedEnd(child) > limit|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                recycleChildren(recycler, 0, i);
            }
        }
    }
}

回收表項的關鍵判斷條件是mOrientationHelper.getDecoratedEnd(child) > limit

其中的mOrientationHelper.getDecoratedEnd(child)代碼如下:

// 屏蔽方向的抽象接口,用於減少關於方向的 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又是什麼意思?

在縱向列表中,“表項底部縱座標 > 某個值”意味着表項位於某條線的下方,即 limit 是列表中隱形的線,所有在這條線上方的表項都應該被回收。

那這條limit 隱形線是如何計算的?

public class LinearLayoutManager extends RecyclerView.LayoutManager {
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        // 計算隱形線的值
        final int limit = scrollingOffset - noRecycleSpace;
        ...
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 若該子表項滿足某個條件,則回收索引從 0 到 i 的表項
            if (mOrientationHelper.getDecoratedEnd(child) > limit|| mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                recycleChildren(recycler, 0, i);
            }
        }
    }
}

limit的值由 2 個變量決定,其中noRecycleSpace的值爲 0(這是斷點告訴我的,詳細過程可移步RecyclerView 動畫原理 | 換個姿勢看源碼(pre-layout)

scrollingOffset的值由外部傳入:

public class LinearLayoutManager {
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        int scrollingOffset = layoutState.mScrollingOffset;
        ...
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

limit 的值即是layoutState.mScrollingOffset的值,問題轉換爲layoutState.mScrollingOffset的值由什麼決定?全局搜索下它被賦值的地方:

public class LinearLayoutManager {
    private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        int scrollingOffset;
        // 獲取列表末尾的表項視圖
        final View child = getChildClosestToEnd();
        // 計算在不往列表裏填充新表項的情況下,列表最多可以滾動多少像素
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child) - mOrientationHelper.getEndAfterPadding();
        ...
        // mLayoutState.mScrollingOffset 被賦值
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // 滑動位移絕對值
        final int absDelta = Math.abs(delta);
        // 更新 LayoutState
        updateLayoutState(layoutDirection, absDelta, true, state);
        // 滑動前,填充新表項,回收舊錶項
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
        ...
    }
}

在滑動還未發生並準備填充新表項之前,調用了updateLayoutState(),該方法先獲取了列表末尾表項的視圖,並通過mOrientationHelper.getDecoratedEnd(child)計算出該表項底部到列表頂部的距離,然後在減去列表長度。這個差值可以理解爲在不往列表裏填充新表項的情況下,列表最多可以滾動多少像素。略抽象,圖示如下:

圖中藍色邊框表示列表,灰色矩形表示表項。

LayoutManager只會加載可見表項,圖中表項 6 有一半露出了屏幕,所以它會被加載到列表中,完全不可見的表項 7 不會被加載。這種情況下,如果不繼續往列表中填充表項 7,那列表最多滑動的距離就是半個表項 6 的長度,表現在代碼中即是mLayoutState.mScrollingOffset的值。

假設表項 6 之後沒有更多的數據,即列表只能滑動到表項 6 的底部。在這個場景下limit的值 = 半個表項 6 的長度。也就是說limit 隱形線應該在如下位置:

回看一下,回收表項的代碼:

public class LinearLayoutManager {
    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        final int limit = scrollingOffset - noRecycleSpace;
        //從頭開始遍歷 LinearLayoutManager,以找出應該被回收的表項
        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 如果表項的下邊界 > limit 隱形線
            if (mOrientationHelper.getDecoratedEnd(child) > limit
                    || mOrientationHelper.getTransformedEndWithDecoration(child) > limit) {
                //回收索引爲 0 到 i-1 的表項
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}

回收邏輯從頭開始遍歷 LinearLayoutManager,當遍歷到表項 1 的時候,發現它的下邊界 > limit,所以觸發表項回收,回收表項的索引區間爲 0 到 0 - 1,即沒有任何表項被回收。(想想也是,表項 1 還未完整地被移出屏幕)。

至此可以得出結論:

RecyclerView 滑動發生之前,會計算出一條limit 隱形線,它是決定哪些表項該被回收的重要依據。觸發回收邏輯時,會遍歷當前所有表項,若某表項的底部位於limit 隱形線下方,則該表項上方的所有表項都會被回收。

把剛纔假設的場景更一般化,若表項 6 之後還有數據,且滑動距離很大時會發生什麼?

計算limit值的方法updateLayoutState()scrollBy()中被調用:

public class LinearLayoutManager {
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        // 將滾動距離的絕對值傳入 updateLayoutState()
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        ...
    }

    private void updateLayoutState(int layoutDirection, int requiredSpace,boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        // 計算在不往列表裏填充新表項的情況下,列表最多可以滾動多少像素
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)- mOrientationHelper.getEndAfterPadding();
        ...
        // 將列表因滾動而需要的額外空間存儲在 mLayoutState.mAvailable
        mLayoutState.mAvailable = requiredSpace;
        mLayoutState.mScrollingOffset = scrollingOffset;
        ...
    }
}

兩個重要的值被依次存儲在mLayoutState.mScrollingOffsetmLayoutState.mAvailable,分別是“在不往列表裏填充新表項的情況下,列表最多可以滾動多少像素”,及“預計滾動像素值”。前者是回收多少舊錶項的依據,後者是填充多少新表項的依據。

srollBy()在調用updateLayoutState()存儲了這兩個重要的值之後,立馬進行了填充表項的操作:

public class LinearLayoutManager {
    int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {
        ...
        final int absDelta = Math.abs(delta);
        updateLayoutState(layoutDirection, absDelta, true, state);
        // 填充表項
        final int consumed = mLayoutState.mScrollingOffset + fill(recycler, mLayoutState, state, false);
        ...
    }
}

存儲着兩個重要值的mLayoutState作爲參數傳入了fill()

public class LinearLayoutManager {
    int fill(RecyclerView.Recycler recycler, LayoutState layoutState, RecyclerView.State state, boolean stopOnFocusable) {
        ...
        // 將預計滑動距離作爲填充多少新表項的依據
        int remainingSpace = layoutState.mAvailable + layoutState.mExtraFillSpace;
        while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
            // 填充單個表項
            layoutChunk(recycler, state, layoutState, layoutChunkResult);
            ...
            // 在 layoutState.mScrollingOffset 上追加因新表項填充消耗的像素值
            layoutState.mScrollingOffset += layoutChunkResult.mConsumed;
            // 回收表項
            recycleByLayoutState(recycler, layoutState);
            ...
        }
        ...
    }
}

在循環填充新表項時,新表項佔用的像素值每次都會追加到layoutState.mScrollingOffset,即它的值在不斷增大(limit 隱形線在不斷下移)。在一次while循環的最後,會調用recycleByLayoutState()根據當前limit 隱形線的位置回收表項:

public class LinearLayoutManager {
    private void recycleByLayoutState(RecyclerView.Recycler recycler, LayoutState layoutState) {
        ...
        recycleViewsFromStart(recycler, scrollingOffset, noRecycleSpace);
    }
}

    private void recycleViewsFromStart(RecyclerView.Recycler recycler, int scrollingOffset,int noRecycleSpace) {
        final int limit = scrollingOffset - noRecycleSpace;
        final int childCount = getChildCount();
        // 從頭遍歷表項
        for (int i = 0; i < childCount; i++) {
            View child = getChildAt(i);
            // 當某表項底部位於 limit 隱形線之後時,回收它以上的所有表項
            if (mOrientationHelper.getDecoratedStart(child) > limit || mOrientationHelper.getTransformedStartWithDecoration(child) > limit) {
                recycleChildren(recycler, 0, i);
                return;
            }
        }
    }
}

每向列表尾部填充一個表項,limit隱形線的位置就往下移動表項佔用的像素值,這樣列表頭部也就有更多的表項符合被回收的條件。

關於回收細節的分析,可以移步RecyclerView 緩存機制 | 回收到哪去?

用一張圖來總結limit 隱形線(圖中紅色虛線):

limit的值表示這一次實際滾動的總距離。(圖中是一種理想情況,即當滾動結束後新插入表項 7 的底部正好和列表底部重疊)

limit 隱形線可以理解爲:隱形線當前所在位置,在滾動完成後會和列表頂部重疊

總結

  1. RecyclerView 在滾動發生之前,會根據預計滾動位移大小來決定需要向列表中填充多少新的表項。
  1. RecyclerView 填充表項是通過while循環一個一個實現的,當列表沒有剩餘空間時,填充表項也就結束了。
  1. RecyclerView 滑動發生之前,會計算出一條limit 隱形線,它是決定哪些表項該被回收的重要依據。它可以理解爲:隱形線當前所在位置,在滾動完成後會和列表頂部重疊
  1. limit 隱形線的初始值 = 列表當前可見表項的底部到列表頂部的距離,即列表在不填充新表項時,可以滑動的最大距離。每一個新填充表象消耗的像素值都會被追加到 limit 值之上,即limit 隱形線會隨着新表項的填充而不斷地下移。
  1. 觸發回收邏輯時,會遍歷當前所有表項,若某表項的底部位於limit 隱形線下方,則該表項上方的所有表項都會被回收。

面試複習筆記:

這份資料我從春招開始,就會將各博客、論壇。網站上等優質的Android開發中高級面試題收集起來,然後全網尋找最優的解答方案。每一道面試題都是百分百的大廠面經真題+最優解答。包知識脈絡 + 諸多細節。
節省大家在網上搜索資料的時間來學習,也可以分享給身邊好友一起學習。
給文章留個小贊,就可以免費領取啦~

戳我領取:Android對線暴打面試指南超硬核Android面試知識筆記3000頁Android開發者架構師核心知識筆記

《960頁Android開發筆記》

《1307頁Android開發面試寶典》

包含了騰訊、百度、小米、阿里、樂視、美團、58、獵豹、360、新浪、搜狐等一線互聯網公司面試被問到的題目。熟悉本文中列出的知識點會大大增加通過前兩輪技術面試的機率。

《507頁Android開發相關源碼解析》

只要是程序員,不管是Java還是Android,如果不去閱讀源碼,只看API文檔,那就只是停留於皮毛,這對我們知識體系的建立和完備以及實戰技術的提升都是不利的。

真正最能鍛鍊能力的便是直接去閱讀源碼,不僅限於閱讀各大系統源碼,還包括各種優秀的開源庫。

資料已經上傳在我的GitHub

文末

聽說點贊關注的小夥伴都面試成功了?如果本篇博客對你有幫助,請支持下小編哦

Android高級面試精選題、架構師進階實戰文檔傳送門:我的GitHub

整寫作易,覺得有幫助的朋友可以幫忙點贊分享支持一下小編~

你的支持,我的動力;祝各位前程似錦,offer不斷!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章