RecyclerView 回收复用机制解析

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

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

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

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

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

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

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

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


0. ListView的缓存机制

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

sql 复制代码
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 的数据结构:

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

  • 第一种, notifyItemChanged,全部更新,所有的 View 都会存入 mScrapViews 里边, 然后重新取出来,执行绑定数据:
  • 第二种,就是滑动了,View 一旦被滑出就界面,就会被回收到 mScrapViews 里边,

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

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

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

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

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

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

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

1. 主要组件

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的运行流程是这样的:

  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的成员变量是一种常见的做法:

例如下边的代码:

kotlin 复制代码
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 仅仅负责根据外部传入的数据来创建和绑定视图, 对上边的逻辑做出如下的调整:

java 复制代码
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()

kotlin 复制代码
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。

kotlin 复制代码
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 就会从缓存列表中取出它们,再按照新的布局要求将它们重新添加到屏幕上。

kotlin 复制代码
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中:

kotlin 复制代码
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 视图。

kotlin 复制代码
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中。

kotlin 复制代码
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中:

kotlin 复制代码
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的公共回收方法中:

kotlin 复制代码
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的大小是空的:

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

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

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

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

  • ListView 中

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

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

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

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

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

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

kotlin 复制代码
@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被移除了屏幕,这个时候会发生什么事情呢?

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

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

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

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

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

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

kotlinSparseArray 复制代码
>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类型

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

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

java 复制代码
// 每一种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 中是不存在的。通过调用

      java 复制代码
      RecyclerView.RecycledViewPool#setMaxRecycledViews(int viewType, int max)

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

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

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

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

kotlin 复制代码
com.android.internal.widget.RecyclerView.Recycler#setRecycledViewPool

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

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

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

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

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

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

重点说明:

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

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

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

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

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 有了质的提升。

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

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

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

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

如果将所有未使用的 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 回收和复用机制,从而保持了职责的清晰分离。

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

严格来说,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 流程。

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

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

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

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

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同时处于列表中。

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

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

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

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

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

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

4.3. ViewCacheExtension

  • 作用:

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

4.4 核心源码流程分析

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

kotlin 复制代码
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的方式:

java 复制代码
// 从下边这两个方法我们就知道,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:

xml 复制代码
<!-- 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 中这样实现:

java 复制代码
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,例如:

java 复制代码
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,从而提升内存复用效率。

kotlin 复制代码
// 核心源码,获取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;
}

总结图

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

这张图已经很好地概括了 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...

相关推荐
PyAIGCMaster3 分钟前
国内 npm 镜像源推荐
前端·npm·node.js
强国6 分钟前
大学生速通前端之入门【第一篇】
前端·javascript
myyyl7 分钟前
typescript中的泛型
前端·javascript·面试
顾言7167 分钟前
Vue2与Vue3的差异
前端
zYear9 分钟前
elpis 一个企业级应用 —— 抽离 npm 包
前端
经常见11 分钟前
深入令人迷惑的JavaScript类型转换
前端·javascript
前端筱园12 分钟前
精通JavaScript:从理解作用域和闭包开始
前端·javascript
Vennn12 分钟前
AccessibilityService-weditor获取节点元素信息&Assists实现自动化
前端
deckcode14 分钟前
CSS基础知识一览
前端
MariaH18 分钟前
CSS过渡与动画
前端