看我这篇没人比你更懂RecyclerView的预加载

news2024/12/27 2:04:41

实际上,预拉取(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)时

    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结构中去。

上一篇文章我们讲过,mCachedViews结构的默认大小限制为2,从这里就可以看出,其这样设计是想刚好能缓存一个被移出屏幕的可复用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:

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

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)、嵌套滚动时
完成期限下一个垂直同步信号发出之前

Android 知识点归整

Android 性能调优系列https://0a.fit/dNHYY
Android 车载学习指南https://0a.fit/jdVoy
Android Framework核心知识点笔记https://0a.fit/acnLL
Android 音视频学习笔记https://0a.fit/BzPVh
Jetpack全家桶(含Compose)https://0a.fit/GQJSl
Kotlin 入门到精进https://0a.fit/kdfWR
Flutter 基础到进阶实战https://0a.fit/xvcHV
Android 八大知识体系https://0a.fit/mieWJ
Android 中高级面试题锦https://0a.fit/YXwVq

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/124131.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【面试题】请你谈谈MySQL性能调优的方法

【面试题】请你谈谈MySQL性能调优的方法 这个问题是一个开放性问题&#xff0c;本人这一段时间参加面试&#xff08;2022.12.26&#xff09;经常被问道...... 刚刚开始我回答的很混乱&#xff01;虽然真的知道MySQL性能调优的方法&#xff0c;也做过类似的工作&#xff0c;但…

【BF算法】

BF 算法 BF 算法精讲 在学习到字符串的匹配问题时&#xff0c;了解到了BF算法和KMP算法。 对比这两个算法&#xff0c;先了解BF算法&#xff1b; 字符串匹配问题&#xff0c;比如说&#xff1a;有一个主串 “abbbcdef” &#xff0c; 子串 “bbc”&#xff0c;该问题就是在主…

Linux基础 - DNS服务基础

‍‍&#x1f3e1;博客主页&#xff1a; Passerby_Wang的博客_CSDN博客-系统运维,云计算,Linux基础领域博主 &#x1f310;所属专栏&#xff1a;『Linux基础』 &#x1f30c;上期文章&#xff1a; Linux基础 - Web服务基础 &#x1f4f0;如觉得博主文章写的不错或对你有所帮助…

共享内存和消息队列

共享内存 共享内存指 (shared memory)在多处理器的计算机系统中&#xff0c;可以被不同中央处理器(CPU)访问的大容量内存。由于多个CPU需要快速访问存储器&#xff0c;这样就要对存储器进行缓存(Cache)。任何一个缓存的数据被更新后&#xff0c;由于其他处理器也可能要存取&am…

某程序员哀叹:有比我更惨的吗?工作6年攒了200万,高位接盘买了600万的房子,现在房子跌了100多万,每个月还要还2万房贷!...

最近这几年&#xff0c;“人间惨事”层出不穷&#xff0c;许多网友都在网上比惨&#xff0c;今天的故事主角是一位程序员。这位程序员哀叹&#xff1a;有比我更惨的吗&#xff1f;工作6年攒了200多万&#xff0c;凑了300万首付&#xff0c;在杭州未来科技城高位接盘买了600万的…

JavaScript普通函数与箭头函数有怎样的区别?

比较点 普通函数 箭头函数 具体案例 简写 / 箭头函数如果没有参数&#xff0c;同时函数体的返回值只有一句&#xff0c;则{}和return都可以省略。 1、函数简写 this指向 this总是指向调用它的对象&#xff0c;如果作为构造函数&#xff0c;它指向创建的对象实例 箭头…

【MySQL】深入理解数据库事务

文章目录优秀借鉴1、事务由来2、何为ACID2.1、Atomicity原子性2.2、Consistency一致性2.3、Isolation隔离性2.4、Durability持久性3、聊回事务3.1、概念3.2、语法3.2.1、开启事务3.2.2、提交事务3.2.3、回滚事务4、隔离级别4.1、引入4.2、并发问题4.2.1、脏读4.2.2、不可重复读…

Spring Cloud alibaba 使用Nacos配置中心

依赖管理 Spring Cloud Alibaba BOM 包含了它所使用的所有依赖的版本 请将下面的 BOM 添加到 pom.xml 中的 部分。 这将允许我们省略任何Maven依赖项的版本&#xff0c;而是将版本控制委派给BOM。 <dependencyManagement><dependencies><dependency><gr…

2022年底了,你们公司还好吗?我这里不太好

以下这些也是和几个朋友聊天的时候慢慢聊出来的&#xff0c;不一定真实啊&#xff0c;当做大家开发累了以后的一点调味剂吧 一、宇宙厂 1.宇宙人员成本优化计划&#xff0c;随着各个业务确认了优化目标&#xff0c;将在接下来陆续开展。 某中台确认了指标&#xff0c;将在“在职…

力扣刷题记录——121买卖股票的最佳时机 和125. 验证回文串

本专栏主要记录力扣的刷题记录&#xff0c;备战蓝桥杯&#xff0c;供复盘和优化算法使用&#xff0c;也希望给大家带来帮助&#xff0c;博主是算法小白&#xff0c;希望各位大佬不要见笑&#xff0c;今天要分享的是——《121.买卖股票的最佳时机和125. 验证回文串》。 目录 12…

QT学习 控件(一):按钮类

文章目录Qt控件&#xff1a;按钮QPushButtonQToolButtonQCommandLinkButtonQRadioButtonQCheckBoxQButtonGroupQt控件&#xff1a;按钮 QAbstractButton的信号&#xff1a; void clicked(bool checked false) &#xff1a; 是否选中按钮void pressed()&#xff1a; 点击按钮v…

嵌入式C语言面向对象编程 --- 继承

上一篇文章主要讲述了 C 语言面向对象编程 – 封装的简单概念和实现,本篇文章继续来讨论一下,如何使用 C 语言实现面向对象编程的另一个重要特性:继承。 继承就是基于一个已有的类(一般称作父类或基类),再去重新声明或创建一个新的类,这个类可以称为子类或派生类。子类…

ES文件浏览器 如何提取盒子已安装(内置)软件APK 教程

ES文件浏览器( ES File Explorer)是一款功能强大免费的本地和网络文件管理器。 主要功能&#xff1a;文件管理&#xff1a;多种视图列表和排序方式&#xff0c;查看并打开各类文件&#xff0c;在本地SD卡、局域网、OTG设备之间任意传输文件。多选、复制、粘帖、剪贴板、查看属性…

海豚dolphinscheduler 通过shell 调用.sql文件 传参

1. 准备sql文件 1.1 资源中心--创建文件 1.2 文件格式选择 sql, 文件内容 填要执行的sql内容 1.3 点击创建保存 2.shell调用.sql文件 2.1 拖拽一个shell 节点 2.2 编辑shell节点 hive -e&#xff1a;后面跟hivesql字符串 例如&#xff1a;hive -e "select * from studen…

【十天成为红帽工程师】第七天 Ansible的模块使用

目录 一、ansible的配置文件和清单文件 二、ansible的模块 三、实际操作命令 一、ansible的配置文件和清单文件 &#xff08;一&#xff09;看ansible的配置文件 1、命令&#xff1a;ansible --version 2、一般的配置文件是&#xff1a;/etc/ansible/ansible.cfg PS&am…

查看磁盘分区

在Window上查看磁盘分区&#xff0c;既可以使用diskpart list vol命令&#xff0c;也可以使用diskmgmt.msc命令&#xff0c;下面分别介绍这2种命令查看方式。 1、diskpart方式 按WinR --> 输入: cmd --> diskpart --> list vol&#xff0c;如下图所示: ## 按WinR, …

借助 Material Design,帮助您打造更好的无障碍应用 (中篇)

随着时代的发展&#xff0c;"无障碍体验" 对开发者的意义也愈发重大&#xff0c;在上一篇文章中&#xff0c;我们为您介绍了辅助技术&#xff0c;层次结构&#xff0c;颜色和对比度等内容。本文将进一步为您介绍无障碍布局和排版、文案等相关的内容。布局和排版Mater…

【Java面经】一次颇为进阶的面试记录

工作之余又参加了一次面试&#xff0c;对我来说比之前的面试难度都提了一个度&#xff0c;面试官从公司场景引申聊到高并发和Redis的很多问题。 可惜我太菜了回答不上来&#xff0c;只能回答基础的问题。面完就是凉凉的味道。。 Redis相关 Redis的String是怎么实现的&#xff…

盘点项目管理工具DHTMLX Gantt 的常见问题

DHTMLX Gantt是用于跨浏览器和跨平台应用程序的功能齐全的Gantt图表。可满足项目管理应用程序的大部分开发需求&#xff0c;具备完善的甘特图图表库&#xff0c;功能强大&#xff0c;价格便宜&#xff0c;提供丰富而灵活的JavaScript API接口&#xff0c;与各种服务器端技术&am…

vue中集成省市区街四级地址组件

大家好&#xff0c;我是雄雄。 前言 省市区地址大家应该都不陌生吧&#xff0c;网上买个东西&#xff0c;得填地址。中午定个饭&#xff0c;得写地址&#xff1b;叫个货拉拉叫个跑腿&#xff0c;是不是也得写地址。 但是选择地址的时候&#xff0c;不同场景下选择的范围不同&…