RecyclerView的预加载实现

说起预加载,其实之前面试的时候被问到的,然后最近看到一篇关于预加载的文章,然后颇有感受,因此才有该篇文章。
在recyclerview的onAttachedToWindow有这么一句:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
 * On L+, with RenderThread, the UI thread has idle time after it has passed a frame off to
 * RenderThread but before the next frame begins. We schedule prefetch work in this window.
 */
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
        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;
                }
            }
            mGapWorker.mFrameIntervalNs = (long) (1000000000 / refreshRate);
            GapWorker.sGapWorker.set(mGapWorker);
        }
        mGapWorker.add(this);
    }
}

可以看到有一个静态变量,ALLOW_THREAD_GAP_WORK是大于等于21(Android5.0)才会为true,这是因为在5.0之后,引进了RenderThread线程,专门用来渲染ui线程绘制好的数据,渲染完后,会提交到事先申请的bufferqueue中,然后当vsync-sf信号来的时候,sufaceflinger会去对应app的bufferqueue中取出前面提交的buffer数据,然后进行合成layer,最终屏幕(hwc)收到该请求后,进行图层进行合成,最终送到屏幕硬件上显示。这就是一针从创建到消费的过程。 由于ui线程把绘制好的数据绘制好后,会通知renderthread线程进行渲染,直到下一针来的时候才开始工作,此时会有主线程空闲的时候,recyclerview正是利用此空闲时间来进行预加载,而如果在下一针来临的时候,预加载还没有完成,那么此时会放弃此次的预加载,了解原理后,开始分析过程: 预加载的处理类是GapWorker类,一个线程对应一个GapWorker,它是存储在ThreadLocal中。GapWorker是一个runnable类,它如何要工作的话,是通过GapWorker.postFromTraversal工作的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
    if (recyclerView.isAttachedToWindow()) {
        if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
            throw new IllegalStateException("attempting to post unregistered view!");
        }
        if (mPostTimeNs == 0) {
            mPostTimeNs = recyclerView.getNanoTime();
            recyclerView.post(this);
        }
    }
    recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
}

可以看出来实际是通过recyclerview.post,然后传的是自己,因此会直接run方法。预加载时机在源码里面有两处调用了:

  • Recyclerview被拖动时:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
    @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惯性滑动时:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class ViewFlinger implements Runnable {
    ...
    @Override
    public void run() {
        ...
            if (!smoothScrollerPending && doneScrolling) {
            ...
            } else {
            ...
                if (mGapWorker != null) {
                    mGapWorker.postFromTraversal(RecyclerView.this, consumedX, consumedY);
                }
            }
    }
    ...
}    

看下GapWorker的run方法如何实现的预加载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@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;
        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();
    }
}

在调用prefetch前传入nextFrameNs,该值表示预估的下一个vsync来临的时间,首先获取上一针的绘制起始时间,也就是latestFrameVsyncMs,而mFrameIntervalNs是通过刷新率算出来的一针需要的时间,比如60hz的手机,一秒是60帧,那么mFrameIntervalNs的值是一针需要16ms,mFrameIntervalNs的单位是纳秒,这个时间在开篇的onAttachedToWindow方法中计算的,所以可以看出来,nextFrameNs时间就是预加载最后的期限时间,超过这个时间就会放弃该预加载,上面提到的上一针时间是用过recyclerview的getDrawingTime获取的,它是获取的attachInfo的mDrawingTime时间,而mDrawingTime是在viewRootImpl中draw方法给赋值的:

1
2
3
4
5
6
private boolean draw(boolean fullRedrawNeeded, boolean forceDraw) {
    //省略代码
    mAttachInfo.mDrawingTime =
            mChoreographer.getFrameTimeNanos() / TimeUtils.NANOS_PER_MS;
    //省略代码        
}

可以看出来,它是通过Choreographer的getFrameTimeNanos方法来获取的:

1
2
3
4
5
@UnsupportedAppUsage
public long getFrameTimeNanos() {
    //省略代码
    return USE_FRAME_TIME ? mLastFrameTimeNanos : System.nanoTime();
}

mLastFrameTimeNanos是在doFrame中将参数frameTimeNanos给赋值的,而frameTimeNanos参数表示的就是当前vsnyc信号来临的时间。所以最终结论就是通过遍历recyclerview的drawingtime,来获取最近一次的vsync时间,并加上当前设备一针所需要的时间,从而得到下一个vsync信号来临的时间,也就是预加载要完成的最晚时间。继续跟进GapWorker的prefetch方法:

1
2
3
4
void prefetch(long deadlineNs) {
    buildTaskList();
    flushTasksWithDeadline(deadlineNs);
}

buildTaskList,它是用来构建预加载的task列表:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void buildTaskList() {
    // Update PrefetchRegistry in each view
    final int viewCount = mRecyclerViews.size();
    int totalTaskCount = 0;
    for (int i = 0; i < viewCount; i++) {
        RecyclerView view = mRecyclerViews.get(i);
        if (view.getWindowVisibility() == View.VISIBLE) {
            //收集要预加载的view的position
            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
            totalTaskCount += view.mPrefetchRegistry.mCount;
        }
    }
    //省略代码
}

首先是调用了view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false)来收集预加载的view:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
    mCount = 0;
    if (mPrefetchArray != null) {
        Arrays.fill(mPrefetchArray, -1);
    }
    final RecyclerView.LayoutManager layout = view.mLayout;
    if (view.mAdapter != null
            && layout != null
            && layout.isItemPrefetchEnabled()) {
        //省略代码
        if (!view.hasPendingAdapterUpdates()) {
            layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                    view.mState, this);
        }
        //省略代码
    }
}

可以看出来调用了layoutmanager的collectAdjacentPrefetchPositions的方法,把需要预加载的水平和竖直方向的偏移量传入其中,看下LinearLayoutManager的该方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
        LayoutPrefetchRegistry layoutPrefetchRegistry) {
    int delta = (mOrientation == HORIZONTAL) ? dx : dy;
    if (getChildCount() == 0 || delta == 0) {
        // can't support this scroll, so don't bother prefetching
        return;
    }
    ensureLayoutState();
    final int layoutDirection = delta > 0 ? LayoutState.LAYOUT_END : LayoutState.LAYOUT_START;
    final int absDelta = Math.abs(delta);
    updateLayoutState(layoutDirection, absDelta, true, state);
    collectPrefetchPositionsForLayoutState(state, mLayoutState, layoutPrefetchRegistry);
}

通过方法来判断偏移量,然后把方向标识传入到updateLayoutState中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void updateLayoutState(int layoutDirection, int requiredSpace,
            boolean canUseExistingSpace, RecyclerView.State state) {
    //省略代码
    if (layoutToEnd) {
        //获取可见的最后一个表项
        final View child = getChildClosestToEnd();
        //判断是否是reverse的
        mLayoutState.mItemDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
        //获取预加载的position
        mLayoutState.mCurrentPosition = getPosition(child) + mLayoutState.mItemDirection;
        mLayoutState.mOffset = mOrientationHelper.getDecoratedEnd(child);
        //获取要预加载的表项与recyclerview底部的距离
        scrollingOffset = mOrientationHelper.getDecoratedEnd(child)
                - mOrientationHelper.getEndAfterPadding();

    } else {
        //省略代码
    }
    //省略代码
    mLayoutState.mScrollingOffset = scrollingOffset;
}

此处只保留从上到下的滑动,先是获取页面滚动的最后一个表项,然后判断是不是reverseLayout,如果不是则取LayoutState.ITEM_DIRECTION_TAIL(该值等于1),预加载的position等于最后一个表项的position+1,接着算出预加载的表项离recyclerview底部的距离。它是通过mOrientationHelper.getDecoratedEnd(child)- mOrientationHelper.getEndAfterPadding()得到的。 mOrientationHelper.getDecoratedEnd(child)它是在OrientationHelper中定义的createVerticalHelper方法中实现的:

1
2
3
4
5
6
@Override
public int getDecoratedEnd(View view) {
    final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams)
            view.getLayoutParams();
    return mLayoutManager.getDecoratedBottom(view) + params.bottomMargin;
}
1
2
3
public int getDecoratedBottom(@NonNull View child) {
    return child.getBottom() + getBottomDecorationHeight(child);
}

getDecoratedBottom是获取child的bottom+child的底部间距高度,所以getDecoratedEnd是获取child的bottom+child的底部间距高度+下间距。 mOrientationHelper.getEndAfterPadding()也是在OrientationHelper中定义的createVerticalHelper方法中实现的:

1
2
3
4
@Override
public int getEndAfterPadding() {
    return mLayoutManager.getHeight() - mLayoutManager.getPaddingBottom();
}

getEndAfterPadding是recyclerview的高度-recyclerview的下内边距。所以scrollingOffset是即将要预加载的表项离recyclerview底部的间距。接着看下collectPrefetchPositionsForLayoutState方法:

1
2
3
4
5
6
7
void collectPrefetchPositionsForLayoutState(RecyclerView.State state, LayoutState layoutState,
        LayoutPrefetchRegistry layoutPrefetchRegistry) {
    final int pos = layoutState.mCurrentPosition;
    if (pos >= 0 && pos < state.getItemCount()) {
        layoutPrefetchRegistry.addPosition(pos, Math.max(0, layoutState.mScrollingOffset));
    }
}

将刚才算的预加载的postion和偏移量传入到addPosition中,该方法是在GapWorker中实现的:

1
2
3
4
5
6
7
8
@Override
public void addPosition(int layoutPosition, int pixelDistance) {
    //省略代码
    final int storagePosition = mCount * 2;
    mPrefetchArray[storagePosition] = layoutPosition;
    mPrefetchArray[storagePosition + 1] = pixelDistance;
    mCount++;
}

将position和偏移量成对保存在mPrefetchArray数组中。构建完数组中,接着就是将数组的数据存到task中。这块逻辑在buildTaskList中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private void buildTaskList() {
    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);
        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];
            task.immediate = distanceToItem <= viewVelocity;
            task.viewVelocity = viewVelocity;
            task.distanceToItem = distanceToItem;
            task.view = view;
            task.position = prefetchRegistry.mPrefetchArray[j];
            totalTaskIndex++;
        }
    }
}

task信息由由以下组成: immediate:表示是否立即执行,判断依据预加载的表项离recyclerview底部距离是否小于滑动的速度 viewVelocity:滑动的速度 distanceToItem:预加载的表项离recyclerview底部距离 view:recyclerview position:预加载表项的位置 从上面可以看出来从mPrefetchArray数组中取值是每两个值取出的,和上面build过程对应。所有的完事后,在buildTaskList中就是对task进行排序:

1
2
3
4
5
private void buildTaskList() {
    ...
    // 3.对任务列表进行优先级排序
    Collections.sort(mTasks, sTaskComparator);
}

上面就是整个buildTaskList的逻辑,接着就是根据构建的task来创建viewholder,该逻辑是在flushTasksWithDeadline方法中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private void flushTasksWithDeadline(long deadlineNs) {
    for (int i = 0; i < mTasks.size(); i++) {
        final Task task = mTasks.get(i);
        if (task.view == null) {
            break; // done with populated tasks
        }
        flushTaskWithDeadline(task, deadlineNs);
        task.clear();
    }
}

flushTaskWithDeadline:

1
2
3
4
5
6
private void flushTaskWithDeadline(Task task, long deadlineNs) {
    long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
    RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
            task.position, taskDeadlineNs);
    //省略代码
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
private RecyclerView.ViewHolder prefetchPositionWithDeadline(RecyclerView view,
        int position, long deadlineNs) {
    RecyclerView.Recycler recycler = view.mRecycler;
    RecyclerView.ViewHolder holder;
    holder = recycler.tryGetViewHolderForPositionByDeadline(
            position, false, deadlineNs);
    if (holder != null) {
        if (holder.isBound() && !holder.isInvalid()) {
            //如果holder已经绑定过并且是可用的,加入到cacheview缓存中
            recycler.recycleView(holder.itemView);
        } else {
            //否则加入到RecycledViewPool中
            recycler.addViewHolderToRecycledViewPool(holder, false);
        }
    }
    return holder;
}

上面通过tryGetViewHolderForPositionByDeadline获取viewholder,和正常获取viewholder的区别是传入了deadlineNs,直接看该方法是如何放弃超时的viewholder:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
                boolean dryRun, long deadlineNs) {
    if (holder == null) {
        long start = getNanoTime();
        if (deadlineNs != FOREVER_NS
                && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
            return null;
        }
    }
}

如果deadlineNs不是FOREVER_NS,普通调用该方法传入的deadlineNs是FOREVER_NS,所以是通过该值区分是不是预加载调用的,接着通过willCreateInTime返回值判断要不要放弃:

1
2
3
4
boolean willCreateInTime(int viewType, long approxCurrentNs, long deadlineNs) {
    long expectedDurationNs = getScrapDataForType(viewType).mCreateRunningAverageNs;
    return expectedDurationNs == 0 || (approxCurrentNs + expectedDurationNs < deadlineNs);
}

expectedDurationNs取的是对应viewholder的平均创建时间,其实也不叫平均时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void factorInCreateTime(int viewType, long createTimeNs) {
    ScrapData scrapData = getScrapDataForType(viewType);
    scrapData.mCreateRunningAverageNs = runningAverage(
            scrapData.mCreateRunningAverageNs, createTimeNs);
}
long runningAverage(long oldAverage, long newValue) {
    if (oldAverage == 0) {
        return newValue;
    }
    return (oldAverage / 4 * 3) + (newValue / 4);
}

每次将之前的创建viewholder时间占3分之4,当前创建的时间占1分之4。所以willCreateInTime的返回值是如果当前时间+平均创建viewholder时间小于最晚约定时间则不会放弃,否则直接放弃。如果不放弃接着会判断bind过程有没有超过约定时间:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private boolean tryBindViewHolderByDeadline(@NonNull ViewHolder holder, int offsetPosition,
        int position, long deadlineNs) {
    final int viewType = holder.getItemViewType();
    long startBindNs = getNanoTime();
    if (deadlineNs != FOREVER_NS
            && !mRecyclerPool.willBindInTime(viewType, startBindNs, deadlineNs)) {
        return false;
    }
    mAdapter.bindViewHolder(holder, offsetPosition);
    long endBindNs = getNanoTime();
    mRecyclerPool.factorInBindTime(holder.getItemViewType(), endBindNs - startBindNs);
    return true;
}

bind过程是通过willBindInTime方法来判断有没有超过约定时间的,整个逻辑很清晰。

前面分析过如果holder是bind过的,则会加入到cacheview缓存中,否则加入到RecycledViewPool中,正常滑动的时候离屏的viewholder也是添加到cacheview缓存中的,那两者在缓存中又是怎么区分的呢?直接看recycler的recycleViewHolderInternal方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void recycleViewHolderInternal(ViewHolder holder) {
    //省略代码
    //默认插入cacheview缓存的索引是末尾
    int targetCacheIndex = cachedViewSize;
    if (ALLOW_THREAD_GAP_WORK
            && cachedViewSize > 0
            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {//如果是正常离屏的viewholder
        //默认指向最后一个元素
        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);
    //省略代码
}

默认插入cacheview缓存的索引是在末尾,在插入正常离屏的viewholder时候如果遇到预拉取的viewholder,则往前找直到最后一个离屏的viewholder,然后插入到它后面。所以不难看出,预拉取的会在后面,离屏的会在前面。这样的好处是预加载的viewholder由于在后面使用的机会会很大,放在集合的后面删除的概率要小。

参考:掌握这17张图,没人比你更懂RecyclerView的预加载

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy