RecyclerView(一):預取機制

什麼是預取

預取就是界面沒有展示出來的元素,是下一個即將要展示出來的元素,比如界面展示的是10條目,那麼當你滑動的時候,那麼即將展示的將是第11個(還沒顯示出來),這時候RecyclerView就會提前將需要展示的第一個元素緩存到mCachedViews中(RecyclerView四級緩存中的第二級緩存,後面會單獨提取一篇講緩存),mCachedViews中緩存的view是不要重新綁定的,也就說不會執行adapter的onBindViewHolder()方法,這個功能是在版本21之後加的。

預取原理實現

預取原理實現主要涉及到的是GapWorker這個類,先來看下這個類的初始化,在RecyclerView的onAttachedToWindow()中:

    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
                mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
                GapWorker.sGapWorker.set(mGapWorker);
            }
            //將RecyclerView添加到RecyclerView中
            mGapWorker.add(this);
        }
    }

整個邏輯還是很清晰,就是初始化一個GapWorker並保存到ThreadLocal中。創建之後,接下來就是對這個類的使用了,預取機制主要是在滑動的時候,那就去RecyclerView的onTouchEvent中MOVE事件中查找了,可以看到其中有這樣一段代碼:

	if (mGapWorker != null && (dx != 0 || dy != 0)) {
           mGapWorker.postFromTraversal(this, dx, dy);
    }

這段代碼是在請求繪製界面之後調用的,也就說,在請求繪製當前幀的時候,會提前緩存下一幀的view,現在就跟着postFromTraversal()這個方法往下走:

    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);
    }

下面就來看它的run方法中做了什麼操作:

    @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);
    }

可以看到prefetch()中有兩個方法,先來看第一個buildTaskList():

    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是當前預取位置的個數(比如當前可見的RecyclerView有兩個,那麼mCount就爲2),這裏*2是因爲保存位置
            //的數組不僅保存了位置,還保存了到預取位置的距離
            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);
                }
                //當前可見item到預取位置的距離
                final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
				//表示這個預取的item在下一幀是否會顯示,通常爲false,表示在下一幀不顯示,爲true就說明在下一幀是會顯示的
                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);
    }

他做的事情可分爲兩點:

  1. 計算需要預取的位置;
  2. 新建需要預取的task,裏面包含了需要預取的相關信息;
    重新回到上面的prefetch()方法,現在來看他的第二個方法flushTasksWithDeadline(long deadlineNs):
    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()去執行:

    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()是去預取view,然後返回的就是ViewHolder,ViewHolder不爲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
                    //一般會執行到這裏,這裏是將獲取的view添加到第二級緩存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;
    }

到這裏就將RecyclerView的預取機制整個的過了一遍,最後在總結一下:

總結

  1. 預取功能是在21及以上版本纔有的,這個功能可以通過LayoutManager的setItemPrefetchEnabled(boolean enabled)去關閉的;
  2. 如果RecyclerView中還有子RecyclerView(並且是用LinearLayoutManager),那麼子RecyclerView可以通過LinearLayoutManager的setInitialPrefetchItemCount()去設置預取的個數;
  3. 預取功能開啓,滑動時,會先去計算下一個需要顯示的item,如果計算到下一個item在下一幀中一定會顯示出來,那麼就一定會取出來,如果下一幀不會顯示,會根據下一幀到來的時間看是否已經取出, 如果沒取出那麼就會預取失敗,取出了就會放到RecyclerView中的第二級緩存中去,第二級緩存是不需要重新綁定的,這樣就可以減少下一幀時間。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章