RecyclerView:預取

[toc]數組

關聯地址

RecyclerView(一):預取機制緩存

RecyclerView 預取機制

什麼是預取

預取 就是把將要顯示的 ViewHolder 預先放置到緩存中,以優化 RecyclerView 滑動流暢度。預取 功能是在 Android Version 21 以後加入的。markdown

源碼分析

GapWorkerRecyclerView 實現預取主要涉及到的類,GapWorker 初始化的位置在 RecyclerView.onAttachedToWindow() 中:app

private static final boolean ALLOW_THREAD_GAP_WORK = Build.VERSION.SDK_INT >= 21;
@Override
protected void onAttachedToWindow() {
    super.onAttachedToWindow();
    //對當前版本進行判斷,
    if (ALLOW_THREAD_GAP_WORK) {
        // Register with gap worker
        //這裏利用的是 ThreadLocal 的特性,這也說明主線程中就一個 GapWorker 實例對象 
        mGapWorker = GapWorker.sGapWorker.get();
        if (mGapWorker == null) {
            mGapWorker = new GapWorker();

            // break 60 fps assumption if data from display appears valid
            // NOTE: we only do this query once, statically, because it's very expensive (> 1ms)
            Display display = ViewCompat.getDisplay(this);
            float refreshRate = 60.0f;
            if (!isInEditMode() && display != null) {
                float displayRefreshRate = display.getRefreshRate();
                if (displayRefreshRate >= 30.0f) {
                    refreshRate = displayRefreshRate;
                }
            }
            //計算繪製一幀所需時間,單位是納秒 (ns),1秒 = 10億納秒
            mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
            GapWorker.sGapWorker.set(mGapWorker);
        }
        //將 RecyclerView 添加到 mRecyclerView 集合中
        mGapWorker.add(this);
    }
}
複製代碼

onAttachedToWindow() 方法中初始化了 GapWorker 對象時也賦值給 mFrameIntervalNs 變量。mFrameIntervalNs 的做用是防止預取消耗的時間過長反而影響性能。它會在以後被用到。ide

GapWorker 是如何發揮做用的呢,經過搜索能夠看到 GapWorkerRecyclerView 中,被調用的方法除 add()、remove() 外還有一個就是 postFromTraversal() 方法。能夠看到它調用的位置都與滑動相關。在 onTouchEventMOVE 事件中能夠看到有以下代碼:oop

@Override
public boolean onTouchEvent(MotionEvent e) {
    
    switch (action) {
        case MotionEvent.ACTION_MOVE: {               
            int dx = mLastTouchX - x;
            int dy = mLastTouchY - y;

            if (mScrollState == SCROLL_STATE_DRAGGING) {

                if (scrollByInternal(
                        canScrollHorizontally ? dx : 0,
                        canScrollVertically ? dy : 0,
                        vtev)) {
                    getParent().requestDisallowInterceptTouchEvent(true);
                }
                if (mGapWorker != null && (dx != 0 || dy != 0)) {
                    mGapWorker.postFromTraversal(this, dx, dy);
                }
            }
        } break;

    return true;
}
複製代碼

RecyclerView 發生滑動事件時會執行 scrollByInternal()postFromTraversal() 方法,在 scrollByInternal() 中調用 invalidate() 方法觸發控件樹刷新,而 postFromTraversal() 調用了 View.post(Runnable) 方法,瞭解控件樹刷新機制的同窗應該清楚這樣就會在下一次控件樹刷新時執行 Runnable 參數的 Run() 方法了。源碼分析

void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
    //爲true
    if (recyclerView.isAttachedToWindow()) {
    //mRecyclerViews中包含的是已經綁定到Window上的全部RecyclerView,不止是當前處在前臺的activity
    //能夠這麼理解,只要activity中含有RecyclerView,而且沒有被銷燬,那麼這個RecyclerView就會被添加到mRecyclerViews中
        if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
            throw new IllegalStateException("attempting to post unregistered view!");
        }
        if (mPostTimeNs == 0) {
            mPostTimeNs = recyclerView.getNanoTime();
            //預取的邏輯是經過這裏處理的,GapWorker實現了Runnable接口
            recyclerView.post(this);
        }
    }
    //這裏只是將這兩值傳遞進去,就是賦值而已
    recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}
複製代碼

GapWorker 自己就實現了 Runnable 接口,下面就來看 run() 方法中作了什麼操做:post

@Override
public void run() {
    try {
        TraceCompat.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

        if (mRecyclerViews.isEmpty()) {
            // abort - no work to do
            return;
        }

        // Query most recent vsync so we can predict next one. Note that drawing time not yet
        // valid in animation/input callbacks, so query it here to be safe.
        final int size = mRecyclerViews.size();
        long latestFrameVsyncMs = 0;
        //遍歷全部保存的 RecyclerView,查找處於可見狀態的view並獲取最近上一幀開始的時間
        for (int i = 0; i < size; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            if (view.getWindowVisibility() == View.VISIBLE) {
                latestFrameVsyncMs = Math.max(view.getDrawingTime(), latestFrameVsyncMs);
            }
        }

        if (latestFrameVsyncMs == 0) {
            // abort - either no views visible, or couldn't get last vsync for estimating next
            return;
        }
        //計算下一幀到來的時間,在這個時間內沒有預取到那麼就會預取失敗,預取的本意就是爲了滑動更流暢,若是預取在
        //下一幀到來時還沒取到,還去取的話那麼就會影響到繪製,得不償失,
        long nextFrameNs = TimeUnit.MILLISECONDS.toNanos(latestFrameVsyncMs) + mFrameIntervalNs;
        //看名字就知道這裏是去預取了
        prefetch(nextFrameNs);

        // TODO: consider rescheduling self, if there's more work to do
    } finally {
        mPostTimeNs = 0;
        TraceCompat.endSection();
    }
}

void prefetch(long deadlineNs) {
    buildTaskList();
    flushTasksWithDeadline(deadlineNs);
}
複製代碼

run() 方法中經過計算得到了要刷新下一幀的時間,根據此時間防止在 CreateViewHolderBindViewHolder 時耗時過多。一旦預取超時則預取失敗。性能

至此準備工做都作好了,接下來執行 perfetch() 中預取的具體邏輯,它調用了buildTaskList()flushTasksWithDeadline(long) 這兩個方法。先來看 buildTaskList()fetch

private void buildTaskList() {
    // Update PrefetchRegistry in each view
    final int viewCount = mRecyclerViews.size();
    int totalTaskCount = 0;
    //計算有多少個可見的RecyclerView
    for (int i = 0; i < viewCount; i++) {
        RecyclerView view = mRecyclerViews.get(i);
        if (view.getWindowVisibility() == View.VISIBLE) {
        //計算須要預取條目的位置,最終會調用到 addPosition() 方法,將位置信息和偏移量保存到mPrefetchArray數組中
            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
            totalTaskCount += view.mPrefetchRegistry.mCount;
        }
    }

    // Populate task list from prefetch data...
    mTasks.ensureCapacity(totalTaskCount);
    int totalTaskIndex = 0;
    for (int i = 0; i < viewCount; i++) {
        RecyclerView view = mRecyclerViews.get(i);
        if (view.getWindowVisibility() != View.VISIBLE) {
            // Invisible view, don't bother prefetching
            continue;
        }

        LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
        final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
                + Math.abs(prefetchRegistry.mPrefetchDy);
         //建立預取條目的task
        //mCount 是當前 ViewHolder 須要預取的個數,這裏*2是由於 mPrefetchArray 數組不只保存了位置,還保存了到預取 ViewHolder 到窗口的距離
        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);
            }
            //預取 ViewHolder 和窗口的距離
            final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
            //表示這個預取的 ViewHolder 在下一幀是否會顯示,當滑動距離大於等於 distanceToItem 說明本次滑動後此 ViewHolder 將出如今屏幕上
            task.immediate = distanceToItem <= viewVelocity;
            //滑動的距離
            task.viewVelocity = viewVelocity;
            //...
            task.distanceToItem = distanceToItem;
            //預取item的RecyclerView
            task.view = view;
            //預取item所處的位置(position)
            task.position = prefetchRegistry.mPrefetchArray[j];
            //預取的總個數
            totalTaskIndex++;
        }
    }

    // ... and priority sort
    //對須要預取的 task 按照優先級進行排序,immediate = true的將會排在前面,這是由於immediate =true的將會在下一幀顯示
    Collections.sort(mTasks, sTaskComparator);
}
複製代碼

buildTaskList() 方法的主要做用是填充預取任務 Task 集合。能夠看到方法內有嵌套 for 循環,表明集合的填充數量由兩方面決定:

  • 當前處於可見狀態的 RecyclerView 數量。
  • RecyclerView.LayoutManager.LayoutPrefetchRegistry 接口的實現定義 RecyclerView 每次預取 ViewHolder 的數量。

雖然咱們能夠本身實現 LayoutPrefetchRegistry 接口來決定每次預取數量,可是這牽扯到設備性能用戶用戶操做習慣等一些問題,因此最好仍是用 GapWorker 中提供的默認 LayoutPrefetchRegistry 實現比較方便。

static class LayoutPrefetchRegistryImpl
        implements RecyclerView.LayoutManager.LayoutPrefetchRegistry {

    // 預取 ViewHolder 信息
    int[] mPrefetchArray;

    // 預取 ViewHolder 數
    int mCount;

    /**
     * 收集預取項
     * @ param view RecyclerView
     * @ nested 是不是嵌套 RecyclerView
     */
    void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
        mCount = 0;
        if (mPrefetchArray != null) {
            Arrays.fill(mPrefetchArray, -1);
        }

        // 調用 LayoutManager 中的預取和嵌套預取的實現來收集預取項信息
        final RecyclerView.LayoutManager layout = view.mLayout;
        if (view.mAdapter != null
                && layout != null
                && layout.isItemPrefetchEnabled()) {
            if (nested) {
                // nested prefetch, only if no adapter updates pending. Note: we don't query
                // view.hasPendingAdapterUpdates(), as first layout may not have occurred
                if (!view.mAdapterHelper.hasPendingUpdates()) {
                    layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
                }
            } else {
                // momentum based prefetch, only if we trust current child/adapter state
                if (!view.hasPendingAdapterUpdates()) {
                    layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                            view.mState, this);
                }
            }

            // 更新 RecyclerView 中 mCacheViews 緩存集合的大小
            if (mCount > layout.mPrefetchMaxCountObserved) {
                layout.mPrefetchMaxCountObserved = mCount;
                layout.mPrefetchMaxObservedInInitialPrefetch = nested;
                view.mRecycler.updateViewCacheSize();
            }
        }
    }

    /**
     * 收集預取項信息,其中每兩個元素表明一個 ViewHolder 的信息,第一位表明 ViewHolder 在 RecyclerView 中的位置,第二位表明其到窗口的距離
     * @ param view RecyclerView
     * @ nested 是不是嵌套 RecyclerView
     */
    @Override
    public void addPosition(int layoutPosition, int pixelDistance) {
    	...
        // add position
        mPrefetchArray[storagePosition] = layoutPosition;
        mPrefetchArray[storagePosition + 1] = pixelDistance;

        mCount++;
    }
}
複製代碼

經過 buildTaskList() 方法以後咱們就獲得了須要預取的 Task 任務集合了,那 Task 究竟是什麼呢?每個 Task 都表明一個預取任務,其內保存了預取任務執行所需的各類信息。

static class Task {
    //表示這個預取 ViewHolder 在下一幀是否會顯示,一般爲false,表示在下一幀不顯示,爲true就說明在下一幀是會顯示的
    public boolean immediate;
    //滑動的距離
    public int viewVelocity;
    //預取 ViewHolder 和窗口的距離
    public int distanceToItem;
    //對應的 RecyclerView
    public RecyclerView view;
    //預取 ViewHolder 所處的位置
    public int position;
}
複製代碼

那下面來看 flushTasksWithDeadline(long) 方法:

private void flushTasksWithDeadline(long deadlineNs) {
    //全部的task,預取出相應的view,而後清空task
    for (int i = 0; i < mTasks.size(); i++) {
        final Task task = mTasks.get(i);
        if (task.view == null) {
            break; // done with populated tasks
        }
        //這裏就是去取task
        flushTaskWithDeadline(task, deadlineNs);
        task.clear();
    }
}
複製代碼

遍歷任務列表,交給 flushTaskWithDeadline(Task, long) 去執行預取,執行結束後清理 Task

/**
 * @param task 預取任務
 * @param deadlineNs 下一幀刷新的時間
 */
private void flushTaskWithDeadline(Task task, long deadlineNs) {
    long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
    RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
            task.position, taskDeadlineNs);
    if (holder != null
            && holder.mNestedRecyclerView != null
            && holder.isBound()
            && !holder.isInvalid()) {
        prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
    }
}
複製代碼

看名字就知道 prefetchPositionWithDeadline() 是去預取的,而後返回的就是 ViewHolderViewHolder != null 就說明預取成功了。下面還一個判斷執行,這個判斷的做用是預取的這個 view 是不是 RecyclerView,若是是就會執行嵌套預取邏輯。來看 prefetchPositionWithDeadline() 方法:

private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
        int position, long deadlineNs) {
    if (isPrefetchPositionAttached(view, position)) {
        // don't attempt to prefetch attached views
        return null;
    }
	//這裏先拿到 RecyclerView 的緩存對象
    RecyclerView.Recycler recycler = view.mRecycler;
    RecyclerView.ViewHolder holder;
    try {
        view.onEnterLayoutOrScroll();
        //這裏就是去緩存中獲取或是新建立一個,這裏先不講,以後分析 RecyclerView 緩存實現的時候會說到
        holder = recycler.tryGetViewHolderForPositionByDeadline(
                position, false, deadlineNs);

        if (holder != null) {
            if (holder.isBound() && !holder.isInvalid()) {
                // Only give the view a chance to go into the cache if binding succeeded
                // Note that we must use public method, since item may need cleanup
                //通常會執行到這裏,這裏是將獲取的 ViewHolder 添加到 mCachedViews 緩存中
                recycler.recycleView(holder.itemView);
            } else {
                // Didn't bind, so we can't cache the view, but it will stay in the pool until
                // next prefetch/traversal. If a View fails to bind, it means we didn't have
                // enough time prior to the deadline (and won't for other instances of this
                // type, during this GapWorker prefetch pass).
                //將holder添加到第四級緩存mRecyclerPool中
                recycler.addViewHolderToRecycledViewPool(holder, false);
            }
        }
    } finally {
        view.onExitLayoutOrScroll(false);
    }
    return holder;
}
複製代碼

看到經過 tryGetViewHolderForPositionByDeadline() 方法最後獲取到了目標 ViewHolder,並將其存放到緩存中。這個方法會在 RecyclerView 緩存中具體說明,大體的流程是,首先在各個緩存集合中尋找目標 ViewHolder,若未找到則調用 Adapter.createViewHolder() 方法新建 ViewHolder,再判斷此 ViewHolder 是否須要綁定數據,若須要則調用 tryBindViewHolderByDeadline() 方法綁定數據,而後將 ViewHolder 返回。

ViewHolder tryGetViewHolderForPositionByDeadline(int position, boolean dryRun, long deadlineNs) {
    ...
    if (holder == null) {
        long start = getNanoTime();
        if (deadlineNs != FOREVER_NS
                && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
            // abort - we have a deadline we can't meet
            return null;
        }
        holder = mAdapter.createViewHolder(RecyclerView.this, type);
    }
}
複製代碼

這裏在調用 createViewHolder()、tryBindViewHolderByDeadline() 以前都會判斷調用方法消耗的事件是否會超過下一幀刷新的時間,若耗時超過則返回 null,以防影響流暢度。

總結來講,RecyclerView 預取功能是經過判斷用戶的滑動預判即將加載的 ViewHolder 將其提早放置在緩存中以達到優化滑動體驗的功能。

相關文章
相關標籤/搜索