掌握這17張圖,沒人比你更懂RecyclerView的預加載

回顧上一篇文章,我們為了減少描述問題的維度,于演示之前附加了許多限制條件,比如禁用了RecyclerView的預拉取機制。

實際上,預拉取(prefetch)機制作為RecyclerView的重要特性之一,常常與緩存復用機制一起配合使用、共同協作,極大地提升了RecyclerView整體滑動的流暢度。

并且,這種特性在ViewPager2中同樣得以保留,對ViewPager2滑動效果的呈現也起著關鍵性的作用。因此,我們ViewPager2系列的第二篇,就是要來著重介紹RecyclerView的預拉取機制。


預拉取是指什么?

在計算機術語中,預拉取指的是在已知需要某部分數據的前提下,利用系統資源閑置的空檔,預先拉取這部分數據到本地,從而提高執行時的效率。

具體到RecyclerView預拉取的情境則是:

  1. 利用UI線程正好處于空閑狀態的時機
  1. 預先拉取待進入屏幕區域內的一部分列表項視圖并緩存起來
  1. 從而減少因視圖創建或數據綁定等耗時操作所引起的卡頓。

預拉取是怎么實現的?

正如把緩存復用的實際工作委托給了其內部的Recycler類一樣,RecyclerView也把預拉取的實際工作委托給了一個名為GapWorker的類,其內部的工作流程,可以用以下這張思維導圖來概括:

接下來我們就循著這張思維導圖,來一一拆解預拉取的工作流程。

1.發起預拉取工作

通過查找對GapWorker對象的引用,我們可以梳理出3個發起預拉取工作的時機,分別是:

  • RecyclerView被拖動(Drag)時
    @Override
    public boolean onTouchEvent(MotionEvent e) {
        ...
        switch (action) {
            ...
            case MotionEvent.ACTION_MOVE: {
                ...
                if (mScrollState == SCROLL_STATE_DRAGGING) {
                    ...
                    // 處于拖動狀態并且存在有效的拖動距離時
                    if (mGapWorker != null && (dx != 0 || dy != 0)) {
                        mGapWorker.postFromTraversal(this, dx, dy);
                    }
                }
            }
            break;
            ...
        }
        ...
        return true;
    }

  • RecyclerView慣性滑動(Fling)時
101-1.gif
    class ViewFlinger implements Runnable {
        ...
        @Override
        public void run() {
            ...
             if (!smoothScrollerPending && doneScrolling) {
                ...
             } else {
                ...
                 if (mGapWorker != null) {
                        mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                    }
             }
        }
        ...
    }    
  • RecyclerView嵌套滾動時
   private void nestedScrollByInternal(int x, int y, @Nullable MotionEvent motionEvent, int type) {
        ...
        if (mGapWorker != null && (x != 0 || y != 0)) {
            mGapWorker.postFromTraversal(this, x, y);
        }
        ...
    }

2.執行預拉取工作

GapWorker是Runnable接口的一個實現類,意味著其執行工作的入口必然是在run方法。

final class GapWorker implements Runnable {
    @Override
    public void run() {
        ...
        prefetch(nextFrameNs);
        ...
    }
}

在run方法內部我們可以看到其調用了一個prefetch方法,在進入該方法之前,我們先來分析傳入該方法的參數。

        // 查詢最近一個垂直同步信號發出的時間,以便我們可以預測下一個
        final int size = mRecyclerViews.size();
        long latestFrameVsyncMs = 0;
        for (int i = 0; i < size; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            if (view.getWindowVisibility() == View.VISIBLE) {
                latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
            }
        }
        ...
        // 預測下一個垂直同步信號發出的時間
        long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;

        prefetch(nextFrameNs);

由該方法的實參命名nextFrameNs可知,傳入的是下一幀開始繪制的時間

了解過Android屏幕刷新機制的人都知道,當GPU渲染完圖形數據并放入圖像緩沖區(buffer)之后,顯示屏(Display)會等待垂直同步信號(Vsync)發出,隨即交換緩沖區并取出緩沖數據,從而開始對新的一幀的繪制。

所以,這個實參同時也表示下一個垂直同步信號(Vsync)發出的時間,這是個預測值,單位為納秒。由最近一個垂直同步信號發出的時間(latestFrameVsyncMs),加上每一幀刷新的間隔時間(mFrameIntervalNs)計算而成。

其中,每一幀刷新的間隔時間是這樣子計算得到的:

    // 如果取自顯示屏的刷新率數據有效,則不采用默認的60fps
    // 注意:此查詢我們只靜態地執行一次,因為它非常昂貴(>1ms)
    Display display = ViewCompat.getDisplay(this);
    float refreshRate = 60.0f;  // 默認的刷新率為60fps
    if (!isInEditMode() && display != null) {
        float displayRefreshRate = display.getRefreshRate();
        if (displayRefreshRate >= 30.0f) {
            refreshRate = displayRefreshRate;
        }
    }
    mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);   // 1000000000納秒=1秒

也即假定在默認60fps的刷新率下,每一幀刷新的間隔時間應為16.67ms。

再由該方法的形參命名deadlineNs可知,傳入的參數表示的是預抓取工作完成的最后期限

    void prefetch(long deadlineNs) {
        ...
    }

綜合一下就是,預抓取的工作必須在下一個垂直同步信號發出之前,也即下一幀開始繪制之前完成

什么意思呢?

這是由于從Android 5.0(API等級21)開始,出于提高UI渲染效率的考慮,Android系統引入了RenderThread機制,即渲染線程。這個機制負責接管原先主線程中繁重的UI渲染工作,使得主線程可以更加專注于與用戶的交互,從而大幅提高頁面的流暢度。

但這里有一個問題。

當UI線程提前完成工作,并將一個幀傳遞給RenderThread渲染之后,就會進入所謂的休眠狀態,出現了大量的空閑時間,直至下一幀開始繪制之前。如圖所示:

一方面,這些UI線程上的空閑時間并沒有被利用起來,相當于珍貴的線程資源被白白浪費掉;

另一方面,新的列表項進入屏幕時,又需要在UI線程的輸入階段(Input)就完成視圖創建與數據綁定的工作,這會推遲UI線程及RenderThread上的其他工作,如果這些被推遲的工作無法在下一幀開始繪制之前完成,就有可能造成界面上的丟幀卡頓。

GapWorker正是選擇在此時間窗口內安排預拉取的工作,也即把創建和綁定的耗時操作,移到UI線程的空閑時間內完成,與原先的RenderThread并行執行

但這個預拉取的工作同樣必須在下一幀開始繪制之前完成,否則預拉取的列表項視圖還是會無法被及時地繪制出來,進而導致丟幀卡頓,于是才有了前面表示最后期限的傳入參數。

了解完這個參數的含義后,讓我們繼續往下閱讀源碼。

2.1 構建預拉取任務列表

    void prefetch(long deadlineNs) {
        buildTaskList();
        ...
    }

進入prefetch方法后可以看到,預拉取的第一個動作就是先構建預拉取的任務列表,其內部又可分為以下3個事項:

2.1.1 收集預拉取的列表項數據

    private void buildTaskList() {
        // 1.收集預拉取的列表項數據
        final int viewCount = mRecyclerViews.size();
        int totalTaskCount = 0;
        for (int i = 0; i < viewCount; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            // 僅對當前可見的RecyclerView收集數據
            if (view.getWindowVisibility() == View.VISIBLE) {
                view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
                totalTaskCount += view.mPrefetchRegistry.mCount;
            }
        }
        ...
    }
    static class LayoutPrefetchRegistryImpl
            implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {
        ...
        void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
            ...
            // 啟用了預拉取機制
            if (view.mAdapter != null
                    && layout != null
                    && layout.isItemPrefetchEnabled()) {
                if (nested) {
                    ...
                } else {
                    // 基于移動量進行預拉取
                    if (!view.hasPendingAdapterUpdates()) {
                        layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                                view.mState, this);
                    }
                }
                ...
            }
        }
    }
public class LinearLayoutManager extends RecyclerView.LayoutManager implements
        ItemTouchHelper.ViewDropHandler, RecyclerView.SmoothScroller.ScrollVectorProvider {
        
    public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
            LayoutPrefetchRegistry layoutPrefetchRegistry) {
        // 根據布局方向取水平方向的移動量dx或垂直方向的移動量dy    
        int delta = (mOrientation == HORIZONTAL) ? dx : dy;
        ...
        ensureLayoutState();
        // 根據移動量正負值判斷移動方向
        final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
        final int absDelta = Math.abs(delta);
        // 收集與預拉取相關的重要數據,并存儲到LayoutState
        updateLayoutState(layoutDirection, absDelta, true, state);
        collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
    }
    
}

這一事項主要是依據RecyclerView滾動的方向,收集即將進入屏幕的、待預拉取的列表項數據,其中,最關鍵的2項數據是:

  • 待預拉取項的position值——用于預加載項位置的確定
  • 待預拉取項與RecyclerView可見區域的距離——用于預拉取任務的優先級排序

我們以最簡單的LinearLayoutManager為例,看一下這2項數據是怎樣收集的,其最關鍵的實現就在于前面的updateLayoutState方法。

假定此時我們的手勢是向上滑動的,則其進入的是layoutToEnd == true的判斷:

    private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
        ...
        if (layoutToEnd) {
            ...
            // 步驟1,獲取滾動方向上的第一個項
            final View child = getChildClosestToEnd();
            // 步驟2,確定待預拉取項的方向
            mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                    : LayoutState.ITEM_DIRECTION_TAIL;
            // 步驟3,確認待預拉取項的position
            mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
            mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
            // 步驟4,確認待預拉取項與RecyclerView可見區域的距離
            scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                    - mOrientationHelper.getEndAfterPadding();

        } else {
            ...
        }
        ...
        mLayoutState.mScrollingOffset = scrollingOffset;
    }

步驟1,獲取RecyclerView滾動方向上的第一項,如圖中①所示:

步驟2,確定待預拉取項的方向。不用反轉布局的情況下是ITEM_DIRECTION_TAIL,該值等于1,如圖中②所示:

步驟3,確認待預拉取項的position值。由滾動方向上的第一項的position值加上步驟2確定的方向值相加得到,對應的是RecyclerView待進入屏幕區域的下一個項,如圖中③所示:

步驟4,確認待預拉取項與RecyclerView可見區域的距離,該值由以下2個值相減得到:

  • getEndAfterPadding:指的是RecyclerView去除了Padding后的底部位置,并不完全等于RecyclerView的高度。
  • getDecoratedEnd:指的是由列表項的底部位置,加上列表項設立的外邊距,再加上列表項間隔的高度計算得到的值。

我們用一張圖來說明一下:

首先,圖中的①表示一個完整的屏幕可見區域,其中:

  • 深灰色區域對應的是RecyclerView設立的上下內邊距,即Padding值。
  • 中灰色區域對應的是RecyclerView的列表項分隔線,即Decoration。
  • 淺灰色區域對應的是每一個列表項設立的外邊距,即Margin值。

RecyclerView的實際可見區域,是由虛線a和虛線b所包圍的區域,即去除了上下內邊距之后的區域。getEndAfterPadding方法返回的值,即是虛線b所在的位置。

圖中的②是對RecyclerView底部不可見區域的透視圖,假定現在position=2的列表項的底部正好貼合到RecyclerView可見區域的底部,則getDecoratedEnd方法返回的值,即是虛線c所在的位置。

接下來,如果按前面的步驟4進行計算,即用虛線c所在的位置減去的虛線b所在的位置,得到的就是圖中的③,即剛好是列表項的外邊距加上分隔線的高度。

這個結果就是待預拉取列表項與RecyclerView可見區域的距離。隨著向上滑動的手勢這個距離值逐漸變小,直到正好進入RecyclerView的可見區域時變為0,隨后開始預加載下一項。

這2項數據收集到之后,就會調用GapWorker的addPosition方法,以交錯的形式存放到一個int數組類型的mPrefetchArray結構中去:

        @Override
        public void addPosition(int layoutPosition, int pixelDistance) {
            ...
            // 根據實際需要分配新的數組,或以2的倍數擴展數組大小
            final int storagePosition = mCount * 2;
            if (mPrefetchArray == null) {
                mPrefetchArray = new int[4];
                Arrays.fill(mPrefetchArray, -1);
            } else if (storagePosition >= mPrefetchArray.length) {
                final int[] oldArray = mPrefetchArray;
                mPrefetchArray = new int[storagePosition * 2];
                System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
            }

            // 交錯存放position值與距離
            mPrefetchArray[storagePosition] = layoutPosition;
            mPrefetchArray[storagePosition + 1] = pixelDistance;

            mCount++;
        }

需要注意的是,RecyclerView每次的預拉取并不限于單個列表項,實際上,它可以一次獲取多個列表項,比如使用了GridLayoutManager的情況

2.1.2 根據預拉取的數據填充任務列表

    private void buildTaskList() {
        ...
        // 2.根據預拉取的數據填充任務列表
        int totalTaskIndex = 0;
        for (int i = 0; i < viewCount; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            ...
            LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
            final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
                    + Math.abs(prefetchRegistry.mPrefetchDy);
            // 以2為偏移量進行遍歷,從mPrefetchArray中分別取出前面存儲的position值與距離        
            for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
                final Task task;
                if (totalTaskIndex >= mTasks.size()) {
                    task = new Task();
                    mTasks.add(task);
                } else {
                    task = mTasks.get(totalTaskIndex);
                }
                final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
                
                // 與RecyclerView可見區域的距離小于滑動的速度,該列表項必定可見,任務需要立即執行
                task.immediate = distanceToItem <= viewVelocity;
                task.viewVelocity = viewVelocity;
                task.distanceToItem = distanceToItem;
                task.view = view;
                task.position = prefetchRegistry.mPrefetchArray[j];

                totalTaskIndex++;
            }
        }
        ...
    }

Task是負責存儲預拉取任務數據的實體類,其所包含屬性的含義分別是:

  • position:待預加載項的Position值
  • distanceToItem:待預加載項與RecyclerView可見區域的距離
  • viewVelocity:RecyclerView的滑動速度,其實就是滑動距離
  • immediate:是否立即執行,判斷依據是與RecyclerView可見區域的距離小于滑動的速度
  • view:RecyclerView本身

從第2個for循環可以看到,其是以2為偏移量進行遍歷,從mPrefetchArray中分別取出前面存儲的position值與距離的

2.1.3 對任務列表進行優先級排序

填充任務列表完畢后,還要依據實際情況對任務進行優先級排序,其遵循的基本原則就是:越可能快進入RecyclerView可見區域的列表項,其預加載的優先級越高

    private void buildTaskList() {
        ...
        // 3.對任務列表進行優先級排序
        Collections.sort(mTasks, sTaskComparator);
    }
   static Comparator<Task> sTaskComparator = new Comparator<Task>() {
        @Override
        public int compare(Task lhs, Task rhs) {
            // 首先,優先處理未清除的任務
            if ((lhs.view == null) != (rhs.view == null)) {
                return lhs.view == null ? 1 : -1;
            }

            // 然后考慮需要立即執行的任務
            if (lhs.immediate != rhs.immediate) {
                return lhs.immediate ? -1 : 1;
            }

            // 然后考慮滑動速度更快的
            int deltaViewVelocity = rhs.viewVelocity - lhs.viewVelocity;
            if (deltaViewVelocity != 0) return deltaViewVelocity;

            // 最后考慮與RecyclerView可見區域距離最短的
            int deltaDistanceToItem = lhs.distanceToItem - rhs.distanceToItem;
            if (deltaDistanceToItem != 0) return deltaDistanceToItem;

            return 0;
        }
    };

2.2 調度預拉取任務

    void prefetch(long deadlineNs) {
        ...
        flushTasksWithDeadline(deadlineNs);
    }

預拉取的第二個動作,則是將前面填充并排序好的任務列表依次調度執行:

    private void flushTasksWithDeadline(long deadlineNs) {
        for (int i = 0; i < mTasks.size(); i++) {
            final Task task = mTasks.get(i);
            if (task.view == null) {
                break; // 任務已完成
            }
            flushTaskWithDeadline(task, deadlineNs);
            task.clear();
        }
    }
    private void flushTaskWithDeadline(Task task, long deadlineNs) {
        long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
        RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
                task.position, taskDeadlineNs);
        ...
    }

2.2.1 嘗試根據position獲取ViewHolder對象

進入prefetchPositionWithDeadline方法后,我們終于再次見到了上一篇的老朋友——Recycler,以及熟悉的成員方法tryGetViewHolderForPositionByDeadline

    private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
            int position, long deadlineNs) {
        ...
        RecyclerView.Recycler recycler = view.mRecycler;
        RecyclerView.ViewHolder holder;
        try {
            ...
            holder = recycler.tryGetViewHolderForPositionByDeadline(
                    position, false, deadlineNs);
        ...
    }


這個方法我們在上一篇文章有介紹過,作用是嘗試根據position獲取指定的ViewHolder對象,如果從緩存中查找不到,就會重新創建并綁定。

2.2.2 根據綁定成功與否添加到mCacheViews或RecyclerViewPool

    private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
            int position, long deadlineNs) {
        ...
            if (holder != null) {
                if (holder.isBound() && !holder.isInvalid()) {
                    // 如果綁定成功,則將該視圖進入緩存
                    recycler.recycleView(holder.itemView);
                } else {
                    //沒有綁定,所以我們不能緩存視圖,但它會保留在池中直到下一次預取/遍歷。
                    recycler.addViewHolderToRecycledViewPool(holder, false);
                }
            }
        ...
        return holder;
    }

接下來,如果順利地獲取到了ViewHolder對象,且該ViewHolder對象已經完成數據的綁定,則下一步就該立即回收該ViewHolder對象,緩存到mCacheViews結構中以供重用。

而如果該ViewHolder對象還未完成數據的綁定,意味著我們沒能在設定的最后期限之前完成預拉取的操作,列表項數據不完整,因而我們不能將其緩存到mCacheViews結構中,但它會保留在mRecyclerViewPool結構中,以供下一次預拉取或重用。

預拉取機制與緩存復用機制的怎么協作的?

既然是與緩存復用機制共用相同的緩存結構,那么勢必會對緩存復用機制的流程產生一定的影響,同樣,讓我們用幾張流程示意圖來演示一下:

  1. 假定現在position=5的列表項的底部正好貼合到RecyclerView可見區域的底部,即還要滑動超過該列表項的外邊距+分隔線高度的距離,下一個列表項才可見。

  2. 隨著向上拖動的手勢,GapWorker開始發起預加載的工作,根據前面梳理的流程,它會提前創建并綁定position=6的列表項的ViewHolder對象,并將其緩存到mCacheViews結構中去。

  1. 繼續保持向上拖動,當position=6的列表項即將進入屏幕時,它會按照上一篇緩存復用機制的流程,從mCacheViews結構取出可復用的ViewHolder對象,無需再次經歷創建和綁定的過程,因此滑動的流暢度有了提升。
  1. 同時,隨著position=6的列表項進入屏幕,GapWorker也開始了對position=7的列表項的預加載
  1. 之后,隨著拖動距離的增大,position=0的列表項也將被移出屏幕,添加到mCachedViews結構中去。
a12.png

上一篇文章我們講過,mCachedViews結構的默認大小限制為2,考慮上以LinearLayoutManager為布局管理器的預拉取的情況的話則還要+1,也即總共能緩存兩個被移出屏幕的可復用ViewHolder對象+一個待進入屏幕的預拉取ViewHolder對象

不知道你們注意到沒有,在步驟5的示意圖中,可復用ViewHolder對象是添加到預拉取ViewHolder對象前面的,之所以這樣子畫是遵循了源碼中的實現:

    // 添加之前,先移除最老的一個ViewHolder對象
    int cachedViewSize = mCachedViews.size();
    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {   // 當前已經放滿
        recycleCachedViewAt(0); // 移除mCachedView結構中的第1個
        cachedViewSize--;   // 總數減1
    }

    // 默認從尾部添加
    int targetCacheIndex = cachedViewSize;
    // 處理預拉取的情況
    if (ALLOW_THREAD_GAP_WORK
            && cachedViewSize > 0
            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
        // 從最后一個開始,跳過所有最近預拉取的對象排在其前面
        int cacheIndex = cachedViewSize - 1;
        while (cacheIndex >= 0) {
            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
            // 添加到最近一個非預拉取的對象后面
            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                break;
            }
            cacheIndex--;
        }
        targetCacheIndex = cacheIndex + 1;
    }
    mCachedViews.add(targetCacheIndex, holder);

也就是說,雖然緩存復用的對象和預拉取的對象共用同一個mCachedViews結構,但二者是分組存放的,且緩存復用的對象是排在預拉取的對象前面的。這么說或許還是很難理解,我們用幾張示意圖來演示一下就懂了:

1.假定現在mCachedViews中同時有2種類型的ViewHolder對象,黑色的代表緩存復用的對象,白色的代表預拉取的對象;

2.現在,有另外一個緩存復用的對象想要放到mCachedViews中,按源碼的做法,默認會從尾部添加,即targetCacheIndex = 3:

a13.png

3.隨后,需要進一步確認放入的位置,它會從尾部開始逐個遍歷,判斷是否是預拉取的ViewHolder對象,判斷的依據是該ViewHolder對象的position值是否存在mPrefetchArray結構中:

    boolean lastPrefetchIncludedPosition(int position) {
        if (mPrefetchArray != null) {
            final int count = mCount * 2;
            for (int i = 0; i < count; i += 2) {
                if (mPrefetchArray[i] == position) return true;
            }
        }
        return false;
    }

a13-1.png

4.如果是,則跳過這一項繼續遍歷,直到找到最近一個非預拉取的對象,將該對象的索引+1,即targetCacheIndex = cacheIndex + 1,得到確認放入的位置。

5.雖然二者是分組存放的,但二者內部仍是有序的,即按照加入的順序正序排列。

開啟預拉取機制后的實際效果如何?

最后,我們還剩下一個問題,即預拉取機制啟用之后,對于RecyclerView的滑動展示究竟能有多大的性能提升?

關于這個問題,已經有人做過相關的測試驗證,這里就不再大量貼圖了,只概括一下其方案的整體思路:

  • 測量工具:開發者模式-GPU渲染模式


    • 該工具以滾動顯示的直方圖形式,直觀地呈現渲染出界面窗口幀所需花費的時間
    • 水平軸上的每個豎條即代表一個幀,其高度則表示渲染該幀所花的時間。
    • 綠線表示的是16.67毫秒的基準線。若想維持每秒60幀的正常繪制,則需保證代表每個幀的豎條維持在此線以下。
  • 耗時模擬:在onBindViewHolder方法中,使用Thread.sleep(time)來模擬頁面渲染的復雜度。復雜度的大小,通過time時間的長短來體現。時間越長,復雜度越高。
  • 測試結果:對比同一復雜度下的RecyclerView滑動,未啟用預拉取機制的一側流暢度明顯更低,并且隨著復雜度的增加,在16ms內無法完成渲染的幀數進一步增多,延時更長,滑動卡頓更明顯。


最后總結一下:

預加載機制
概念 利用UI線程正好處于空閑狀態的時機,預先拉取一部分列表項視圖并緩存起來,從而減少因視圖創建或數據綁定等耗時操作所引起的卡頓。
重要類 GapWorker:綜合滑動方向、滑動速度、與可見區域的距離等要素,構建并調度預拉取任務列表。
Recycler:獲取ViewHolder對象,如果緩存中找不到,則重新創建并綁定
結構 mCachedViews:順利獲取到了ViewHolder對象,且已完成數據的綁定時放入
mRecyclerPool:順利獲取到了ViewHolder對象,但還未完成數據的綁定時放入
發起時機 被拖動(Drag)、慣性滑動(Fling)、嵌套滾動時
完成期限 下一個垂直同步信號發出之前
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,663評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,125評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,506評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,614評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,402評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,934評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,021評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,168評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,690評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,596評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,288評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,027評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,404評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,662評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,398評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,743評論 2 370

推薦閱讀更多精彩內容