RecyclerView 回收复用机制解析

629 阅读1小时+

想看总结图的直接去末尾。

首先提个问题,RecyclerView的缓存结构中 mAttachedScrap mChangedScrap mCachedViews mRecyclerPoolmAttachedScrap mChangedScrap其实只是暂存区,能否真的算作是缓存?

RecyclerView 不仅仅是一个承载大量数据的 ViewGroup,它还依靠多层缓存机制和各种辅助组件,实现了高效的 View 复用与数据绑定。虽然在 RecyclerView 中,LayoutManager 主要负责测量和布局这些工作,但仅凭这一点,ListView 也能做到。然而,RecyclerView 的优势在于其优化了数据更新、动画效果和整体性能,从而大幅提升了用户体验。

20250320-223628.jpg

其实,LayoutManager的作用远不止于此,他还有:

  • 视图布局管理:LayoutManager 负责对子项(item view)的测量和布局,确定每个 item 在屏幕上的具体位置和尺寸。不同的 LayoutManager(如 LinearLayoutManager、GridLayoutManager、StaggeredGridLayoutManager 等)会提供不同的布局效果。

  • 滚动处理:它管理 RecyclerView 的滚动行为,无论是垂直滚动还是水平滚动。LayoutManager 决定了在滚动过程中如何加载新的视图、如何将不可见的视图回收,以及如何平滑地响应用户的滑动操作。

  • 视图回收与复用:在用户滚动过程中,当某个 item 不再显示在屏幕上时,LayoutManager 会将其回收,以便后续复用。这样可以极大地提高内存和性能效率,因为无需为每个 item 都创建独立的视图。

  • 定制布局效果:通过继承和自定义 LayoutManager,我们可以实现复杂或独特的布局方式,满足特定应用场景的需求。

20250320-140614.jpg


0. ListView的缓存机制

ListView的二级缓存指的就是源码里边的 mActiveViews 与 mScrapViews 。mActiveViews 表示的是屏幕上看到的View,而 mScrapViews 表示已经移除屏幕外的可以复用的 View, 下边我们来聊聊他们的工作原理。

class RecycleBin {
    /**
     * Views that were on screen at the start of layout. This array is populated at the start of
     * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
     * Views in mActiveViews represent a contiguous range of Views, with position of the first
     * view store in mFirstActivePosition.
     */
    private View[] mActiveViews = new View[0];

    /**
     * Unsorted views that can be used by the adapter as a convert view.
     */
    private ArrayList<View>[] mScrapViews;

mActiveViews 的作用非常简单,当 ListView 的数据并没有发生改变的时候,这个时候 ListView 又进行了重新布局,既然数据没有改变,那么 ListView 中的子元素内容也不会改变,此时将子元素在 mActiveViews 一存一取,达到快速复用的效果。这个时候的复用效率极高,ListView 不需要 调用 create bind 方法,触发这个流程并不是使用 notifyItemChanged 方法,这个方法一旦调用, ListView 就认为数据已经发生变化了, 正确的触发方式是调用 requestLayout 等只改变布局的方式。此时才会有 mActiveViews 的用武之处。所以可以知道 mActiveViews 的用处真的很低。目前来看,只有在Android SDK 版本较低的时候,用户手机的内容第一次上屏,会触发多次的布局流程。mScrapViews 则会在 notifyItemChanged ,item 滑动出场和进场的时候都会使用到。mScrapViews 的数据结构:

image.png

每一个垂直的 ArrayList 代表的是一种 ViewType 在回收池中的一组 View, ViewType 在哪里呢?就是数组的索引,从这里可以看出,ListView 的 ViewType 必须是从0开始,且是连续的。那么当一个 ListView 加载到界面中,后续是怎么做到回收复用的呢?有两种情况:

  • 第一种, notifyItemChanged,全部更新,所有的 View 都会存入 mScrapViews 里边, 然后重新取出来,执行绑定数据:

image.png

  • 第二种,就是滑动了,View 一旦被滑出就界面,就会被回收到 mScrapViews 里边,

image.png

而当一个 View 被滑入到界面中,也就是从 mScrapViews 中取出一个View

image.png

对应的方法就是这个里边的 convertView 这个对象:

image.png

当然,如果没有找到对应的 View,

image.png

就只能通过 getView 创建一个出来了:

image.png

ListView 的复用机制就是这么简单。ListView的复用机制有哪些可以改进的呢?

image.png

  • 1 对应的就是直接找到View,不需要创建也不需要绑定数据
  • 2 对应的只需要重新绑定数据即可
  • 3 完全需要走创建+绑定流程。

因此,我们尽量提高 1 和 2 的效率, 经过前边的分析,我们了解到 mActiveView 这层缓存并没有被很好的利用起来。ListView 的二级缓存,绝大多数情况下 只有 mScarpViews 被用到了。

1. 主要组件

img_v3_02kg_14bec0fe-9c21-43f9-8d48-d68707b364bl.jpg

1.1 RecyclerView

  • 职责:

    • 整体管理者
    • 作为 ViewGroup 的载体,最终负责展示各组件生成的 View 对象

1.2 LayoutManager

  • 职责:

    • 负责 View 的测量、布局和触摸反馈
    • 决定何时回收 View
  • 工作流程:

    • 通过 getViewForPosition() 向 Recycler 请求 View

1.3 ViewHolder

  • 职责:

    • 辅助“循环系统”中可复用的 View 对象
    • 减少 findViewById 的性能消耗

1.4 Recycler

  • 职责:

    • 作为回收管理者,为 LayoutManager 提供 ViewHolder 对象
    • 根据策略选择处于不同状态的 ViewHolder 对象

1.5 Adapter

  • 职责:

    • 将数据转换为 View 对象
    • 负责对 View 对象进行更新操作
  • 工作流程:

    1. 当 Recycler 内部没有合适的 ViewHolder 时,调用 onCreateViewHolder() 创建一个新的 ViewHolder
    2. 随后调用 onBindViewHolder() 为 ViewHolder 绑定数据

1.6 ItemAnimator

  • 职责:

    • 在 Item 状态变化时提供动画效果

1.7 ItemDecoration

  • 职责:

    • 用于对 Item 进行增强,如绘制分割线、实现高亮效果,甚至用于自定义滚动条

2. RecyclerView 简单运⾏机制

一个标准的RecyclerView的运行流程是这样的:

img_v3_02kg_841d4897-252f-4077-8ba0-219f7406b41l.jpg

  1. LayoutManager 请求 View:

    • 通过调用 getViewForPosition() 向 Recycler 索要 View
  2. ViewHolder 创建:

    • 如果 Recycler 内部没有可复用的 ViewHolder,则通过 Adapter 的 onCreateViewHolder() 创建一个新的 ViewHolder
  3. 数据绑定:

    • Adapter 调用 onBindViewHolder() 为新创建或复用的 ViewHolder 绑定数据
  4. View 展示:

    • Recycler 将绑定好数据的 ViewHolder 的 View 交给 LayoutManager 进行布局展示

但是很多时候我们的使用方式并不是最完美的,将Adapter和数据进行了强绑定。尽管在很多实际项目中,将数据存储在 Adapter 内部,甚至将数据作为了Adapter的成员变量是一种常见的做法:

20250321-092513.jpg

例如下边的代码:

public class RecyclerViewDemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    // 存储字符串数据的列表
    private List<String> data = new ArrayList<>();

    // 创建 ViewHolder(此处使用匿名内部类简单示例)
    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, int viewType) {
        // 1. 为演示方便,这里直接动态创建一个 LinearLayout 作为 itemView
        LinearLayout container = new LinearLayout(parent.getContext());
        container.setOrientation(LinearLayout.VERTICAL);
        container.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        ));

        // 2. (可选)如果打算通过 getChildAt(position) 来绑定数据,
        //    可以在此处一次性添加多个 TextView。这里演示添加 10 个 TextView。
        for (int i = 0; i < 10; i++) {
            TextView tv = new TextView(parent.getContext());
            tv.setLayoutParams(new LinearLayout.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT,
                    ViewGroup.LayoutParams.WRAP_CONTENT
            ));
            tv.setTextSize(16);
            container.addView(tv);
        }

        // 3. 返回一个匿名 ViewHolder;真实开发中应使用自定义的 ViewHolder 类
        return new RecyclerView.ViewHolder(container) { };
    }

    // 绑定数据
    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        // index = position,表示要给第 position 个 TextView 设置文本
        int index = position;
        // 从 data 列表中取出字符串,并设置给 itemView 第 index 个子 View(TextView)
        // 注意:若 index 超过 itemView 子视图数量,会抛异常
        ((TextView) holder.itemView.getChildAt(index)).setText(data.get(index));
    }

    // 返回数据列表大小
    @Override
    public int getItemCount() {
        return data.size();
    }

    // 设置/更新数据
    public void setData(List<String> data) {
        this.data = data;
    }
}

这种实现方式就是把数据给Adapter,然后Adapter进过处理,完成数据的展示,不过,从严格的单一职责原则角度出发, Adapter 只应该负责“映射”而不是“管理”数据。也就是说,数据的维护(如数据的获取、更新和业务逻辑)可以交给 Activity、Fragment 或 ViewModel,而 Adapter 仅仅负责根据外部传入的数据来创建和绑定视图, 对上边的逻辑做出如下的调整:

public class RecyclerViewDemoAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

    // 不直接存储数据,而是通过 DataProvider 接口来获取数据
    private DataProvider dataProvider;

    public RecyclerViewDemoAdapter(DataProvider dataProvider) {
        this.dataProvider = dataProvider;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull final ViewGroup parent, int viewType) {
        // 示例:创建一个简单的 TextView 作为 itemView
        TextView textView = new TextView(parent.getContext());
        textView.setLayoutParams(new ViewGroup.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
        ));
        textView.setTextSize(16);
        return new RecyclerView.ViewHolder(textView) { };
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        // 从外部 DataProvider 中获取数据
        String text = dataProvider.getDataAt(position);
        // 假设 itemView 就是一个 TextView
        ((TextView) holder.itemView).setText(text);
    }

    @Override
    public int getItemCount() {
        return dataProvider.getItemCount();
    }

    // 定义一个数据提供接口,由外部实现
    public interface DataProvider {
        String getDataAt(int position);
        int getItemCount();
    }
}

这样Adapter就不存储任何数据了,数据的来源都在外部。

3. RecyclerView 内部缓存机制

LinearLayoutManager中,来到itemView布局入口的方法onLayoutChildren()

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    // 如果存在需要恢复状态(mPendingSavedState)或有待滚动到的目标位置(mPendingScrollPosition),
    // 并且当前 RecyclerView 中没有任何数据(state.getItemCount() == 0),则移除并回收所有已附加的子 View,
    // 因为此时没有数据需要布局,直接返回。
    if (mPendingSavedState != null || mPendingScrollPosition != RecyclerView.NO_POSITION) {
        if (state.getItemCount() == 0) {
            removeAndRecycleAllViews(recycler); // 移除并回收所有子 View
            return;
        }
    }
    
    // 确保布局状态对象已初始化,准备好记录当前布局所需的状态信息
    ensureLayoutState();
    
    // 禁止在本次布局过程中进行回收操作,
    // mLayoutState.mRecycle 为 false 表示当前布局阶段不允许回收 View(防止干扰后续布局)
    mLayoutState.mRecycle = false;
    
    // 根据当前的布局方向和数据状态,确定是否需要颠倒(反转)布局顺序
    // 例如,当 RecyclerView 需要反向布局时,可能需要调整子 View 的排列顺序
    resolveShouldLayoutReverse();
    
    // 准备锚点信息,锚点决定了布局的起始位置和方向
    // onAnchorReady 方法根据 RecyclerView 的状态、数据以及布局方向来设置 mAnchorInfo,
    // 这个信息将在后续布局过程中作为参考点使用
    onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    
    // 将当前所有附加(attached)的子 View 临时分离(detach),
    // 并将它们放入 Scrap 回收集合中,以便后续根据新的数据状态重新布局这些 View
    // 这个步骤可以看作是将现有的布局“清空”,为下一轮布局做准备
    detachAndScrapAttachedViews(recycler);
    
    // 后续的代码将根据 mAnchorInfo 和新的布局状态,
    // 重新从 recycler 中获取并布局需要显示的子 View
}

当 RecyclerView 需要重新布局(例如插入、删除或重新排序 item)时,为了将屏幕上现有的 item 调整到新的位置,最简单的策略就是:

  1. “拿下”屏幕上的所有 item:
    调用 detachAndScrapAttachedViews() 方法时,RecyclerView 会把当前屏幕上显示的所有 item(它们的 ViewHolder 对应的 View)从布局中分离下来。此时,这些 ViewHolder 不再处于“附着”状态,但不会被销毁,而是被存储到一个叫 Scrap 的临时列表中。

  2. Scrap 的组成:
    Scrap 主要包括两个列表:

    • mAttachedScrap: 保存正常分离下来的 ViewHolder。
    • mChangedScrap: 用于存放那些由于数据发生变化(例如 notifyItemChanged)而需要做动画处理的 ViewHolder(例如旧状态的 ViewHolder,用于出场动画)。
  3. 重新布局:
    在布局过程中,LayoutManager 根据新的数据和顺序,将 Scrap 中的 ViewHolder 重新排列并添加回屏幕。

    • 这些 Scrap 中的 ViewHolder仅在当前布局过程中使用,不参与长期的回收复用。
    • 对于屏幕之外的 item,则会被存放在 mCachedViews 或 RecycledViewPool 中,等待下次复用。

简单来说,detachAndScrapAttachedViews() 的作用就是先把当前所有显示的 item “拿下来”,临时保存到 Scrap 中,然后在重新计算 item 位置后,再把它们一个个“放回”到屏幕上,达到快速调整布局的目的,而不必每次都新建 ViewHolder。

public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
     final int childCount = getChildCount();
     for (int i = childCount - 1; i >= 0; i--) {
         final View v = getChildAt(i);
         scrapOrRecycleView(recycler, i, v);
     }
 }

在重新布局时,RecyclerView 会先遍历当前屏幕上所有已经添加的 itemView,将它们从布局中分离下来(相当于“拆下”这些 View),并把它们标记为废弃后存入缓存列表。之后,当需要复用这些 itemView(比如因为数据更新或滚动)时,RecyclerView 就会从缓存列表中取出它们,再按照新的布局要求将它们重新添加到屏幕上。

private void scrapOrRecycleView(Recycler recycler, int index, View view) {
    // 获取该 view 对应的 ViewHolder
    final ViewHolder viewHolder = getChildViewHolderInt(view);
    
    // 判断条件:
    // 如果 viewHolder 被标记为无效(isInvalid() 返回 true)
    // 并且该 viewHolder 并未被移除(!isRemoved())
    // 并且 Adapter 不支持稳定 ID(即 mAdapter.hasStableIds() 返回 false)
    // 则认为这个 viewHolder 的数据已经过时,不能复用,所以直接回收它
    if (viewHolder.isInvalid() && !viewHolder.isRemoved()
            && !mRecyclerView.mAdapter.hasStableIds()) {
        // 从 RecyclerView 的子视图中移除该 view
        removeViewAt(index);
        // 将该 ViewHolder 回收到内部回收机制中,
        // 可能会存放到 mCachedViews 或 RecycledViewPool 中,等待未来复用
        recycler.recycleViewHolderInternal(viewHolder);
    } else {
        // 如果不满足上述条件,则该 viewHolder 可以暂存,等待后续重新布局复用
        // 将该 view 从 RecyclerView 中分离出来,但不完全销毁(不移除缓存)
        detachViewAt(index);
        // 将该 view 放入 scrap 列表中,scrap 列表仅在当前布局过程中保存,
        // 用于重新排列布局,而不参与长期回收复用
        recycler.scrapView(view);
        // 通知 ViewInfoStore,表明该 ViewHolder 已经从 RecyclerView 中分离,
        // 这可能用于记录动画状态或其他附加信息
        mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
    }
}

进入else分支,可以看到先detachViewAt()分离视图,然后再通过scrapView()缓存到scrap中:

void scrapView(View view) {
    // 获取该 view 对应的 ViewHolder 实例
    final ViewHolder holder = getChildViewHolderInt(view);
    
    // 判断当前 ViewHolder 是否符合加入 mAttachedScrap 的条件:
    // 1. 如果该 ViewHolder 已经被标记为已移除或无效(FLAG_REMOVED 或 FLAG_INVALID),
    // 2. 或者它没有经过更新(!isUpdated()),
    // 3. 或者当前可以复用更新后的 ViewHolder(canReuseUpdatedViewHolder(holder) 返回 true)。
    // 如果以上任一条件成立,就将该 holder 视为普通的 Scrap,
    // 并加入到 mAttachedScrap 列表中,用于正常的回收和复用。
    if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
            || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
        // 设置 holder 的 Scrap 容器为当前 Recycler(this),标识它不是“变化”状态的 Scrap
        holder.setScrapContainer(this, false);
        // 将该 holder 添加到 mAttachedScrap 列表中
        mAttachedScrap.add(holder); // 保存到 mAttachedScrap 中
    } else {
        // 如果不满足上述条件,则说明该 ViewHolder 属于更新后的项,需要单独处理变化动画
        // 将它存入 mChangedScrap 中,以便在预布局阶段用于出场动画
        // 如果 mChangedScrap 还未初始化,则先创建新的 ArrayList
        if (mChangedScrap == null) {
            mChangedScrap = new ArrayList<ViewHolder>();
        }
        // 设置 holder 的 Scrap 容器为当前 Recycler,并标记为“变化”类型(changed scrap)
        holder.setScrapContainer(this, true);
        // 将该 holder 添加到 mChangedScrap 列表中
        mChangedScrap.add(holder); // 保存到 mChangedScrap 中
    }
}

当一个 ViewHolder 进入 scrapView() 方法时:

  • 如果符合条件(例如无效、未被移除或能直接复用),它就会被放进 mAttachedScrap 列表;
  • 否则,它会被放进 mChangedScrap 列表(用于需要特殊处理动画的情况)。

而在 scrapOrRecycleView() 方法中:

  • 如果进入 if 分支时,发现该 ViewHolder 是无效的、但没有被移除且没有被标记,那么 RecyclerView 会先调用 removeViewAt() 把它从布局中移除,
  • 然后调用 recycleViewHolderInternal() 把它缓存到内部的回收池中,以便未来复用。

这样就保证了根据不同情况,ViewHolder 分别存入不同的缓存区域或直接回收,从而帮助 RecyclerView 高效地管理和复用 item 视图。

void recycleViewHolderInternal(ViewHolder holder) {
    // … 其他前置逻辑

    // 如果需要强制回收(forceRecycle为true)或者该 ViewHolder 是可以回收的(isRecyclable()返回true)
    if (forceRecycle || holder.isRecyclable()) {
        // 检查是否允许加入本地缓存 mCachedViews:
        // 1. mViewCacheMax > 0 表示允许缓存一定数量的 ViewHolder;
        // 2. 同时确保 holder 不带有以下任何标记:
        //    - FLAG_INVALID:表示 ViewHolder 已经无效,不应复用
        //    - FLAG_REMOVED:表示 ViewHolder 已经被移除
        //    - FLAG_UPDATE:表示 ViewHolder 正在更新数据
        //    - FLAG_ADAPTER_POSITION_UNKNOWN:表示 ViewHolder 的适配器位置未知
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                | ViewHolder.FLAG_REMOVED
                | ViewHolder.FLAG_UPDATE
                | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {

            // 获取当前 mCachedViews 缓存列表中的 ViewHolder 数量
            int cachedViewSize = mCachedViews.size();
            // 如果当前缓存数量已达到或超过最大容量限制,并且缓存中至少有一个 ViewHolder,
            // 则需要先回收掉缓存列表中的第一个 ViewHolder,为新的 ViewHolder 腾出空间
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { // 如果超出容量限制,把第一个移除
                recycleCachedViewAt(0);
                cachedViewSize--;
            }
            // 将当前 holder 插入到 mCachedViews 指定的位置(targetCacheIndex),加入本地缓存
            mCachedViews.add(targetCacheIndex, holder); // mCachedViews 回收
            // 标记 holder 已经成功缓存到 mCachedViews 中
            cached = true;
        }
        // 如果 holder 没有被加入到 mCachedViews(例如条件不满足或 mViewCacheMax 为 0),
        // 则将 holder 添加到全局的 RecycledViewPool 中进行回收复用
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true); // 放到 RecycledViewPool 回收
            recycled = true;
        }
    }
    // … 其他后续逻辑
}

如果符合条件,会优先缓存到mCachedViews中时,如果超出了mCachedViews的最大限制,通过recycleCachedViewAt()CacheView缓存的第一个数据添加到终极回收池RecycledViewPool后再移除掉,最后才会add()新的ViewHolder添加到mCachedViews中。

剩下不符合条件的则通过addViewHolderToRecycledViewPool()缓存到RecycledViewPool中。

void addViewHolderToRecycledViewPool(@NonNull ViewHolder holder, boolean dispatchRecycled) {
    clearNestedRecyclerViewIfNotNested(holder);
    View itemView = holder.itemView;
    ······
    holder.mOwnerRecyclerView = null;
    getRecycledViewPool().putRecycledView(holder);//将holder添加到RecycledViewPool中
}

还有一个就是在填充布局fill()的时候,它会回收移出屏幕的view到mCachedViews或者RecycledViewPool中:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
           RecyclerView.State state, boolean stopOnFocusable) {
       if (layoutState.mScrollingOffset != LayoutState.SCROLLING_OFFSET_NaN) {
             recycleByLayoutState(recycler, layoutState);//回收移出屏幕的view
       }
   }

recycleByLayoutState()层层追查下去,会来到recycler.recycleView(view)Recycler的公共回收方法中:

public void recycleView(@NonNull View view) {
      ViewHolder holder = getChildViewHolderInt(view);
      if (holder.isTmpDetached()) {
          removeDetachedView(view, false);
      }
      recycleViewHolderInternal(holder);
  }

回收分离的视图时,RecyclerView 会将它们存入缓存池以便将来重新绑定和复用。这个过程进入了 recycleViewHolderInternal(holder) 方法,方法内部首先优先尝试将 ViewHolder 添加到 mCachedViews 缓存中;如果达到容量上限,则会先移除最旧的项;当无法缓存到 mCachedViews 时,才会将 ViewHolder 放入全局共享的 RecycledViewPool。至此,整个回收流程就完成了。

4. RecyclerView 内部复用机制

提要:mAttachedScrap mChangedScrap mCachedViews mRecyclerPool 他们的数据被使用后都会被移除掉。

4.1 CachedViews 与 RecycledViewPool

  • CachedViews作用:

    • 缓存屏幕滑动时被移入/移出的 ViewHolder
  • CachedViews特点:

    • 默认大小为 2,可通过 setItemViewCacheSize() 修改
    • 从这一层获取的 ViewHolder 的 position 是正确的
    • 命中缓存时无需重新绑定数据
  • RecycledViewPool作用:

    • 根据 viewType 对 ViewHolder 进行缓存
  • RecycledViewPool特点:

    • 默认每个 viewType 缓存数量为 5,可通过 setMaxRecycledViews() 修改,可以单独根据ViewType设置容量,进行单独的优化。尤其是这种调用notifyDataSetChanged()的情况;
    • 多个 RecyclerView 可以共享同一个 RecycledViewPool
    • 从这一层获取的 ViewHolder position 不一定正确,通常需要重新绑定数据
    • ViewType的值因为不是数组索引,可以R.layout来直接作为ViewType

mCachedViews 类似于 ListView 中的 mActiveViews,都是用来缓存那些无需重新绑定的视图。但它们的使用场景大不相同:

  • mActiveViews:仅在数据没有改变且触发布局更新时才会起作用;
  • mCachedViews:主要用于优化滑动性能,尤其是在快速滑动或回滚时,当 item 被滑出屏幕后就会存入该缓存,从而极大地提升复用效率。

尽管 mCachedViews 的实现是一个 ArrayList,默认最多只缓存两个 ViewHolder,但采用 ArrayList 的目的是为了在特定情况下能够灵活调整缓存容量。这个容量由 mViewCacheMax 控制,并且可以通过调用 RecyclerView#setItemViewCacheSize 来设置默认大小。

在下图中,刚开始的时候mActiveViews的大小是空的:

image.png

此时向上滑动列表,此时RecyclerView的回收机制开始发挥作用,上边的itemView就会被移除屏幕,放入 mCachedView 中,下边就会出现新的ItemView

image.png

这样mCachedViews里边就有可以复用的ViewHolder了:

image.png

当你向下滑动列表以查看上方的内容时,新出现的 ItemView 会触发复用机制。RecyclerView 会根据新 ItemView 的 position 值,去 mCachedViews 中查找对应的、可以直接复用的 ViewHolder。例如,假设 position 为 1 的 ItemView 重新出现,而 mCachedViews 中恰好有一个对应 position 为 1 的 ViewHolder,那么系统就会直接命中缓存,从而复用该 ViewHolder,而不必重新创建和绑定。这大大提高了滚动过程中的性能和响应速度。

image.png

直接命中缓存的好处在于可以避免不必要的数据绑定,从而提升滑动时的性能和响应速度。回顾 ListView 的逻辑:

  • ListView 中

    • mActiveViews 存储了已绑定数据的 View 对象。
    • 当一个 item 被滑回屏幕并且命中 mActiveViews 时,不需要重新绑定数据,直接复用已有的 View。
  • RecyclerView 中

    • 如果 mCachedViews 中命中一个对应位置(position)的 ViewHolder,说明该 ViewHolder 已经绑定过数据,无需再调用 onBindViewHolder 方法。
    • 这意味着 RecyclerView 可以直接复用这个 ViewHolder,而跳过数据重新绑定的过程,进一步减少开销并提高性能。

image.png

在 RecyclerView 中,mCachedViews 并不是按照 ViewType 分类存储的,而是简单地以 position 为依据进行查找。这样一来,如果一个 ViewHolder 刚刚绘制到屏幕上又立刻被划回(例如快速滑动时),它会直接从 mCachedViews 中命中,从而跳过 onBindViewHolder 的调用,因为数据本身并未改变。

但是,在某些场景下,你可能希望在 View 消失时释放一些资源,而当它重新出现在屏幕上时又重新申请这些资源。为此,Adapter 提供了两个专门的回调方法来处理这种需求:

  • onViewDetachedFromWindow(ViewHolder holder)
    当一个 ViewHolder 的 View 从窗口上分离时(例如滑出屏幕),这个方法会被调用。你可以在这里释放不再需要的资源,比如停止动画、取消网络请求、释放大图缓存等。
  • onViewAttachedToWindow(ViewHolder holder)
    当一个 ViewHolder 的 View 重新附着到窗口上时(例如重新滑入屏幕),这个方法会被调用。你可以在这里重新初始化或申请之前释放的资源,确保视图恢复正常显示。

这两个回调确保了即使在缓存中直接命中、没有触发 onBindViewHolder 的情况下,你仍然可以通过窗口附着和分离的事件来管理资源。下面是一个示例代码,突出显示了这两个方法的使用:

@Override
public void onViewDetachedFromWindow(@NonNull MyViewHolder holder) {
    super.onViewDetachedFromWindow(holder);
    // 当 View 从窗口分离时,释放相关资源
    holder.releaseResources();
}

@Override
public void onViewAttachedToWindow(@NonNull MyViewHolder holder) {
    super.onViewAttachedToWindow(holder);
    // 当 View 重新附着到窗口时,重新申请或初始化资源
    holder.initResources();
}

通过他们我们就可以知道ViewHolder重新出现在屏幕上了。这个比onBindViewHolder准确的多。继续上边,因为滑动,我们把放入mCachedViews里边的ViewHolder又拿了出来,导致最下边的ItemView被移除了屏幕,这个时候会发生什么事情呢?

image.png

很简单,就是把下边的ViewHolder放入到mCachedViews里边就可以了:

image.png

到现在我们知道了,RecyclerView针对部分情况的上下滑动操作,可以通过mCachedViews来实现避免重复绑定数据的操作,但是前边我们的滑动都很小心,没有让滑动的数据量超过mCachedViews的容量。如果缓存中已经存在了两个ViewHolder的情况,应该怎么处理呢?比如下边这种情况,我们还要继续滑动:

image.png

这个时候就会有个新的ViewHolder因为超过了RecyclerView的边界,需要被回收了。

image.png

回收的时候会发生什么呢?

image.png 原本较早的那个会被挤出去,挤出去放到哪里呢?会被放入回收池中,在ListView中是mScrapViews对象,而在RecyclerView中就是mRecyclerPool对象,在ListView中,mScrapViews是一个数组,它的数据结构是这样的:

image.pngmRecyclerPool则有很大的区别,它的内部还有一个ScrapData内部类,ScrapData里边有一个mScrapHeapArrayList的成员变量。而成员变量mScrap的类型就是SparseArray<ScrapData>.

public static class RecycledViewPool {
    // 表示每个ViewType对应的ArrayList最大能容纳ViewHolder的数量默认是5,listView中没有这个功能。这个值可以通过RecyclerView.RecycledViewPool#setMaxRecycledViews来修改为一个更恰当的值。
    private static final int DEFAULT_MAX_SCRAP = 5;
    static class ScrapData {
        @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
        ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
SparseArray<ScrapData> mScrap = new SparseArray<>(); // 可以认为SparseArray是一个HashMap,只不过它的key只能是Int类型

因此从上边我们可以总结出下边一个高度概括的图:

image.png 可以认为RecyclerViewPool拥有这样类型的一个成员变量:

// 每一种ViewType可以默认可以存储最多5个同类型的ViewHolder
RecycledviewPool#SparseArray<ArrayList<Recyclerview.viewHolder>>

与 ListView 中的 mScrapViews 相比,RecyclerView 在管理缓存的机制上有两个主要区别:

  1. 数据结构的不同

    • ListView 使用的是 SparseArray 存储 mScrapViews,而 SparseArray 依赖索引查找,而在 ListView 中这个索引就是 ViewType。这就要求 ViewType 必须是连续的数字。
    • RecyclerView 则采用了 ArrayList 来保存每个 ViewType 对应的 ViewHolder,可以使用任意的 int 值作为 ViewType(甚至直接用 layoutID),不要求连续性,从而提供了更大的灵活性。
  2. 缓存容量的控制

    • RecyclerView 引入了一个常量 DEFAULT_MAX_SCRAP,它表示每个 ViewType 对应的 ArrayList 最多能缓存的 ViewHolder 数量(默认值为 5)。

    • 这一机制在 ListView 中是不存在的。通过调用

      RecyclerView.RecycledViewPool#setMaxRecycledViews(int viewType, int max)
      

      你可以根据实际情况调整每个 ViewType 的缓存容量。

      • 如果某个 ViewType 在屏幕上频繁出现,比如有几十个 item,则可以适当增大这个值,从而缓存更多的 ViewHolder,避免频繁创建新的 ViewHolder。
      • 反之,如果 ViewType 很多,而每种类型的 item 数量较少,则可以减小这个值以节省内存。

这种设计使得 RecyclerView 在处理多样化的 item 类型时更加灵活高效,同时也能根据实际场景优化复用性能。

image.png

另外RecyclerView还支持多个RecyclerView共用一个RecyclerViewPool,后边会有相应的示例:

com.android.internal.widget.RecyclerView.Recycler#setRecycledViewPool

继续我们的滑动流程,如果此时,我们又需要向下滑动的话,让上边的Item显示出来,会发生什么呢?

image.png 上方的两个ItemView直接从mCachedViews通过position中拿出来然后显示,于此同时,下边被移除屏幕的ViewHolder会被放入mCachedViews中,注意,postion为7的会先被移除。

image.png

如果此时继续下拉列表,已经不能通过mCachedViews里边获取到正确的ViewHolder了。只能通过ViewType从回收池中获取了。而刚好有一个符合的ViewHolder,刚好就可以复用了。

image.png

在回收池找到合适的ViewHolder进行复用,需要做什么事呢?我们再看一次在ListView中的复用逻辑,我们的RecyclerViewPool就相当于ListView中的mScrapViews这一层,而在这一层获取的ItemView是需要重新绑定数据的,因为从这里取出的ViewHolder只能保证ViewType的类型是一致的,不能保证position是一致的。position不一致,也就可能导致ViewHolder中残留的数据并不是正确的数据。

image.png

补充一个额外的知识点:在上划或下划滚动过程中,从回收池中取出的 ViewHolder 通常就是原先处于那个位置上的 ViewHolder。如果数据没有变化,那么实际上不需要重新绑定数据,ViewHolder 直接展示原来的内容就能正确显示。虽然这个细节在大多数场景下看似无关紧要,但在数据绑定操作非常耗时的情况下,它的意义就非常明显了。

例如,在某些复杂的 Item 中,调用 setText 可能比较耗时。如果在绑定数据之前,先检查当前 TextView 中已有的文本和即将绑定的文本是否一致,就可以避免不必要的 setText 调用,从而提升性能。同样的逻辑也适用于图片加载等其他耗时操作。

重点说明:

  • 直接命中缓存的好处:当从 mRecyclerPool 中取出 ViewHolder 后,如果数据没有变化,就不必重复绑定,节省了 CPU 资源。
  • 避免不必要的更新:在绑定前进行差异比较(如文本、图片等),确保只有在数据变化时才进行更新,从而降低耗时操作的频率。
  • 性能优化场景:在数据绑定操作较重(例如复杂的文本计算、图片处理)的场景下,这种优化手段可以显著提升滑动流畅度和响应速度。

image.png

另外有一种情况,如果我们要对屏幕整体刷新(一般调用差异更新,最好不要全部刷新。),调用notifyDataSetChanged()的方法, 并不会进入mCachedViews里边的,因为mCachedViews是不需要进行数据重新绑定的。而是把所有的ViewHolder全部存入了mRecyclerPool中。

image.png

每个 ViewType 默认最多缓存 5 个 ViewHolder。这意味着,如果某种类型(例如 B 类型)的 item 数量超过 5 个,多余的 ViewHolder 将不会被缓存,而是直接丢弃。当这些被丢弃的 ViewHolder 再次出现在屏幕上时,就需要重新执行完整的创建与绑定流程;而缓存中的 ViewHolder 则仅需调用 onBindViewHolder 进行数据绑定,从而降低了额外的开销。

为了解决这个问题,可以在调用 notifyDataSetChanged() 之前,临时增大该类型的缓存容量;待 RecyclerView 完成复用之后,再将回收池的容量恢复到默认值。虽然这种方式看起来不够优雅,但在大量相同类型 item 出现时,能显著提升运行效率。

image.png

4.2 AttachedScrap & ChangedScrap

  • 作用:

    • 提供 pre-layout 和 post-layout 过程中的缓存支持
    • 没有大小限制

RecyclerView着重在两个场景缓存和回收的优化,一是:在数据更新时,使用Scrap进行局部更新,尽可能复用原来 viewHolder,减少绑定数据的工作;二是:在滑动的时候,重复利用原来的 ViewHolder ,尽可能减少重复创建 ViewHolder 和绑定数据的工作。最终思想就是,能不创建就不创建,能不重新绑定就不重新绑定,尽可能减少重复不必要的工作。 很多人都说 RecyclerView 有4级缓存(CachedViewsmRecyclerPoolmAttachedScrapmChangedScrap),如果仅仅是CachedViewsmRecyclerPool,这其实就是在 ListView 的两层基础上做优化,其实改进并不大,仅仅凭借这两点完全没有必要重新创建一个 RecyclerView 出来,其实 RecyclerView 对于复用 View 或者是 ViewHolder 对于 ListView 最大的提升,有了以下这几个方法的出现,才让 RecyclerView 相比 ListView 有了质的提升。

image.png

ListViewnotifyDataSetChanged() 无论item是否有更新,都会全部绑定数据。这是非常不明智的做法。

image.png

最好的做法是谁改变谁刷新:

image.png

当某一项数据改变了,我们通过notifyItemChanged()进行特定的项目进行调整就行了。这样让我们才有使用RecyclerView有别于ListView的意义。这样使用后只有指定的Item会走onBindViewHolder,其余的都不会触发。其余的ViewHolder都节约掉一次绑定数据的操作。能节约掉onCreateViewholder已经很不错了,现在连onBindViewHolder也节省了。RecyclerView是如何做到这一点的呢?当我们调用notifyItemChanged()时,对于RecyclerView来说,也就知道哪些ViewHolder是没有改变的,没有改变当然就不需要走重新绑定的流程了。那么问题来了,我们没有被改变的View,是不是就可以直接被layoutManger使用呢?并不是这样的,例如, Item发生改变的时候可能影响到没有修改过的其他的Item的布局,比如删除一个Item后,可能屏幕上边的Item就不够了,需要添加一个新的Item进来。

image.png 又比如我们需要插入一个Item进来,会导致当前正在显示的Item变为不可见,进而把这个超出边界的Item进行回收: image.png

如果将所有未使用的 ViewHolder 全部交给 LayoutManager,那么 LayoutManager 就需要关注这些 ViewHolder 是否可用、是否需要回收,从而导致职责边界混乱。为了解决这一问题,RecyclerViewRecycler 内部增加了一层 mAttachedScrap 暂存区,用于存放那些可以直接复用的 ViewHolder,从而让 LayoutManager 只专注于布局,而无需关心 ViewHolder 的可用性问题。

具体流程如下:

  • mAttachedScrap 暂存区:用于存放最新且可以直接复用的 ViewHolder,这部分数据比 mCachedViews 更新;
  • mCachedViews 缓存区:当 mAttachedScrap 中没有足够的 ViewHolder 时,RecyclerView 会从这里获取 ViewHolder;
  • mRecyclerPool 回收池:如果前两者均无法提供所需的 ViewHolder,RecyclerView 最后会从回收池中获取。

这一取出 ViewHolder 的优先级顺序可以表示为:mAttachedScrap > mCachedViews > mRecyclerPool

在一次布局过程中,LayoutManager 首先尝试从 mAttachedScrap 中获取 ViewHolder;若不足,则依次从 mCachedViewsmRecyclerPool 中获取。这种设计使得 LayoutManager 无需关心具体的 ViewHolder 回收和复用机制,从而保持了职责的清晰分离。

image.png

如果在一次布局里边,LayoutManager完成布局之后,mAttachedScrap里边还有剩余的ViewHolder,这个时候会发生什么事呢?

image.png

image.png

严格来说,mAttachedScrap 并不算是一个持久性的缓存,而是一种临时的存储区域。它的作用是在一次布局过程中,临时保存那些数据没有变化、可以直接复用的 ViewHolder,以便 LayoutManager 在需要时快速获取这些 ViewHolder。布局结束后,任何剩余在 mAttachedScrap 中的 ViewHolder 都会被转移到 mRecyclerPool 中,供后续复用。

另一方面,当某项数据发生变化时,对应的 ViewHolder 通常会被放入另一个暂存区——mChangedScrap。那为什么需要两个不同的暂存区呢?这就涉及到 RecyclerView 的核心机制——pre-layoutpost-layout

  • Pre-layout 阶段
    在这个阶段,RecyclerView 会捕捉数据变化的信息,同时为后续动画等效果做准备。

    • 未发生变化的 ViewHolder 放在 mAttachedScrap 中,供当前布局过程中快速复用。
    • 数据变化的 ViewHolder 则放入 mChangedScrap,以便在后续阶段正确处理动画和位置变化。
  • Post-layout 阶段
    布局结束后,RecyclerView 会根据 pre-layout 阶段收集的信息,对 ViewHolder 进行最终布局和动画处理。此时,所有未被 LayoutManager 使用的 mAttachedScrap ViewHolder 会被统一放回到 RecyclerPool 中,保持整个回收机制的整洁和高效。

这种设计方式确保了 LayoutManager 只专注于布局,而不必关心 ViewHolder 是否需要复用或者数据是否发生了变化,从而实现了职责的清晰分离。总体来说,mAttachedScrap 作为一个临时存储区域,本身并非传统意义上的缓存,而是与 mChangedScrap 协同工作的一个机制,专门用于支持 RecyclerViewpre-layoutpost-layout 流程。

image.png

在下边的示例中,插入与删除动画都没有异常,但是当我们调用notifyItemChanged的时候会发生整个Item闪烁的情况:

image.png

20250321-221037.gif

这里就有两个问题了:RecyclerViewItem做出改变的时候自动添加了动画?为什么执行更新动画的时候整个Item都会闪烁?

比如下边的场景,AB 在屏幕上显示,然后我们删除了 B 的时候,RecyclerView创建了 C ,并且让 C 有一个出现的动画。 image.png

20250321-221037.gif

动画的启动都是需要一个起始值和结束值的。结束值很明显是 C 处于 A 的下方,那么开始的时候 C 的位置在哪里呢?因为这个时候 C 是不可见的,LayoutManager 是对 C 没有感知的,你可能认为不就是在 B 的下边吗?但是不要忘了LayoutManager 可以不只是垂直排列,还有横向排列、瀑布流排列等等。所以 C 的出现位置可能是四面八方任何位置。所以我们不知道 C 的起始值。那么谷歌是如何解决这个问题的呢?

image.png

Adapter 发生改变之后,通知到 RecyclerView ,RecyclerView 就会向 LayoutManager 请求两个布局:pre-layout 和 post-layout ,pre-layout 就是数据改变之前的布局的操作,post-layout就是对应的数据改变之后的布局的操作。pre-layout的作用就是在通过Adapter给的信息,把C提前的摆放在RecyclerView中(尽管此时还是看不见的),一旦完成摆放,那么动画的起始点就确定了。这就是RecyclerView中pre-layout和post-layout机制了。正是因为这样的机制,才让RecyclerView做预测性动画非常容易了。

不过说了这么多,pre-layout 和 post-layout 与 mAttachedScrap 和 mChangedScrap 这两个暂存区有什么关联呢?这个可以从更新 Item 时出现闪烁这个点着手。闪烁的原因是有两个Item在做动画,一个是淡出的,一个是渐入的,也就是以前的一个慢慢的透明,新的慢慢的不透明,咦,这不是局部刷新吗?不仅没有把以前的ViewHolder给利用起来,还引入了一个新的ViewHolder,这个是不是就不是局部刷新了?的确是这样,但是RecyclerView必须这么做。这就需要了解一个事实,那就是插入和删除动画其实都很简单,就是被插入的Item执行进场动画,被移除的Item执行出场动画就足够了,但是改变Item并不是只有自身一个就够了。现实世界中有两种改变形态,都是称之为Changed,一种改变就是在原来的基础上发生变动,例如对象的成员变量发生变化,另一种改变则是整个对象的替换,换成一个全新的同样的对象,如果是第二种改变,仅仅只有一个ViewHolder参与动画是不够的。必须要有两个ViewHolder,一个出场一个进场。这也就意味着在某一时刻,有两个position完全一致的ViewHolder同时处于列表中。

image.png

从这个角度看,pre-layout 和 post-layout 只能在暂存区里边获取一次改变的对象,那谁能从暂存区获取呢?暂存区里边存储的是原本的数据,pre-layout 对应的就是数据没有改变的那一行,而在最终的布局中 post-layout 中,这个相同的 ViewHolder 一定不能和其他的 ViewHolder 一样,继续在暂存区获取 ViewHolder ,因为如果是在暂存区 mChangedScrap 和 mAttachedScrap 中获取,那么岂不是在一个 ViewHolder 上同时做了出场与入场动画?

image.png

那么如何去避免这个事情呢?很简单,因为我们增加了一个 mChangedScrap 暂存区,那么在 pre-layout 的时候,可以同时在 mAttachedScrap 和 mChangedScrap 中尝试获取对应的 ViewHolder ,其实也就是遍历两个Arraylist,但是在 post-layout 的时候,就只能从 mAttachedScrap 中获取对应的ViewHolder了。这样就不能获取 mChangedScrap 里边的ViewHolder了。

image.png

那么可能又有新的疑问了,post-layout 中另一个 ViewHolder 从哪里来呢?这个 ViewHolder 只能从 mCachedViews 和 mRecyclerPool 中获取了,如果这里边都找不到,只能通过 Create 创造一个出来了。

image.png

因此前边闪烁的问题得到答案了,就是因为在change的时候两个 ViewHolder 分别执行了入场与出场动画。解决闪烁的方法也出来了:RecyclerView.LayoutManager#supportsPredictiveItemAnimations调用这个方法禁止预测性动画,此时,因为不需要动画了。pre 和post layout 机制就可以直接使用同一个 ViewHolder 了,改变后的这个ViewHolder 就会直接进入 mAttachedScrap 里边获取 ViewHolder 了。但是这样会导致插入和删除动画也没有了。还有没有更好的办法,当然有:

image.png 使用双参数的RecyclerView.Adapter#notifyItemChanged(int, java.lang.Object),第二个参数表示的是一个数据包裹中真正有意义的数据,例如Http即便发送一个字符串,也会有一大堆的Header,而payload就是指的是处 Http header 等之外的核心数据,也就是一个字符串。通过这种方式,ViewHolder 也不会进入 mChangedScrap ,而是直接进入了 mAttachedScrap 中,此时也不会看到图片有闪烁的情况了。

image.png

通过上边我们彻底的了解了 RecyclerView 的核心内容:局部刷新,预测性动画,pre-layout机制,post-layout机制,和怎么实现更加高效的局部刷新,最重要的是知道了他们这些内容的实现基础。在缓存之前,会有 ViewHolder 的暂存区。到目前为止,RecyclerView 的两层缓存和暂存区就分析过了。

4.3. ViewCacheExtension

  • 作用:

    • 允许开发者自定义缓存层,目前一般应用较少

4.4 核心源码流程分析

缓存机制的核心就是Recycler,对应的就是ListView中的RecyclerBin,

public final class Recycler {
    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;
    
    // 作用于ListView中的mActiveViews类似,连绑定过程都不需要的缓存。但是和mActiveViews的作用场景有很大的不同,
    // mActiveViews的作用很有限,只有在数据没有改变,触发了Layout的情况才能发挥作用,而mCachedViews主要优化的事滑动的性能,尤其是滑动的过程中回滚的时候能够极大的提升复用的效率,当滑动过程中item被画出屏幕的时候就会被存入ViewHolder。虽然mCachedViews是一个ArrayList,但是它默认最多只能存放两个回收的ViewHolder,那为什么存放两个还需要弄一个ArrayList呢?这是为了在某些特殊的场景设置更合适的大小。它的大小就是由mViewCacheMax决定的,可以通过RecyclerView#setItemViewCacheSize来设置它的默认大小。
    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

    private final List<ViewHolder>
            mUnmodifiableAttachedScrap = Collections.unmodifiableList(mAttachedScrap);

    private int mRequestedCacheMax = DEFAULT_CACHE_SIZE;
    int mViewCacheMax = DEFAULT_CACHE_SIZE;

    RecycledViewPool mRecyclerPool;

    private ViewCacheExtension mViewCacheExtension;

    static final int DEFAULT_CACHE_SIZE = 2;

获取View的方式:

// 从下边这两个方法我们就知道,LayoutManager其实并不知道ViewHolder的,LayoutManager管理的是View,LayoutManager和View与RecyclerView与ViewHolder的界限就产生了。
public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(position, dryRun, FOREVER_NS).itemView;
}

// 复用机制完成在这个方法中完成的
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    // 检查 position 是否有效
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                + "(" + position + "). Item count:" + mState.getItemCount());
    }
    // 标识是否从 scrap、隐藏视图或缓存中获取到 ViewHolder
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;

    // 0) 如果处于预布局阶段,先尝试从 mChangedScrap 中获取对应 position 的 ViewHolder
    //    这里主要用于获取旧数据的 ViewHolder,供出场动画使用,只会在pre-layout的时候进入这个逻辑,LayoutManager向Recycler里边索取View会经过两次layout,粉笔是pre-layout和post-layout
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }

    // 1) 如果未从 mChangedScrap 中获取到,则尝试从 mAttachedScrap 或隐藏列表/缓存中获取
    if (holder == null) {
        // 这里的Hidden只会在及其特殊的情况下才会发生作用,页面上新插入的Item正在把一个Item挤出屏幕,正当这个Item通过动画移除的时候,突然新插入的Item又被删除了,这个时候就可以在Hidden中找到了。
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
        if (holder != null) {
            // 校验该 ViewHolder 是否适用于当前的 offset position(即是否与数据匹配)
            if (!validateViewHolderForOffsetPosition(holder)) {
                // 如果校验不通过,则需要回收该 ViewHolder
                if (!dryRun) {
                    // 标记为无效,以防止被动画等逻辑继续使用
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    // 如果 ViewHolder 正处于 scrap 状态,则先将其从附着的视图中移除,并取消 scrap 状态
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        // 如果之前从 scrap 返回过,则清除相应标志
                        holder.clearReturnedFromScrapFlag();
                    }
                    // 将该 ViewHolder 回收到内部回收机制中
                    recycleViewHolderInternal(holder);
                }
                // 设置 holder 为 null,后续会尝试创建或从其他缓存中获取
                holder = null;
            } else {
                // 如果校验通过,说明这个 holder 是有效的
                fromScrapOrHiddenOrCache = true;
            }
        }
    }

    // 2) 如果仍然没有找到合适的 ViewHolder,则尝试创建新的或从其他缓存中获取
    if (holder == null) {
        // 通过 AdapterHelper 查找实际的 item offset(考虑到 adapter 可能做了插入、删除等操作)
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
            throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                    + "position " + position + "(offset:" + offsetPosition + ")."
                    + "state:" + mState.getItemCount());
        }

        // 获取该位置对应的 item 类型,因为通过position已经找不到ViewHolder了,必须要通过Type了。就可以从回收池里边获取ViewHolder了。
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 如果 Adapter 使用稳定 ID,则可以尝试根据 ID 从 scrap 或缓存中查找 ViewHolder,这个StableId是怎么来的呢?只有在调用notifyItemChanged的时候才特别有意义,
        // 因为在调用notifyItemChanged的时候,如果没有设置StableId,所有ViewHolder并不会像局部刷新一样,被放入到Scrap里边,而是全部被放入回收池中。
        // 如果屏幕同时显示10个Item,而回收池的同类型最多只能容纳5个Item,这样只有5个能够进入回收池,另外5个被丢弃了。少5个怎么办?只能重新创建了。
        // 所以从这里也能看出来,notifyItemChanged是多么的糟糕。
        // 只要我们setStableIds = true, 所以的ViewHolder在重新,布局的时候就会进入Scrap里边,而不是直接进入回收池。于此同时,也需要改变获取ViewHolder的方式,变成了mAdapter.getItemId了。
        // 这意味着你需要重写Adapter的getItemId这个方法。根据数据返回ID才能取得正确的效果。通过setStableIds+getItemId就能够避免notifyItemChanged带来的糟糕体验。
        // 同时还能让我们即便调用notifyItemChanged的时候也能拥有动画。
        if (mAdapter.hasStableIds()) {
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // 更新 holder 的位置
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
        // 如果仍然没找到,并且有自定义缓存扩展(ViewCacheExtension),则使用它,不过这个目前没有找到可用之处,也鲜有开发者自定义这个缓存。
        if (holder == null && mViewCacheExtension != null) {
            // 注意:这里不传 offsetPosition,因为 LayoutManager 并不关心这个值
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view which does not have a ViewHolder");
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view that is ignored. You must call stopIgnoring before"
                            + " returning this view.");
                }
            }
        }
        // 如果以上都没有找到,则尝试从全局共享的回收池(mRecyclerPool)中获取
        if (holder == null) { // fallback to pool
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                        + position + ") fetching from shared pool");
            }
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                // 重置 holder 的内部状态
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
        // 如果还是没有找到,则创建一个新的 ViewHolder
        if (holder == null) {
            long start = getNanoTime();
            // 检查是否在指定 deadline 内可以创建新的 ViewHolder
            if (deadlineNs != FOREVER_NS
                    && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
                // 如果无法在 deadline 内创建,则返回 null(放弃获取)
                return null;
            }
            // 创建新的 ViewHolder
            holder = mAdapter.createViewHolder(RecyclerView.this, type);
            if (ALLOW_THREAD_GAP_WORK) {
                // 如果允许线程间 gap work,则检查是否有嵌套的 RecyclerView,
                // 如果有则记录在 ViewHolder 中,用于后续预取操作
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }
            long end = getNanoTime();
            // 将创建时间纳入统计,用于未来判断是否能在 deadline 内创建
            mRecyclerPool.factorInCreateTime(type, end - start);
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
            }
        }
    }

    // 以下部分用于记录一些动画信息
    // 只有在非预布局阶段,并且 holder 来自 scrap/缓存的情况下,才需要执行以下逻辑
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        // 清除 FLAG_BOUNCED_FROM_HIDDEN_LIST 标记
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            // 构建动画所需的标志
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            // 标记该 ViewHolder 在预布局中出现过
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            // 记录预布局阶段的动画信息
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
    // 如果处于预布局阶段且 holder 已经绑定过,说明数据可用,则只需更新其预布局位置,无需重新绑定数据
    if (mState.isPreLayout() && holder.isBound()) {
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
        // 如果 holder 尚未绑定(刚刚创建出来的Holder),或需要更新,或标记为无效(从回收池获取的ViewHolder等),则尝试重新绑定
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder);
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        // 绑定 ViewHolder,bound 表示绑定是否成功
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    // 设置 ViewHolder 对应的 LayoutParams
    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        // 如果没有 LayoutParams,则生成默认的 LayoutParams
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        // 如果已有的 LayoutParams 不符合要求,则重新生成
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    // 将当前 ViewHolder 绑定到 LayoutParams 上,方便 LayoutManager 后续使用
    rvLayoutParams.mViewHolder = holder;
    // 标记如果该 holder 来自 scrap 或缓存且经过绑定,则需要等待invalidate
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
    // 平时我们可以通过holder.itemView获取到与Holder对应的View,其实ItemView也可以通过layoutParams上的mViewHolder来获取对应的Holder
    return holder;
}

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    // 获取 mAttachedScrap 列表中的 ViewHolder 数量
    final int scrapCount = mAttachedScrap.size();

    // 1. 首先尝试在 mAttachedScrap 中查找一个符合条件的 ViewHolder
    // 遍历 mAttachedScrap 中的所有 ViewHolder,这些代码在pre-layout和post-layout的时候都会执行
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        // 条件判断:
        //  a. holder 没有被标记为已从 scrap 返回过(避免重复使用)
        //  b. holder 的布局位置与请求的位置一致
        //  c. holder 没有被标记为无效
        //  d. 如果处于预布局阶段,则允许任意状态;如果不是预布局阶段,则该 holder 不能处于被移除状态
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            // 标记该 ViewHolder 为“已从 scrap 返回”,防止重复使用
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    // 2. 如果 dryRun 为 false,则尝试从隐藏的 view 中查找 ViewHolder
    if (!dryRun) {
        // 从 mChildHelper 中查找一个隐藏且未被移除的 view,其布局位置为 position
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            // 获取该 view 对应的 ViewHolder
            final ViewHolder vh = getChildViewHolderInt(view);
            // 取消隐藏该 view
            mChildHelper.unhide(view);
            // 获取该 view 在父 ViewGroup 中的布局索引
            int layoutIndex = mChildHelper.indexOfChild(view);
            if (layoutIndex == RecyclerView.NO_POSITION) {
                // 如果 index 无效,则抛出异常
                throw new IllegalStateException("layout index should not be -1 after "
                        + "unhiding a view:" + vh);
            }
            // 将该 view 从父 ViewGroup 中分离
            mChildHelper.detachViewFromParent(layoutIndex);
            // 将该 view 添加到 scrap(暂存)列表中,以便后续复用
            scrapView(view);
            // 给该 ViewHolder 添加两个标记:
            // FLAG_RETURNED_FROM_SCRAP:表示已从 scrap 中返回
            // FLAG_BOUNCED_FROM_HIDDEN_LIST:表示该 ViewHolder曾来自隐藏列表
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            return vh;
        }
    }

    // 3. 最后,在一级缓存 mCachedViews 中查找符合条件的 ViewHolder
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // 检查条件:
        //  a. holder 必须有效(没有被标记为无效)
        //  b. holder 的布局位置与请求的位置一致
        //  为什么一直强调通过position就能找到ViewHolder?除了position还有ViewType的类型也可以,但是通过position找到的ViewHolder是不需要Bind的,因为绑定的数据都是正确的。
        if (!holder.isInvalid() && holder.getLayoutPosition() == position) {
            // 如果 dryRun 为 false,则从缓存中移除该 ViewHolder,防止重复使用
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            if (DEBUG) {
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                        + ") found match in cache: " + holder);
            }
            return holder;
        }
    }
    // 如果以上所有途径都没有找到合适的 ViewHolder,则返回 null
    return null;
}

4.5 notifyDataSetChanged() 对缓存的影响

  • 现象:

    • 调用 notifyDataSetChanged() 后,所有 ViewHolder 都会被放入 RecycledViewPool
    • 默认情况下,每个 viewType 缓存数量为 5,超出部分将被丢弃
    • 再次展示时需要调用 Adapter 的 onCreateViewHolder() 创建新的 ViewHolder,带来额外开销

4.6 稳定 ID 与缓存复用

  • 优化策略:

    • 通过调用 Adapter 的 setHasStableIds(true) 并正确重写 getItemId()
    • 可以使得 ViewHolder 进入 Scrap 层而非 RecycledViewPool
    • 这样 RecyclerView 可利用多层缓存更有效地复用 ViewHolder,避免不必要的创建开销

5. 同一个页面上多个recyclerView共享RecyclerViewPool

下面是一个简单示例,展示如何在同一页面上让多个 RecyclerView 共享同一个 RecycledViewPool,从而实现 ViewHolder 的跨 RecyclerView 复用。

假设你的布局文件 activity_main.xml 中有两个 RecyclerView:

<!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" 
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView1"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView2"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1" />

</LinearLayout>

接下来在 Activity 中这样实现:

public class MainActivity extends AppCompatActivity {

    private RecyclerView recyclerView1;
    private RecyclerView recyclerView2;
    // 创建一个共享的 RecycledViewPool 对象
    private RecyclerView.RecycledViewPool sharedPool;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

         // 初始化 RecyclerView
        recyclerView1 = findViewById(R.id.recyclerView1);
        recyclerView2 = findViewById(R.id.recyclerView2);

        // 初始化共享的 RecycledViewPool
        sharedPool = new RecyclerView.RecycledViewPool();
        // 如果需要,还可以设置每种 viewType 的最大缓存数量
        // 例如:sharedPool.setMaxRecycledViews(0, 10);

        // 将共享池设置给两个 RecyclerView
        recyclerView1.setRecycledViewPool(sharedPool);
        recyclerView2.setRecycledViewPool(sharedPool);

        // 为 RecyclerView 设置 LayoutManager
        recyclerView1.setLayoutManager(new LinearLayoutManager(this));
        recyclerView2.setLayoutManager(new LinearLayoutManager(this));

        // 为两个 RecyclerView 设置 Adapter
        // 这里假设使用相同的 Adapter 类型,不同的数据列表
        List<String> data1 = Arrays.asList("A", "B", "C", "D");
        List<String> data2 = Arrays.asList("1", "2", "3", "4");
        MyAdapter adapter1 = new MyAdapter(data1);
        MyAdapter adapter2 = new MyAdapter(data2);
        recyclerView1.setAdapter(adapter1);
        recyclerView2.setAdapter(adapter2);
    }
}

这里的 MyAdapter 是一个简单的 RecyclerView.Adapter,例如:

public class MyAdapter extends RecyclerView.Adapter<MyAdapter.MyViewHolder> {

    private List<String> items;

    public MyAdapter(List<String> items) {
        this.items = items;
    }

    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        // Inflate item 布局
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_layout, parent, false);
        return new MyViewHolder(view);
    }

    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        holder.bind(items.get(position));
    }

    @Override
    public int getItemCount() {
        return items.size();
    }

    static class MyViewHolder extends RecyclerView.ViewHolder {

        private TextView textView;
        public MyViewHolder(@NonNull View itemView) {
            super(itemView);
            // 假设 item_layout.xml 中有一个 TextView,id 为 textView
            textView = itemView.findViewById(R.id.textView);
        }

        public void bind(String text) {
            textView.setText(text);
        }
    }
}

通过这种方式,两个 RecyclerView 共用同一个 RecycledViewPool,当其中一个 RecyclerView 回收了 ViewHolder 时,另一个 RecyclerView 也能从共享池中获取 ViewHolder,从而提升内存复用效率。

// 核心源码,获取ViewHolder
@Nullable
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
        boolean dryRun, long deadlineNs) {
    if (position < 0 || position >= mState.getItemCount()) {
        throw new IndexOutOfBoundsException("Invalid item position " + position
                + "(" + position + "). Item count:" + mState.getItemCount());
    }
    boolean fromScrapOrHiddenOrCache = false;
    ViewHolder holder = null;
    // 0) If there is a changed scrap, try to find from there, 如果使用的是postlayout,这里不会触发
    if (mState.isPreLayout()) {
        holder = getChangedScrapViewForPosition(position);
        fromScrapOrHiddenOrCache = holder != null;
    }
    // 1) Find by position from scrap/hidden list/cache
    if (holder == null) {
        holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun); // hidden,因为某些原因正在做离场,但是又需要立刻回来的item,可以在hidden中找到。
        if (holder != null) {
            if (!validateViewHolderForOffsetPosition(holder)) {
                // recycle holder (and unscrap if relevant) since it can't be used
                if (!dryRun) {
                    // we would like to recycle this but need to make sure it is not used by
                    // animation logic etc.
                    holder.addFlags(ViewHolder.FLAG_INVALID);
                    if (holder.isScrap()) {
                        removeDetachedView(holder.itemView, false);
                        holder.unScrap();
                    } else if (holder.wasReturnedFromScrap()) {
                        holder.clearReturnedFromScrapFlag();
                    }
                    recycleViewHolderInternal(holder);
                }
                holder = null;
            } else {
                fromScrapOrHiddenOrCache = true;
            }
        }
    }
    if (holder == null) {
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
        if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {
            throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "
                    + "position " + position + "(offset:" + offsetPosition + ")."
                    + "state:" + mState.getItemCount());
        }
		// 通过position已经无法找到ViewHolder了,只能通过ViewType在回收池里边去找。
        final int type = mAdapter.getItemViewType(offsetPosition);
        // 2) Find from scrap/cache via stable ids, if exists
		// 通过setHasStableIds,重新布局的时候,所有的View都不会进入回收池,而是会进入Scrap里边,
		// 这样我们也需要重写getItemId,根据数据获取,才能取得正常的效果。StableIds配合getItemId就能够避免重新创建溢出回收池的糟糕的问题。通过这种方式,我们调用NotifyDataSetChanged也能拥有动画,
        if (mAdapter.hasStableIds()) { 
            holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                    type, dryRun);
            if (holder != null) {
                // update position
                holder.mPosition = offsetPosition;
                fromScrapOrHiddenOrCache = true;
            }
        }
		// 而如果没有设置StableId,mViewCacheExtension是需要自己设置的一级缓存。默认是没有的。可以自定义的缓存。
        if (holder == null && mViewCacheExtension != null) {
            // We are NOT sending the offsetPosition because LayoutManager does not
            // know it.
            final View view = mViewCacheExtension
                    .getViewForPositionAndType(this, position, type);
            if (view != null) {
                holder = getChildViewHolder(view);
                if (holder == null) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view which does not have a ViewHolder");
                } else if (holder.shouldIgnore()) {
                    throw new IllegalArgumentException("getViewForPositionAndType returned"
                            + " a view that is ignored. You must call stopIgnoring before"
                            + " returning this view.");
                }
            }
        }
        if (holder == null) { // fallback to pool
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                        + position + ") fetching from shared pool");
            }
			// 回收池,通过ViewType找到ViewHolder
            holder = getRecycledViewPool().getRecycledView(type);
            if (holder != null) {
                holder.resetInternal();
                if (FORCE_INVALIDATE_DISPLAY_LIST) {
                    invalidateDisplayListInt(holder);
                }
            }
        }
		// 还找不到,就只能创建了。
        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);
            if (ALLOW_THREAD_GAP_WORK) {
                // only bother finding nested RV if prefetching
                RecyclerView innerView = findNestedRecyclerView(holder.itemView);
                if (innerView != null) {
                    holder.mNestedRecyclerView = new WeakReference<>(innerView);
                }
            }

            long end = getNanoTime();
            mRecyclerPool.factorInCreateTime(type, end - start);
            if (DEBUG) {
                Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
            }
        }
    }

    // This is very ugly but the only place we can grab this information
    // before the View is rebound and returned to the LayoutManager for post layout ops.
    // We don't need this in pre-layout since the VH is not updated by the LM.
    if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
            .hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
        holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        if (mState.mRunSimpleAnimations) {
            int changeFlags = ItemAnimator
                    .buildAdapterChangeFlagsForAnimations(holder);
            changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
            final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
                    holder, changeFlags, holder.getUnmodifiedPayloads());
            recordAnimationInfoIfBouncedHiddenView(holder, info);
        }
    }

    boolean bound = false;
	// 判断是数据是否已经绑定切可用?
    if (mState.isPreLayout() && holder.isBound()) {
        // do not update unless we absolutely have to.
        holder.mPreLayoutPosition = position;
    } else if (!holder.isBound()/*刚刚创建的ViewHolder*/ || holder.needsUpdate()/*数据改变的ViewHolder*/ || holder.isInvalid()/*从回收池等地方拿到的ViewHolder*/) {
        if (DEBUG && holder.isRemoved()) {
            throw new IllegalStateException("Removed holder should be bound and it should"
                    + " come here only in pre-layout. Holder: " + holder);
        }
        final int offsetPosition = mAdapterHelper.findPositionOffset(position);
		// 完成View和数据的绑定。
        bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
    }

    final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
    final LayoutParams rvLayoutParams;
    if (lp == null) {
        rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else if (!checkLayoutParams(lp)) {
        rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
        holder.itemView.setLayoutParams(rvLayoutParams);
    } else {
        rvLayoutParams = (LayoutParams) lp;
    }
    rvLayoutParams.mViewHolder = holder;
    rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
	// 平时我们可以通过holder.itemView来获取与holder对应的View,同样的,itemView也可以通过layoutParams上的mViewHolder反向的获取到ViewHolder对象。
    return holder;
}


ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
    final int scrapCount = mAttachedScrap.size();

    // Try first for an exact, non-invalid match from scrap.
    for (int i = 0; i < scrapCount; i++) { // pre-layout和post-layout都会执行
        final ViewHolder holder = mAttachedScrap.get(i); //
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }

    if (!dryRun) {
        View view = mChildHelper.findHiddenNonRemovedView(position);
        if (view != null) {
            // This View is good to be used. We just need to unhide, detach and move to the
            // scrap list.
            final ViewHolder vh = getChildViewHolderInt(view);
            mChildHelper.unhide(view);
            int layoutIndex = mChildHelper.indexOfChild(view);
            if (layoutIndex == RecyclerView.NO_POSITION) {
                throw new IllegalStateException("layout index should not be -1 after "
                        + "unhiding a view:" + vh);
            }
            mChildHelper.detachViewFromParent(layoutIndex);
            scrapView(view);
            vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                    | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
            return vh;
        }
    }

    // Search in our first-level recycled view cache.,这个才算是第一层缓存, 前边的scrap并不算,只能算做是临时缓存(暂存区)
    final int cacheSize = mCachedViews.size();
    for (int i = 0; i < cacheSize; i++) {
        final ViewHolder holder = mCachedViews.get(i);
        // invalid view holders may be in cache if adapter has stable ids as they can be
        // retrieved via getScrapOrCachedViewForId
        if (!holder.isInvalid() && holder.getLayoutPosition() == position) { // 通过position就能找到ViewHolder,通过position找到的ViewHolder都是bind过的,数据都是正确的,而通过Viewtype找到的ViewHolder需要重新绑定数据才能保证数据不会错乱。
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            if (DEBUG) {
                Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                        + ") found match in cache: " + holder);
            }
            return holder;
        }
    }
    return null;
}

总结图

该图来自参考文献的博客,对于列表滚动和布局时的基本复用逻辑,这张图已经足够帮助了解核心流程:

image.png

这张图已经很好地概括了 RecyclerView 在布局(layout)和填充(fill)流程中,对 ViewHolder 进行「回收与复用」的主干逻辑,包括:

  1. detachAndScrapAttachedViews 把当前屏幕上的子 View 移除并放入 mAttachedScrap
  2. fill / layoutChunk / next 等过程会调用 tryGetViewHolderForPositionByDeadline,依次尝试从 mChangedScrapmAttachedScrapmCachedViewsmRecyclerPool 获取可复用的 ViewHolder。
  3. 若所有缓存都无法满足,则会调用 createViewHolder 创建新的 ViewHolder。
  4. 布局完成后,再把最终需要的 ViewHolder 加回到屏幕(addView)。

涵盖了常见情况下的回收复用流程,尤其是对多级缓存(mChangedScrapmAttachedScrapmCachedViewsmRecyclerPool)的使用顺序进行了直观展示。

不过,有部分高级或边缘场景(如动画、预布局、prefetch、自定义扩展缓存等)并未在图中展示

参考文献

juejin.cn/post/698497…

可以,这次我按你这篇 MD 的内容出一套 30 道题,覆盖面尽量全,但每题都控制得比较精炼。内容覆盖:

ListView 缓存
RecyclerView 核心组件
LayoutManager / Recycler / Adapter 分工
detachAndScrapAttachedViews
mAttachedScrap / mChangedScrap
mCachedViews
RecycledViewPool
ViewCacheExtension
tryGetViewHolderForPositionByDeadline
notifyDataSetChanged
stableId
多个 RecyclerView 共享 RecyclerViewPool

RecyclerView 缓存与复用机制练习题

1. ListView 的二级缓存是哪两个?

题目:

ListView 中常说的二级缓存主要指哪两个结构?

A. mAttachedScrapmChangedScrap
B. mActiveViewsmScrapViews
C. mCachedViewsmRecyclerPool
D. mViewCacheExtensionmRecyclerPool

答案:B

解析:

ListView 的缓存核心在 RecycleBin 中:

private View[] mActiveViews;
private ArrayList<View>[] mScrapViews;

其中:

mActiveViews:
屏幕上当前可见的 View,主要用于布局过程中快速复用。

mScrapViews:
已经滑出屏幕、可以作为 convertView 复用的 View

mActiveViews 更像是当前布局期间的临时活跃视图数组,mScrapViews 才是更常用的滑动复用池。


2. ListView 的 mActiveViews 什么时候发挥作用?

题目:

mActiveViews 主要在什么情况下发挥作用?

A. 数据发生变化时
B. 调用 notifyDataSetChanged()
C. 数据没变,只是触发重新布局时
D. ViewType 发生变化时

答案:C

解析:

mActiveViews 的典型场景是:

数据没有发生变化
只是触发了 requestLayout 等重新布局流程

这时屏幕上的 View 内容没有变,可以直接从 mActiveViews 中取出来复用,不需要重新创建,也不需要重新绑定。

但是如果调用了类似 notifyDataSetChanged(),ListView 会认为数据发生变化,此时更多会走 mScrapViews,需要重新绑定数据。


3. ListView 的 mScrapViews 为什么要求 ViewType 从 0 开始且连续?

题目:

为什么 ListView 中的 mScrapViews 要求 ViewType 从 0 开始并且连续?

答案:

因为 mScrapViews 本质是一个数组,ViewType 会作为数组下标使用。

解析:

ListView 中:

private ArrayList<View>[] mScrapViews;

每一种 ViewType 对应数组中的一个位置:

mScrapViews[0]viewType = 0 的缓存列表
mScrapViews[1]viewType = 1 的缓存列表
mScrapViews[2]viewType = 2 的缓存列表

所以如果 ViewType 不连续,比如直接返回 R.layout.xxx 这种大整数,就可能造成数组越界或浪费。

这和 RecyclerView 不同,RecyclerView 的 RecycledViewPool 是按 ViewType 映射管理,不要求 ViewType 是连续数组下标。


4. RecyclerView 相比 ListView 的优势在哪里?

题目:

RecyclerView 相比 ListView 的主要优势不包括下面哪一项?

A. 更灵活的 LayoutManager
B. 更完善的 ViewHolder 复用机制
C. 支持 ItemAnimator 动画
D. 不需要 Adapter

答案:D

解析:

RecyclerView 仍然需要 Adapter。

RecyclerView 的优势主要在:

1. LayoutManager 可插拔,支持线性、网格、瀑布流、自定义布局
2. ViewHolder 机制更完整
3. 缓存层级更丰富
4. 支持局部刷新
5. 支持 ItemAnimator
6. 支持 ItemDecoration
7. 支持多个 RecyclerView 共享 RecycledViewPool

所以不是“不需要 Adapter”,而是把职责拆得更清楚。


5. LayoutManager 只负责布局吗?

题目:

LayoutManager 是否只负责 item 的测量和布局?

答案:

不是。它还负责滚动处理、决定何时回收 View、向 Recycler 请求 View 等。

解析:

LayoutManager 的职责包括:

1. 测量 itemView
2. 布局 itemView
3. 处理滚动方向和滚动距离
4. 决定哪些 View 需要回收
5. 通过 getViewForPosition() 向 Recycler 请求 View

所以 LayoutManager 是 RecyclerView 布局和回收流程中的核心调度者之一。


6. RecyclerView 的标准运行流程是什么?

题目:

请按顺序描述 RecyclerView 展示一个 item 的基本流程。

答案:

LayoutManager 请求 View
    ↓
Recycler 查找可复用 ViewHolder
    ↓
没有可复用对象时,Adapter 创建 ViewHolder
    ↓
Adapter 绑定数据
    ↓
LayoutManager 测量、布局并展示 itemView

解析:

核心链路是:

LayoutManager.getViewForPosition(position)

内部会交给 Recycler:

Recycler.tryGetViewHolderForPositionByDeadline()

如果没有合适的缓存,就会调用:

Adapter.onCreateViewHolder()
Adapter.onBindViewHolder()

最后把 ViewHolder.itemView 交给 LayoutManager 布局到屏幕上。


7. LayoutManager 直接管理的是 ViewHolder 吗?

题目:

LayoutManager 直接管理的是 ViewHolder 还是 View?

答案:

LayoutManager 直接管理的是 View,不是 ViewHolder

解析:

从源码调用可以看出来:

public View getViewForPosition(int position) {
    return getViewForPosition(position, false);
}

View getViewForPosition(int position, boolean dryRun) {
    return tryGetViewHolderForPositionByDeadline(...).itemView;
}

Recycler 内部拿到的是 ViewHolder,但最终返回给 LayoutManager 的是:

ViewHolder.itemView

所以可以这样理解:

RecyclerView / Recycler:关心 ViewHolder
LayoutManager:关心 View
Adapter:负责创建和绑定 ViewHolder

8. Recycler 的职责是什么?

题目:

RecyclerView 中的 Recycler 主要负责什么?

A. 绘制分割线
B. 管理 ViewHolder 的缓存、回收和复用
C. 决定 item 动画
D. 保存业务数据

答案:B

解析:

Recycler 可以理解为 RecyclerView 内部的“回收管理器”。

它负责:

1. 保存不同层级的 ViewHolder 缓存
2. 根据 position / viewType / stableId 查找可复用 ViewHolder
3. 负责把不再显示的 ViewHolder 放入缓存
4. 必要时从 RecycledViewPool 中取 ViewHolder
5. 没有缓存时触发 Adapter 创建新的 ViewHolder

它类似于 ListView 中的 RecycleBin,但机制更复杂。


9. Adapter 的核心职责是什么?

题目:

Adapter 的核心职责是什么?

A. 负责 item 的布局坐标计算
B. 负责将数据转换成 ViewHolder,并绑定数据
C. 负责管理 RecyclerView 的滚动
D. 负责执行 item 动画

答案:B

解析:

Adapter 主要负责两个关键方法:

onCreateViewHolder()
onBindViewHolder()

分别表示:

onCreateViewHolder:
创建 ViewHolder 和 itemView。

onBindViewHolder:
把 position 对应的数据绑定到 ViewHolder 上。

Adapter 不负责 item 摆放位置,这个由 LayoutManager 负责。


10. ItemAnimator 和 ItemDecoration 分别负责什么?

题目:

ItemAnimatorItemDecoration 的作用分别是什么?

答案:

ItemAnimator:
负责 item 增删改移动时的动画效果。

ItemDecoration:
负责对 item 进行额外绘制或偏移,比如分割线、间距、高亮、自定义滚动条等。

解析:

两者都不是缓存层。

ItemAnimator 关注状态变化:

add / remove / move / change

ItemDecoration 关注绘制增强:

分割线
间距
背景
吸顶效果辅助

11. detachAndScrapAttachedViews() 的作用是什么?

题目:

detachAndScrapAttachedViews(recycler) 的主要作用是什么?

A. 销毁所有 ViewHolder
B. 把当前屏幕上的 itemView 临时 detach,并放入 Scrap 暂存区
C. 直接把所有 ViewHolder 放入 RecycledViewPool
D. 清空 Adapter 数据

答案:B

解析:

在重新布局时,RecyclerView 会先把当前屏幕上的子 View “拿下来”:

detachAndScrapAttachedViews(recycler);

这些 View 不会立刻销毁,而是进入 Scrap:

mAttachedScrap
mChangedScrap

后续 LayoutManager 重新布局时,如果还能用,就直接从 Scrap 中拿回来放回屏幕。

所以它更像:

临时拆下 → 临时保存 → 重新布局时快速复用

12. mAttachedScrap 是什么?

题目:

mAttachedScrap 的作用是什么?

A. 长期缓存屏幕外的 ViewHolder
B. 保存当前布局过程中 detach 下来的正常 ViewHolder
C. 按 viewType 缓存 ViewHolder
D. 专门给多个 RecyclerView 共享

答案:B

解析:

mAttachedScrap 是布局过程中的临时暂存区。

典型场景:

RecyclerView 重新布局
    ↓
当前屏幕上的 View 被 detach
    ↓
正常的 ViewHolder 进入 mAttachedScrap
    ↓
后续布局时按 position 重新拿回来使用

它不是长期缓存池,用完通常会被移除。

所以严格来说,它更像“布局过程中的暂存区”,而不是传统意义上的长期缓存。


13. mChangedScrap 是什么?

题目:

mChangedScrap 主要用于什么场景?

A. 普通滑动复用
B. viewType 缓存
C. 数据变化、预布局、动画场景中保存旧状态 ViewHolder
D. 多个 RecyclerView 共享缓存

答案:C

解析:

mChangedScrap 主要和:

pre-layout
notifyItemChanged
ItemAnimator
旧状态 ViewHolder

这些场景有关。

当 item 数据发生变化时,RecyclerView 可能需要旧 ViewHolder 参与动画,比如 change animation。此时旧状态的 ViewHolder 会进入 mChangedScrap

它更多服务于:

预布局阶段
动画计算
局部更新

14. mAttachedScrap 和 mChangedScrap 能不能算缓存?

题目:

mAttachedScrapmChangedScrap 能否算 RecyclerView 缓存?

答案:

可以广义上算缓存,但更准确地说,它们是布局过程中的临时暂存区,不是长期复用缓存。

解析:

如果从“避免重新创建 ViewHolder”的角度看,它们确实有缓存效果。

但是它们和 mCachedViewsRecycledViewPool 不一样:

mAttachedScrap / mChangedScrap:
当前 layout / pre-layout 阶段临时使用,用完即移除。

mCachedViews / RecycledViewPool:
用于滑动过程或后续布局中的长期复用。

所以面试时可以这样说:

Scrap 层是 RecyclerView 布局期间的临时复用区,不是传统意义上的长期缓存池。


15. RecyclerView 常说的“四级缓存”是哪四个?

题目:

RecyclerView 常说的四级缓存包括哪些?

答案:

1. mAttachedScrap
2. mChangedScrap
3. mCachedViews
4. RecycledViewPool / mRecyclerPool

解析:

常见说法是四级缓存,但要注意:

mAttachedScrap、mChangedScrap:
更偏向临时 scrap 暂存区。

mCachedViews:
真正意义上的一级滑动缓存。

RecycledViewPool:
按 viewType 分类的回收池。

另外还有一个扩展层:

ViewCacheExtension

它是开发者自定义缓存层,但一般使用较少。


16. mCachedViews 的作用是什么?

题目:

mCachedViews 主要用于什么?

A. 保存屏幕滑动过程中刚移出屏幕、可能马上回来复用的 ViewHolder
B. 保存所有被删除的 ViewHolder
C. 按 viewType 跨 RecyclerView 共享
D. 保存旧状态动画 ViewHolder

答案:A

解析:

mCachedViews 主要优化滑动性能。

比如用户向上滑动,顶部 item 滑出屏幕:

这个 ViewHolder 会先进入 mCachedViews

如果用户马上向下滑回来,RecyclerView 可以按 position 找到它,直接复用。

命中 mCachedViews 时:

position 通常是正确的
不需要重新 onBindViewHolder

所以它对回滚场景优化明显。


17. mCachedViews 默认大小是多少?如何修改?

题目:

mCachedViews 默认缓存数量是多少?如何修改?

答案:

默认大小是 2,可以通过:

recyclerView.setItemViewCacheSize(size);

修改。

解析:

源码中默认值类似:

static final int DEFAULT_CACHE_SIZE = 2;

这个大小由:

mViewCacheMax

控制。

如果某些场景下滑动回滚非常频繁,可以适当增大这个值。但不能无脑加大,否则会占用更多内存。


18. 命中 mCachedViews 后是否需要重新绑定数据?

题目:

mCachedViews 中取到 ViewHolder 后,一定需要调用 onBindViewHolder() 吗?

答案:

通常不需要。

解析:

mCachedViews 是按 position 保存可直接复用的 ViewHolder。

命中时说明:

这个 ViewHolder 对应的位置和数据仍然匹配

所以可以直接拿来用,不必重新绑定数据。

这也是它比 RecycledViewPool 更快的原因:

mCachedViews:
不需要 create,也通常不需要 bind。

RecycledViewPool:
不需要 create,但通常需要 bind

19. RecycledViewPool 的作用是什么?

题目:

RecycledViewPool 主要负责什么?

A. 根据 position 缓存 ViewHolder
B. 根据 viewType 缓存 ViewHolder
C. 保存当前屏幕上的 ViewHolder
D. 保存 Adapter 数据

答案:B

解析:

RecycledViewPool 是按 viewType 分类的缓存池。

大致结构可以理解为:

viewType = 0 → 一组 ViewHolder
viewType = 1 → 一组 ViewHolder
viewType = 2 → 一组 ViewHolder

从池里取出来的 ViewHolder:

类型是对的
但 position 和数据不一定对

所以通常需要重新执行:

onBindViewHolder()

20. RecycledViewPool 默认每种 viewType 缓存几个?

题目:

RecycledViewPool 默认每个 viewType 缓存几个 ViewHolder?如何修改?

答案:

默认每种 viewType 缓存 5 个,可以通过:

recycledViewPool.setMaxRecycledViews(viewType, max);

修改。

解析:

例如:

RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
pool.setMaxRecycledViews(0, 10);

表示:

viewType = 0 的 ViewHolder 最多缓存 10

这个在复杂列表、嵌套列表、多个 RecyclerView 共享缓存时很有用。


21. mCachedViews 和 RecycledViewPool 的核心区别是什么?

题目:

请说明 mCachedViewsRecycledViewPool 的核心区别。

答案:

mCachedViews:
按 position 复用,命中后通常不需要重新绑定。

RecycledViewPool:
按 viewType 复用,命中后通常需要重新绑定。

解析:

对比:

缓存层匹配依据是否需要重新 bind默认大小
mCachedViewsposition通常不需要2
RecycledViewPoolviewType通常需要每个 viewType 5

所以性能上:

mCachedViews 命中成本更低
RecycledViewPool 命中成本稍高,但复用范围更广

22. ViewCacheExtension 是什么?

题目:

ViewCacheExtension 的作用是什么?

A. RecyclerView 默认必须使用的缓存层
B. 允许开发者自定义缓存逻辑
C. 替代 Adapter
D. 替代 LayoutManager

答案:B

解析:

ViewCacheExtension 是 RecyclerView 提供给开发者的自定义缓存扩展点。

它可以让开发者自己决定是否返回某个 position 对应的 View。

但是实际项目中使用较少,因为:

1. 使用复杂
2. 容易破坏 RecyclerView 默认复用逻辑
3. 大多数场景 mCachedViews + RecycledViewPool 已经够用

所以知道它存在即可。


23. tryGetViewHolderForPositionByDeadline() 的作用是什么?

题目:

tryGetViewHolderForPositionByDeadline() 的核心作用是什么?

A. 只负责创建新的 ViewHolder
B. 按多级缓存策略获取一个可用于 position 的 ViewHolder
C. 只负责绘制 item 分割线
D. 只负责执行动画

答案:B

解析:

这是 RecyclerView 复用机制的核心方法。

它会根据当前状态,尝试从多处获取 ViewHolder:

1. mChangedScrap
2. mAttachedScrap / hidden / mCachedViews
3. stableId 相关缓存
4. ViewCacheExtension
5. RecycledViewPool
6. Adapter.createViewHolder()

如果拿到的 ViewHolder 需要绑定数据,还会触发:

tryBindViewHolderByDeadline()

最终返回一个可以交给 LayoutManager 使用的 ViewHolder.itemView


24. RecyclerView 获取 ViewHolder 的大致顺序是什么?

题目:

请写出 RecyclerView 查找可复用 ViewHolder 的大致顺序。

答案:

大致顺序是:

1. 如果是 pre-layout,先查 mChangedScrap
2. 查 mAttachedScrap / hidden view / mCachedViews
3. 如果 Adapter 有 stableId,尝试根据 itemId 查找
4. 查 ViewCacheExtension
5. 查 RecycledViewPool
6. 都没有命中,则调用 Adapter.onCreateViewHolder()

解析:

这个顺序体现了 RecyclerView 的优化策略:

越靠前,复用成本越低
越靠后,复用成本越高

比如:

Scrap / CachedViews:
可能不需要重新绑定。

RecycledViewPool:
一般需要重新绑定。

onCreateViewHolder:
成本最高,因为要 inflate / 创建 ViewHolder。

所以 RecyclerView 会优先找更“近”、更准确的缓存。


25. 为什么 RecycledViewPool 中取出的 ViewHolder 通常需要重新 bind?

题目:

为什么从 RecycledViewPool 中取出的 ViewHolder 通常需要重新绑定数据?

答案:

因为 RecycledViewPool 只保证 ViewHolder 的 viewType 匹配,不保证 position 和数据仍然匹配。

解析:

RecycledViewPool 是按 viewType 分类的。

例如:

position = 1,viewType = 0
position = 8,viewType = 0

它们可以复用同一种布局,但数据内容不同。

所以从 Pool 中拿到 ViewHolder 后,必须通过:

onBindViewHolder(holder, position)

重新绑定当前 position 对应的数据。


26. notifyItemChanged() 相比 notifyDataSetChanged() 有什么优势?

题目:

为什么推荐使用 notifyItemChanged(),而不是无脑使用 notifyDataSetChanged()

答案:

因为 notifyItemChanged() 是局部刷新,只会更新指定 item;notifyDataSetChanged() 会让 RecyclerView 认为整体数据都变了,导致更多 ViewHolder 重新绑定甚至进入回收池。

解析:

notifyItemChanged(position) 的好处是:

谁变了,刷新谁。
未变化的 item 可以继续复用,不必重新 bind

notifyDataSetChanged() 的问题是:

RecyclerView 无法知道具体哪些 item 变了
只能按整体变化处理
可能导致所有可见 ViewHolder 重新处理

所以局部更新是 RecyclerView 相比 ListView 的重要优势之一。


27. notifyDataSetChanged() 对缓存有什么影响?

题目:

调用 notifyDataSetChanged() 后,ViewHolder 通常会发生什么?

A. 所有 ViewHolder 都一定留在 mCachedViews
B. ViewHolder 可能被放入 RecycledViewPool,默认每个 viewType 最多缓存 5 个,超出可能被丢弃
C. 所有 ViewHolder 都不会重新绑定
D. Adapter 不会收到任何回调

答案:B

解析:

notifyDataSetChanged() 会让 RecyclerView 无法精确知道变化范围。

这会导致大量 ViewHolder 失去“位置准确性”,进入更低层的缓存,比如 RecycledViewPool

而 Pool 默认每个 viewType 容量为:

5

超出的 ViewHolder 可能被丢弃。

后续再次展示时,如果 Pool 不够,就可能重新:

onCreateViewHolder()
onBindViewHolder()

造成额外开销。


28. StableId 是什么?它如何优化复用?

题目:

setHasStableIds(true)getItemId() 的作用是什么?

答案:

它们告诉 RecyclerView:每个 item 有稳定不变的唯一 ID。这样数据变化时,RecyclerView 可以根据 itemId 更准确地匹配原来的 ViewHolder,提高复用效率。

解析:

使用方式:

adapter.setHasStableIds(true)

override fun getItemId(position: Int): Long {
    return data[position].id
}

如果 StableId 正确,RecyclerView 在数据变化时可以判断:

虽然 position 可能变了
但 itemId 没变
说明还是同一个数据项

这样 ViewHolder 更可能进入 Scrap 层进行复用,而不是直接进入 RecycledViewPool,从而减少不必要的创建和绑定。


29. 使用 StableId 时最容易犯什么错误?

题目:

使用 setHasStableIds(true) 时,下面哪种写法是错误倾向?

A. 用数据库主键作为 itemId
B. 用服务端返回的唯一 id 作为 itemId
C. 直接返回 position 作为 itemId
D. 保证同一个 item 的 id 长期稳定

答案:C

解析:

StableId 的核心是:

同一个数据项,不管位置怎么变,它的 id 都应该不变。

如果直接返回 position:

override fun getItemId(position: Int): Long {
    return position.toLong()
}

当插入、删除、移动 item 后,同一个数据项的 position 会变化。

这就破坏了 stableId 的语义,可能导致复用错误、动画异常或数据显示错乱。


30. 多个 RecyclerView 如何共享 RecycledViewPool?

题目:

同一个页面中,多个 RecyclerView 如何共享同一个 RecycledViewPool

答案:

创建同一个 RecycledViewPool 对象,然后分别设置给多个 RecyclerView。

解析:

示例:

RecyclerView.RecycledViewPool sharedPool =
        new RecyclerView.RecycledViewPool();

recyclerView1.setRecycledViewPool(sharedPool);
recyclerView2.setRecycledViewPool(sharedPool);

如果需要,还可以设置每种 viewType 的最大缓存数量:

sharedPool.setMaxRecycledViews(0, 10);

这样多个 RecyclerView 之间可以复用相同 viewType 的 ViewHolder。

常见场景:

首页多个横向列表
嵌套 RecyclerView
多个结构相同的列表模块

加强理解题

31. 为什么多个 RecyclerView 共享 Pool 时,ViewType 要特别注意?

题目:

两个 RecyclerView 共享同一个 RecycledViewPool 时,为什么要保证 ViewType 语义一致?

答案:

因为 RecycledViewPool 是按 viewType 缓存 ViewHolder 的。如果两个 Adapter 的同一个 viewType 对应不同布局,就可能错误复用 ViewHolder。

解析:

例如:

RecyclerView A:
viewType = 0 → 商品卡片布局

RecyclerView B:
viewType = 0 → 用户头像布局

如果它们共享同一个 Pool,就可能出现:

B 从 Pool 中取到了 A 的商品卡片 ViewHolder

这显然会出错。

所以共享 Pool 时,要保证:

相同 viewType 表示相同布局结构和 ViewHolder 类型

不然就不要共享,或者重新设计 viewType。


32. 为什么 mCachedViews 默认只缓存 2 个,但还要用 ArrayList?

题目:

mCachedViews 默认只缓存 2 个 ViewHolder,为什么还要设计成 ArrayList

答案:

因为它的容量可以动态调整,不是固定只能存 2 个。

解析:

默认是:

static final int DEFAULT_CACHE_SIZE = 2;

但可以通过:

recyclerView.setItemViewCacheSize(size);

调整。

所以用 ArrayList 可以支持不同场景下的缓存容量变化。

比如:

普通列表:默认 2 个足够
复杂回滚场景:可以适当增大
内存敏感场景:不建议太大

33. mCachedViews 和 ListView 的 mActiveViews 是否一样?

题目:

mCachedViews 和 ListView 的 mActiveViews 是否完全一样?

答案:

不完全一样。它们都可能避免重新绑定,但使用场景不同。

解析:

相似点:

命中后都可以减少 create / bind 成本。

不同点:

mActiveViews:
主要用于数据没变但重新 layout 的场景,使用范围有限。

mCachedViews:
主要用于 RecyclerView 滑动过程中移入 / 移出的 ViewHolder 缓存,尤其优化回滚。

所以 mCachedViews 的实战价值比 mActiveViews 更高。


34. 为什么说 RecyclerView 的复用思想是“能不创建就不创建,能不绑定就不绑定”?

题目:

请解释 RecyclerView 复用机制的核心优化目标。

答案:

RecyclerView 的目标是尽量减少两个高成本操作:

1. onCreateViewHolder()
2. onBindViewHolder()

解析:

创建 ViewHolder 可能涉及:

inflate XML
创建 View 树
findViewById
初始化子 View

绑定数据可能涉及:

设置文本
加载图片
状态重置
监听器绑定
局部 UI 更新

所以 RecyclerView 会优先使用:

Scrap / CachedViews

这些可能不需要重新绑定的缓存。

实在不行才用:

RecycledViewPool

最后才创建新的 ViewHolder。


35. RecyclerView 为什么需要 pre-layout?

题目:

RecyclerView 中 pre-layout 的主要意义是什么?

A. 为动画和变化前后的状态计算提供支持
B. 为了强制创建所有 ViewHolder
C. 为了跳过 Adapter
D. 为了禁止滚动

答案:A

解析:

当 item 发生变化,比如:

插入
删除
移动
改变

RecyclerView 需要知道变化前后的状态,才能做动画。

pre-layout 可以保留旧状态 ViewHolder,用于:

出场动画
change 动画
位置变化动画

mChangedScrap 就经常在 pre-layout 阶段发挥作用。


36. 为什么局部刷新是 RecyclerView 相比 ListView 的重要提升?

题目:

为什么 notifyItemChanged() 这类局部刷新能力很重要?

答案:

因为它可以让 RecyclerView 精确知道哪些 item 变化了,未变化的 item 可以跳过重新绑定,提升性能。

解析:

ListView 时代常用:

notifyDataSetChanged()

这会导致大量 item 重新绑定。

RecyclerView 支持:

notifyItemChanged(position)
notifyItemInserted(position)
notifyItemRemoved(position)
notifyItemMoved(from, to)
notifyItemRangeChanged(start, count)

这样 RecyclerView 可以更精确地复用未变化的 ViewHolder。

所以核心是:

减少无意义的 onBindViewHolder()
保留更多可复用 ViewHolder
配合 ItemAnimator 做动画

最核心总结

这套题的主线可以这样记:

ListView:
mActiveViews + mScrapViews

RecyclerView:
LayoutManager 负责布局和请求 View
Recycler 负责缓存和复用 ViewHolder
Adapter 负责创建和绑定 ViewHolder

RecyclerView 多级复用:
mChangedScrap
mAttachedScrap
mCachedViews
ViewCacheExtension
RecycledViewPool
createViewHolder

复用成本:
Scrap / CachedViews 最低
RecycledViewPool 次之
onCreateViewHolder 最高

优化方向:
能不 create 就不 create
能不 bind 就不 bind
能局部刷新就不要 notifyDataSetChanged
多个同构 RecyclerView 可以共享 RecycledViewPool
StableId 可以提升数据变化时的复用准确性

一句话总结:

RecyclerView 的复用机制,本质上是 LayoutManager 向 Recycler 要 View,Recycler 按“临时 Scrap → 位置缓存 → 自定义缓存 → 类型回收池 → 创建新对象”的顺序寻找 ViewHolder,从而尽可能减少 ViewHolder 创建和数据绑定。