2025再读Android RecyclerView源码

RecyclerView源码浅析

1. RecyclerView简单使用流程

  1. 创建RecyclerView new RecyclerView(context);
  2. 设置LayoutManager recyclerView.setLayoutManager(new LinearLayoutManager(context));
  3. 添加分割线ItemDecoration recyclerView.addItemDecoration(new DividerItemDecoration(context, DividerItemDecoration.VERTICAL));
  4. 设置Adapter recyclerView.setAdapter(new MyAdapter());
  5. 刷新 adapter.notifyDataSetChanged(); adapter.notifyItemChanged(position);

2. 初始化RecyclerView阶段

初始化源码如下:

java 复制代码
// RecyclerView 构造函数,接受 Context、AttributeSet 和 defStyle 参数
public RecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    // 调用父类 ViewGroup 的构造函数
    super(context, attrs, defStyle);

    // 处理 XML 属性中定义的 clipToPadding 属性
    if (attrs != null) {
        // 获取 TypedArray 用于读取 CLIP_TO_PADDING_ATTR 属性
        TypedArray a = context.obtainStyledAttributes(attrs, CLIP_TO_PADDING_ATTR, defStyle, 0);
        // 读取属性值,默认 true(裁剪到 padding)
        mClipToPadding = a.getBoolean(0, true);
        a.recycle(); // 回收 TypedArray
    } else {
        mClipToPadding = true; // 无属性时默认 true
    }

    // 启用滚动容器特性(影响窗口焦点行为)
    setScrollContainer(true);
    // 允许在触摸模式下获取焦点(用于处理键盘导航)
    setFocusableInTouchMode(true);

    // (1)获取 ViewConfiguration 对象(包含系统级 UI 参数)
    final ViewConfiguration vc = ViewConfiguration.get(context);
    mTouchSlop = vc.getScaledTouchSlop(); // 触摸事件识别为滚动的最小距离
    mMinFlingVelocity = vc.getScaledMinimumFlingVelocity(); // 最小抛掷速度
    mMaxFlingVelocity = vc.getScaledMaximumFlingVelocity(); // 最大抛掷速度

    // 如果 OVER_SCROLL_NEVER,则优化绘制性能(跳过 onDraw 调用)
    setWillNotDraw(getOverScrollMode() == View.OVER_SCROLL_NEVER);

    // (2)设置 ItemAnimator 的监听器(用于动画生命周期回调)
    mItemAnimator.setListener(mItemAnimatorListener);
    // (3)初始化 Adapter 管理相关组件
    initAdapterManager();
    // (4)初始化子视图管理辅助类
    initChildrenHelper();

    // 处理可访问性相关配置
    if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
        // 如果未明确指定,默认启用可访问性
        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
    }
    // 获取 AccessibilityManager 服务
    mAccessibilityManager = (AccessibilityManager) getContext()
            .getSystemService(Context.ACCESSIBILITY_SERVICE);
    // 设置自定义的可访问性委托
    setAccessibilityDelegateCompat(new RecyclerViewAccessibilityDelegate(this));

    // 处理嵌套滚动和布局管理器初始化
    boolean nestedScrollingEnabled = true; // 默认启用嵌套滚动

    if (attrs != null) { // 再次处理 XML 属性(布局管理器相关)
        int defStyleRes = 0;
        // 获取 RecyclerView 自定义属性
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.RecyclerView,
                defStyle, defStyleRes);
        // 读取布局管理器类名(例如 LinearLayoutManager)
        String layoutManagerName = a.getString(R.styleable.RecyclerView_layoutManager);
        // 处理子控件焦点优先级
        int descendantFocusability = a.getInt(
                R.styleable.RecyclerView_descendantFocusability, -1);
        if (descendantFocusability == -1) {
            setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // 默认行为
        }
        a.recycle(); // 回收 TypedArray

        // (5)根据属性创建 LayoutManager, 如果layoutManagerName是null, 则说明没有在布局中设置, 后续需要代码调用setLayoutManager
        createLayoutManager(context, layoutManagerName, attrs, defStyle, defStyleRes);

        // 处理嵌套滚动属性(API 21+)
        if (Build.VERSION.SDK_INT >= 21) {
            a = context.obtainStyledAttributes(attrs, NESTED_SCROLLING_ATTRS,
                    defStyle, defStyleRes);
            nestedScrollingEnabled = a.getBoolean(0, true); // 读取嵌套滚动使能状态
            a.recycle();
        }
    } else {
        setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); // 默认无属性时
    }

    // 处理 EdgeEffect 相关属性(虽然立即回收,但可能用于默认样式继承)
    TypedArray a = context.obtainStyledAttributes(attrs,
            com.android.internal.R.styleable.EdgeEffect);
    a.recycle(); // 快速回收,可能后续由 LayoutManager 处理

    // (6) sdk>21支持动态配置, 最终设置嵌套滚动是否启用(兼容所有 API 级别); 如果sdk<21, 默认启用); 如果sdk>=21, 读取属性配置,不设置默认启用;
    setNestedScrollingEnabled(nestedScrollingEnabled);
}
  • (1)处: 获取 ViewConfiguration 对象,读取系统级参数配置
  • (2)处: 设置 ItemAnimator 的监听器(用于动画生命周期回调),如无配置自定义Animator则默认使用 DefaultItemAnimator
  • (3)处: 初始化 Adapter管理相关组件 AdapterHelper
  • (4)处: 初始化子视图管理辅助类 ChildHelper
  • (5)处: 根据属性创建 LayoutManager, 如果layoutManagerName是null, 则说明没有在布局中设置, 后续需要代码调用setLayoutManager
  • (6)处: sdk>21支持动态配置, 最终设置嵌套滚动是否启用(兼容所有 API 级别); 如果sdk<21, 默认启用); 如果sdk>=21, 读取属性配置,不设置默认启用;

3. 设置布局管理器LayoutManager

java 复制代码
// 成员变量视图回收复用机制的核心控制器
final Recycler mRecycler = new Recycler();

    /**
     * 成员变量
     * 保存用于动画的视图数据
     */
final ViewInfoStore mViewInfoStore = new ViewInfoStore();
/**
 * 设置 RecyclerView 使用的布局管理器
 * 
 * @param layout 要应用的布局管理器实例(如 LinearLayoutManager、GridLayoutManager)
 */
public void setLayoutManager(LayoutManager layout) {
    // 如果新布局与当前布局相同则直接返回(优化重复设置)
    if (layout == mLayout) {
        return;
    }
  
    // 停止所有正在进行的滚动(防止布局切换时的滚动冲突)
    stopScroll();
  
    /* 处理旧布局的清理工作 */
    if (mLayout != null) {
        // 结束所有正在进行的项目动画
        if (mItemAnimator != null) {
            mItemAnimator.endAnimations(); // 强制终止所有动画
        }
  
        // (1)回收所有已附加的视图和临时缓存视图
        // (2)主视图回收
        mLayout.removeAndRecycleAllViews(mRecycler); 
        // (3)临时碎片视图回收
        mLayout.removeAndRecycleScrapInt(mRecycler); 
        mRecycler.clear(); // (4)清空回收站
  
        // (5)如果已附加到窗口,通知布局管理器分离事件
        if (mIsAttached) {
            mLayout.dispatchDetachedFromWindow(this, mRecycler);
        }
  
        // 解绑旧布局与 RecyclerView 的关联
        mLayout.setRecyclerView(null);
        mLayout = null; // 释放旧布局引用
    } else {
        // 无旧布局时仍清空回收站(确保状态干净)
        mRecycler.clear();
    }
  
    /* 防御性清理:确保所有子视图被移除 */
    mChildHelper.removeAllViewsUnfiltered(); // 通过 ChildHelper 彻底移除所有视图
  
    /* 设置新布局管理器 */
    mLayout = layout; // 更新布局引用
  
    if (layout != null) {
        // 安全性检查:布局不能重复绑定
        if (layout.mRecyclerView != null) {
            throw new IllegalArgumentException("布局管理器 " + layout 
                + " 已绑定到其他 RecyclerView: " + layout.mRecyclerView);
        }
  
        // 建立双向绑定
        mLayout.setRecyclerView(this); // 让布局layoutManager持有 RecyclerView 引用
  
        // 如果已附加到窗口,通知布局管理器附加事件
        if (mIsAttached) {
            mLayout.dispatchAttachedToWindow(this);
        }
    }
  
    /* (6) 后续处理 */
    mRecycler.updateViewCacheSize(); // 根据新布局调整视图缓存策略
    requestLayout(); // 触发重新测量和布局(关键!使新布局生效)
}
  • 一般情况下setLayoutManager()方法只会在RecyclerView初始化时调用一次,所以不会出现重复绑定的情况。但是,在某些情况下,比如在RecyclerView的onCreateViewHolder()方法中调用setLayoutManager()方法,可能会出现重复绑定的情况。

  • 动态setLayoutManager时: 异步化布局计算(API 26+), 限制: 部分布局操作仍需主线程完成

    java 复制代码
    // 使用AsyncLayoutInflater异步加载
    new AsyncLayoutInflater(context).inflate(
        R.layout.grid_item, 
        recycler, 
        (view, resid, parent) -> {
            // 在子线程计算布局参数
            post(() -> recycler.setLayoutManager(gridLayout)); 
        }
    );
  • 在(1)-(6)处 均使用到了mRecycler, recycler详解在3.1中,Recycler 是视图回收复用机制的核心控制器

3.1 Recycler 视图回收复用机制的核心控制器

3.1.1 根据Recycler成员变量跟踪部分核心代码
java 复制代码
 public final class Recycler {
   final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
    ArrayList<ViewHolder> mChangedScrap = null;

    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; //默认mAttachedScrap最大缓存数量

    ...

    //  设置缓存池的扩展
    void setViewCacheExtension(ViewCacheExtension extension) {
            mViewCacheExtension = extension;
        }

  
    // 外部调用设置recyclerView的缓存池
    // 如果您想在多个 RecyclerView 之间回收视图,请创建一个 RecycledViewPool 实例,并使用RecyclerView#setRecycledViewPool
    void setRecycledViewPool(RecycledViewPool pool) {
         if (mRecyclerPool != null) {
            mRecyclerPool.detach();
         }
         mRecyclerPool = pool;
         if (pool != null) {
            mRecyclerPool.attach(getAdapter());
          }
     }

  
    // 获取mRecyclerPool,可以使用此
    RecycledViewPool getRecycledViewPool() {
            if (mRecyclerPool == null) {
                mRecyclerPool = new RecycledViewPool();
            }
            return mRecyclerPool;
        }

    //  Android RecyclerView 中处理 ViewHolder 回收逻辑的核心部分
     void scrapView(View view) {
            final ViewHolder holder = getChildViewHolderInt(view);
            if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
                    || !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
                if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
                    throw new IllegalArgumentException("Called scrap view with an invalid view."
                            + " Invalid views cannot be reused from scrap, they should rebound from"
                            + " recycler pool.");
                }
                // 标记为添加到mAttachedScrap容器中, false表示无需重新绑定
                holder.setScrapContainer(this, false);
                mAttachedScrap.add(holder);
            } else {
                //初始化mChangedScrap容器
                if (mChangedScrap == null) {
                    mChangedScrap = new ArrayList<ViewHolder>();
                }
                // 将 ViewHolder 标记为附加到 mChangedScrap(true 表示需要处理更新)。
                holder.setScrapContainer(this, true);
                mChangedScrap.add(holder);
            }
        }

  
  // mCachedViews 和 RecycledViewPool
  void recycleViewHolderInternal(ViewHolder holder) {
    // 检查1:确保 ViewHolder 未被临时废弃(scrapped)或仍附着在父视图上
    if (holder.isScrap() || holder.itemView.getParent() != null) {
        throw new IllegalArgumentException(
                "Scrapped or attached views may not be recycled. isScrap:"
                        + holder.isScrap() + " isAttached:"
                        + (holder.itemView.getParent() != null));
    }
    // 检查2:确保 ViewHolder 未被临时分离(tmpDetached)
    if (holder.isTmpDetached()) {
        throw new IllegalArgumentException("Tmp detached view should be removed "
                + "from RecyclerView before it can be recycled: " + holder);
    }
    // 检查3:确保 ViewHolder 未被标记为忽略状态
    if (holder.shouldIgnore()) {
        throw new IllegalArgumentException("Trying to recycle an ignored view holder. You"
                + " should first call stopIgnoringView(view) before calling recycle.");
    }
  
    // 检查 ViewHolder 的临时状态是否阻止回收(如有正在运行的动画)
    final boolean transientStatePreventsRecycling = holder
            .doesTransientStatePreventRecycling();
    // 如果临时状态阻止回收,尝试通过 Adapter 的钩子强制回收
    final boolean forceRecycle = mAdapter != null
            && transientStatePreventsRecycling
            && mAdapter.onFailedToRecycleView(holder);
  
    boolean cached = false;  // 标记是否缓存到 mCachedViews
    boolean recycled = false; // 标记是否回收到 RecycledViewPool
  
    // 调试检查:确保不会重复回收已缓存的 ViewHolder
    if (DEBUG && mCachedViews.contains(holder)) {
        throw new IllegalArgumentException("cached view received recycle internal? "
                + holder);
    }
  
    // 决定是否回收:强制回收 或 正常可回收状态
    if (forceRecycle || holder.isRecyclable()) {
        // 条件1:缓存池未满 (mViewCacheMax > 0)
        // 条件2:ViewHolder 无无效/移除/更新/位置未知标志
        if (mViewCacheMax > 0
                && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
        
            // 如果缓存池已满,移除最早缓存的 ViewHolder
            int cachedViewSize = mCachedViews.size();
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);  // 回收位置0的ViewHolder
                cachedViewSize--;
            }
        
            // 计算最佳插入位置(考虑预取优化)
            int targetCacheIndex = cachedViewSize;
            if (ALLOW_THREAD_GAP_WORK
                    && cachedViewSize > 0
                    && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                // 跳过最近预取的 ViewHolder,优先保留非预取项
                int cacheIndex = cachedViewSize - 1;
                while (cacheIndex >= 0) {
                    int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                    if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                        break;
                    }
                    cacheIndex--;
                }
                targetCacheIndex = cacheIndex + 1;
            }
            // 添加到缓存池
            mCachedViews.add(targetCacheIndex, holder);
            cached = true;
        }
    
        // 如果未缓存,则回收到全局 ViewPool
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);
            recycled = true;
        }
    } else {
        // 不可回收情况处理(通常由动画引起)
        if (DEBUG) {
            Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                    + "re-visit here. We are still removing it from animation lists");
        }
    }
  
    // 关键清理:从动画信息存储中移除, 动画如果不清楚 则复用时有可能会错乱,且如果被回收了可能会造成内存泄漏问题
    mViewInfoStore.removeViewHolder(holder);
  
    // 特殊场景清理:临时状态阻止回收时的资源释放
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mOwnerRecyclerView = null;
    }

    }

       /**
         * Set the maximum number of detached, valid views we should retain for later use.
         * 开发者配置的缓存最大值(通过 RecyclerView.setItemViewCacheSize() 设置)
         * @param viewCount Number of views to keep before sending views to the shared pool
         */
        public void setViewCacheSize(int viewCount) {
            mRequestedCacheMax = viewCount;
            updateViewCacheSize();
        }

        // 生成实际生效的最大缓存值  实际上再setLyoutmanager 和 setViewCacheSize的时候会调用这个方法
        void updateViewCacheSize() {
            int extraCache = mLayout != null ? mLayout.mPrefetchMaxCountObserved : 0;
            mViewCacheMax = mRequestedCacheMax + extraCache;

            // first, try the views that can be recycled
            for (int i = mCachedViews.size() - 1;
                    i >= 0 && mCachedViews.size() > mViewCacheMax; i--) {
                recycleCachedViewAt(i);
            }
        }


    ...
 }
  1. mRequestedCacheMaxmViewCacheMax

    • mRequestedCacheMax: 开发者配置的缓存最大值(通过 RecyclerView.setItemViewCacheSize() 设置)
    • mViewCacheMax实际生效的缓存最大值(通过GapWoker根据系统资源动态调整mLayout.mPrefetchMaxCountObserved(最大预取数量), 在3.2中有GapWorker的详解)
  2. mUnmodifiableAttachedScrap: 提供对 mAttachedScrap 的只读访问接口,防止外部意外修改内部状态

  3. mChangedScrap: 已更新且需要重新处理,专门存储数据已变更的 ViewHolder(如被 notifyItemChanged() 标记的项,在 GapWorker执行预取时最先取出的缓存 一个 ViewHolder 会被添加到 mChangedScrap 中,当同时满足以下条件时:

    • 它没有被标记为 REMOVED 或 INVALID。
    • 它对应的数据项被更新了(isUpdated() 返回 true)。
    • 且当前不允许直接重用被更新的 ViewHolder(即 canReuseUpdatedViewHolder(holder) 返回 false)。
  4. mAttachedScrap: 临时存储与当前布局关联的 ViewHolder,这些视图仍在 RecyclerView 可见范围内但需要暂时分离以重新布局。 从 scrapView方法中可以看出viewholder被标记为 FLAG_REMOVEDFLAG_INVALID 或者 holder未update,或者允许复用已更新的 ViewHolder(canReuseUpdatedViewHolder返回true)

    • 触发场景
      • 布局更新(如 notifyDataSetChanged() 触发onLayout(),onMessure())
      • 插入/删除项动画前的视图分离
    • 特点
      • 不触发 onViewRecycled 回调
      • 同一布局周期内快速复用,不需要重新绑定数据
      • 存活周期:仅在单次布局过程中有效
  5. mCachedViews (一级缓存) 离开屏幕,存储最近被移除但未被回收的视图, 完整数据保留 内存占用高 复用成本低(无需绑定)

    • 移除特点:
      • 线性列表:存储最近被移除的 ViewHolder
      • 先进先出:移除最早缓存的项
      • 降级存储:淘汰的 ViewHolder 转入 RecycledViewPool
      • 允许重复类型:可以包含多个相同 viewType 的 ViewHolder
    • 优势:无需重新绑定数据(数据未失效)
    • 容量限制:mViewCacheMax(默认=2),预取GapWorker动态配置,例:recyclerView.setItemViewCacheSize(4); // 增大 mCachedViews 大小
    • 插入优化:避开预取位置(lastPrefetchIncludedPosition)
  6. mViewCacheExtension: 开发者自定义的二级缓存扩展(介于 mCachedViews(一级缓存) 和 mRecyclerPool 之间)

    java 复制代码
    /**
     * 自定义二级缓存扩展,用于开发者自定义二级缓存, 介于 mCachedViews(一级缓存) 和 mRecyclerPool 之间
     */
    public abstract static class ViewCacheExtension {
        /**
         * 返回一个可以绑定到给定适配器位置(Adapter position)的View。
         * 该方法**不应**创建一个新的View。它期望返回一个已经创建好的View,该View可以被重新用于给定的类型(type)和位置
         * 如果该View被标记为忽略(ignored),那么在返回该View之前,应该先调用{@link LayoutManager#stopIgnoring
         * 如有必要,RecyclerView会将返回的View重新绑定到对应位置。
         * @param recycler  可用于绑定View的Recycler
         * @param position  适配器位置(Adapter position)
         * @param type      View的类型,由适配器定义
         * @return 一个绑定到给定位置的View,如果没有可复用的View,则返回NULL
         */
        public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
    }
    • 单向操作:RecyclerView只从该扩展中获取视图,但不会自动将视图存入该扩展。开发者需要自己管理视图的存储和回收。
    • 该方法不允许创建新视图,只能返回已有的视图。
    • 使用场景:
      • 固定位置视图缓存:如常驻顶部的HeaderView,可以预加载并始终通过此扩展返回。
      • 按需保留视图:对某些特定位置的视图(如高优先项)保留引用以避免被回收到Pool。
      • 跨列表复用视图:在不同RecyclerView之间共享特定视图(需注意生命周期)。
    • 典型实现案例:
    java 复制代码
    public class CustomViewCache extends ViewCacheExtension {
    // 自定义缓存容器 (SparseArray<View> 按位置缓存)
    private final SparseArray<View> positionCache = new SparseArray<>();
    
    private final SparseArray<View> typeCache = new SparseArray<>();
    
    // 开发者需主动缓存视图 (通常在onViewRecycled时)
    public void cacheView(int position, View view) {
       positionCache.put(position, view);
    }
    
    public void cacheTypeView(int type,View view){
       typeCache.put(type,view);
    }
    
    @Override
    public View getViewForPositionAndType(Recycler recycler, int pos, int type) {
        if (isFixedPosition(pos)) {
            // 固定位置视图:直接返回(假设已预绑定)
            return positionCache.get(pos);
        } else {
            // 普通视图:从类型缓存获取并重新绑定
            View view = typeCache.get(type).poll();
            if (view != null) {
                recycler.bindViewToPosition(view, pos); // 动态绑定新位置
            }
            return view;
        }
    }
    }
    • 生命周期管理: 需要在适当时机清理缓存
    java 复制代码
    // 在RecyclerView销毁时
    recyclerView.addOnDetachListener(() -> {
       cache.clear();
       customCache = null; // 防止内存泄漏
    });
    
    // 数据失效时 (如刷新数据源)
    adapter.registerAdapterDataObserver(new AdapterDataObserver() {
       @Override
       public void onChanged() {
           cache.clear();
       }
    });
  7. mRecyclerPool RecycledViewPool(长期缓存), 无数据保留 内存占用低 复用成本高(需重新绑定)

      1. 共享特点:全局共享缓存池, 默认是不共享,如需共享需要设置 setRecycledViewPool(RecycledViewPool pool)
    场景 推荐方案 原因
    单个 RecyclerView 默认私有池 简单高效,无需额外管理
    同屏多个相同类型列表 共享池 提高复用率,减少内存占用
    分页/分块加载数据 共享池 跨页复用视图,平滑滚动
    不同 Activity 的列表 独立池 生命周期不同,难以管理
      1. 类型分桶存储
      • 分桶机制:每个 viewType 对应一个独立的 ViewHolder 列表
      • 动态创建:遇到新 viewType 时自动创建新桶
      java 复制代码
      public static class RecycledViewPool {
        // 按 viewType 分桶
        private SparseArray<ArrayList<ViewHolder>> mScrap = new SparseArray<>();
      }
      1. 容量控制
      java 复制代码
      // RecycledViewPool 
      private static final int DEFAULT_MAX_SCRAP = 5;
      private SparseIntArray mMaxScrap = new SparseIntArray();
      
        public void setMaxRecycledViews(int viewType, int max) {
            mMaxScrap.put(viewType, max);
      }
      • 默认容量:每类型最多存储 5 个 ViewHolder
      • 独立配置:可为不同 viewType 设置不同容量上限
      • 溢出处理:当超过上限时,移除最早添加的 ViewHolder
      1. 状态重置机制
      java 复制代码
      public void putRecycledView(ViewHolder scrap) {
            final int viewType = scrap.getItemViewType();
            final ArrayList scrapHeap = getScrapDataForType(viewType).mScrapHeap;
            if (mScrap.get(viewType).mMaxScrap <= scrapHeap.size()) {
                return;
            }
            if (DEBUG && scrapHeap.contains(scrap)) {
                throw new IllegalArgumentException("this scrap item already exists");
            }
            // 关键重置操作
            // 包括:
            // - 清除绑定数据
            // - 重置标志位 (FLAG_BOUND, FLAG_UPDATE等)
            // - 清除临时状态
            scrap.resetInternal();
            scrapHeap.add(scrap);
        }
      • 数据剥离:清除所有与特定位置相关的数据
      • 状态归零:重置为"空白画布"状态
      • 复用要求:下次使用时必须重新绑定数据
      1. 复用顺序策略
      • LIFO(后进先出):优先复用最近添加的 ViewHolder
      • 性能优化:最近使用的 ViewHolder 更可能在缓存中
      1. 线程安全
      1. 生命周期特点: 自动清理:当所有关联的 RecyclerView 被销毁时,池可被 GC 回收
      java 复制代码
      public void onDetachedFromWindow() {
        // RecyclerView 被移除时
        mRecycler.detach();
      }
      
      void detach() {
         mRecycledViewPool.detach(); // 通知池解耦
      }

总结: mChangedScrapmAttachedScrap使用场景:

场景 条件组合 缓存位置
普通更新(无payload) isUpdated=true + canReuse=false ✅ mChangedScrap
带payload的局部更新 isUpdated=true + canReuse=true ❌ mAttachedScrap
删除操作 FLAG_REMOVED 被标记 ❌ mAttachedScrap
数据失效 FLAG_INVALID 被标记 ❌ mAttachedScrap

缓存使用顺序:

  1. mChangeScrap
  2. mAttachedScrap
  3. mCachedViews
  4. mViewCacheExtension
  5. mRecyclerPool

3.2 GapWorker 预取

RecyclerView中,GapWorker实现了Runnable 在sdk_int>=21时在onAttachedToWindow初始化,用于预取数据。是Handler得looper一样,使用ThreadLoacal绑定当前线程和GapWorker,一个线程只存在一个GapWorker,每个GapWorker对象可以存多个RecyclerView,所以 GapWorker是线程安全的。

java 复制代码
// RecyclerView.java 成员变量 如果sdk_int>=21 ALLOW_THREAD_GAP_WORK=true, 默认开启预抓取
GapWorker.LayoutPrefetchRegistryImpl mPrefetchRegistry =
            ALLOW_THREAD_GAP_WORK ? new GapWorker.LayoutPrefetchRegistryImpl() : null;
  • 在RecyclerView初始化时,当sdk_int>=21时,预抓取策略 mPrefetchRegistry被初始化为 GapWorker.LayoutPrefetchRegistryImpl对象,否则为null
java 复制代码
// RecyclerView.java
// GapWorker的run()执行条件1
onTouchEvent(e)
{ ... 
case MotionEvent.ACTION_MOVE: {... mGapWorker.postFromTraversal(this, dx, dy);}
 ...}

// RecyclerView.java 内部类ViewFlinger处理惯性飞滑事件逻辑
//  GapWorker的run()执行条件2
class ViewFlinger implements Runnable {
    ...
    public void run() {
        ...
        mGapWorker.postFromTraversal(RecyclerView.this, dx, dy); 
        ...
    }
    ...
}


// GapWorker.java
/**
     * Schedule a prefetch immediately after the current traversal.
     */
    void postFromTraversal(RecyclerView recyclerView, int prefetchDx, int prefetchDy) {
        if (recyclerView.isAttachedToWindow()) {
            if (RecyclerView.DEBUG && !mRecyclerViews.contains(recyclerView)) {
                throw new IllegalStateException("attempting to post unregistered view!");
            }
            if (mPostTimeNs == 0) {
                mPostTimeNs = recyclerView.getNanoTime();
                // 执行GapWorker的run方法
                recyclerView.post(this);
            }
        }

        recyclerView.mPrefetchRegistry.setPrefetchVector(prefetchDx, prefetchDy);
    }

// GapWorker.java
 @Override
    public void run() {
        try {
            Trace.beginSection(RecyclerView.TRACE_PREFETCH_TAG);

            if (mRecyclerViews.isEmpty()) {
                // abort - no work to do
                return;
            }

            // Query last vsync so we can predict next one. Note that drawing time not yet
            // valid in animation/input callbacks, so query it here to be safe.
            long lastFrameVsyncNs = TimeUnit.MILLISECONDS.toNanos(
                    mRecyclerViews.get(0).getDrawingTime());
            if (lastFrameVsyncNs == 0) {
                // abort - couldn't get last vsync for estimating next
                return;
            }

            // TODO: consider rebasing deadline if frame was already dropped due to long UI work.
            // Next frame will still wait for VSYNC, so we can still use the gap if it exists.
            long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs;

            prefetch(nextFrameNs);

            // TODO: consider rescheduling self, if there's more work to do
        } finally {
            mPostTimeNs = 0;
            Trace.endSection();
        }
    }

预抓取的执行入口

  1. RecyclerView滑动时,手指未离开屏幕, 也就是action_move状态
  2. RecyclerView滑动时,手指离开屏幕, 也就是飞滑惯性状态
  3. 调用 postFromTraversal方法 执行run()方法内, 进一步执行预抓取 prefetch方法

抓取策略 prefetch(nextFrameNs); 参数nextFrameNs:

  • long nextFrameNs = lastFrameVsyncNs + mFrameIntervalNs; 含义:下一帧的VSync时间 = 上一帧VSync时间 + 帧间隔
  1. VSync 信号的基础原理
    • VSync(垂直同步)是 Android 显示系统的核心机制,每帧画面渲染前都会触发一次。
    • 屏幕刷新率决定了 VSync 信号的间隔时间(如 60Hz 屏幕 ≈ 16.6ms/帧,90Hz ≈ 11.1ms/帧)。
    • mFrameIntervalNs 就是当前屏幕刷新周期的纳秒值(通过 Display.getRefreshRate() 计算得出)。
java 复制代码
// GapWork.java
void prefetch(long deadlineNs) {
        // 1
        buildTaskList();
        // 2
        flushTasksWithDeadline(deadlineNs);
    }
  • 步骤1:构建预取任务队列
  • 步骤2:在截止时间内执行任务,避免主线程卡顿
    • 确保预取操作在 deadlineNs 前完成,防止影响下一帧渲染
    • 若任务未能在截止时间内完成,剩余任务会被延迟到下一帧处理

构建预取任务列表

java 复制代码
    private void buildTaskList() {
        // 1. 更新所有RecyclerView的预取注册信息(一般都是1, 存在嵌套可能)
        final int viewCount = mRecyclerViews.size();
        int totalTaskCount = 0;
        for (int i = 0; i < viewCount; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            // (4)
            view.mPrefetchRegistry.collectPrefetchPositionsFromView(view, false);
            totalTaskCount += view.mPrefetchRegistry.mCount;
        }

        // 2. 初始化任务列表
        mTasks.ensureCapacity(totalTaskCount);

        // 3. 填充任务列表
        int totalTaskIndex = 0;
        for (int i = 0; i < viewCount; i++) {
            RecyclerView view = mRecyclerViews.get(i);
            LayoutPrefetchRegistryImpl prefetchRegistry = view.mPrefetchRegistry;
            final int viewVelocity = Math.abs(prefetchRegistry.mPrefetchDx)
                    + Math.abs(prefetchRegistry.mPrefetchDy);
            for (int j = 0; j < prefetchRegistry.mCount * 2; j += 2) {
                final Task task;
                if (totalTaskIndex >= mTasks.size()) {
                    task = new Task();
                    mTasks.add(task);
                } else {
                    // 复用Task对象
                    task = mTasks.get(totalTaskIndex);
                }
                // 从预取数组中读取数据
                final int distanceToItem = prefetchRegistry.mPrefetchArray[j + 1];
                // immediate:标记该任务是否需要立即执行。判断条件为:distanceToItem <= viewVelocity。
                // 若成立,表示该item可能在下一帧就会进入屏幕,需要高优先级处理。
                // 若不成立,表示有更多时间缓冲,可稍后处理。
                task.immediate = distanceToItem <= viewVelocity;
                // 当前视图滚动速度, 计算视图速度:viewVelocity = |dx| + |dy|,用于估算滚动速度(逻辑单位/帧)
                task.viewVelocity = viewVelocity;
                task.distanceToItem = distanceToItem;
                task.view = view;
                task.position = prefetchRegistry.mPrefetchArray[j];

                totalTaskIndex++;
            }
        }

        // ... and priority sort
        Collections.sort(mTasks, sTaskComparator);
    }

    // GapWorker.LayoutPrefetchRegistryImpl implementation RecyclerView.LayoutManager.LayoutPrefetchRegistry 
    LayoutPrefetchRegistryImpl implementation RecyclerView.LayoutManager.LayoutPrefetchRegistry {
      // (5)
      void collectPrefetchPositionsFromView(RecyclerView view, boolean nested) {
            mCount = 0;
            if (mPrefetchArray != null) {
                Arrays.fill(mPrefetchArray, -1);
            }

            final RecyclerView.LayoutManager layout = view.mLayout;
            if (view.mAdapter != null
                    && layout != null
                    && layout.isItemPrefetchEnabled()) {
                if (nested) {
                    // nested prefetch, only if no adapter updates pending. Note: we don't query
                    // view.hasPendingAdapterUpdates(), as first layout may not have occurred
                    if (!view.mAdapterHelper.hasPendingUpdates()) {
                        // 具体实现在LayoutManager的子类中, 并在实现子类中调用addPosttion方法
                        layout.collectInitialPrefetchPositions(view.mAdapter.getItemCount(), this);
                    }
                } else {
                    // momentum based prefetch, only if we trust current child/adapter state
                    if (!view.hasPendingAdapterUpdates()) {
                        layout.collectAdjacentPrefetchPositions(mPrefetchDx, mPrefetchDy,
                                view.mState, this);
                    }
                }
                // 更新预取数量, 如果本次收集的预取数量mCount超过历史最大值,则更新最大值记录,并调整RecyclerView的缓存大小(updateViewCacheSize()会根据最大值优化缓存池大小)
                if (mCount > layout.mPrefetchMaxCountObserved) {
                    layout.mPrefetchMaxCountObserved = mCount;
                    layout.mPrefetchMaxObservedInInitialPrefetch = nested;
                    view.mRecycler.updateViewCacheSize();
                }
            }
        }
    
        // (6) 将需要预取的Item位置和像素距离添加到预取数组中
        @Override
        public void addPosition(int layoutPosition, int pixelDistance) {
            if (pixelDistance < 0) {
                throw new IllegalArgumentException("Pixel distance must be non-negative");
            }

            // allocate or expand array as needed, doubling when needed
            final int storagePosition = mCount * 2;
            if (mPrefetchArray == null) {
                mPrefetchArray = new int[4];
                Arrays.fill(mPrefetchArray, -1);
            } else if (storagePosition >= mPrefetchArray.length) {
                final int[] oldArray = mPrefetchArray;
                mPrefetchArray = new int[storagePosition * 2];
                System.arraycopy(oldArray, 0, mPrefetchArray, 0, oldArray.length);
            }

            // add position
            mPrefetchArray[storagePosition] = layoutPosition;
            mPrefetchArray[storagePosition + 1] = pixelDistance;

            mCount++;
        }
    }
   
  1. 第一步: 遍历所有RecyclerView:mRecyclerViews保存了当前所有注册了预取的RecyclerView(通常一个页面只有一个,但嵌套时可能有多个)。
    • 收集预取位置:对每个RecyclerView,调用其mPrefetchRegistry.collectPrefetchPositionsFromView()方法。该方法的作用是:
    • 根据当前滚动方向(mPrefetchDx/mPrefetchDy)计算需要预取的item位置(即将进入屏幕的item)。
    • 结果存储在mPrefetchRegistry的两个数组中:
      • mPrefetchArray:偶数索引存position,奇数索引存该item与当前可视区域的距离(逻辑距离,如item索引差)。
      • mCount:有效预取项的数量。
    • 统计总任务数:累加所有RecyclerView的预取项数量到totalTaskCount。
  2. 第二步: 初始化任务列表 mTasks.ensureCapacity(totalTaskCount);
    • 确保任务列表mTasks(ArrayList <Task>)有足够的容量存放所有任务,避免多次扩容带来的性能开销。
  3. 填充任务列表,并根据是否在下一帧执行任务进行排序
  4. (4)(5)(6) 之间的协作关系
    • collectPrefetchPositionsFromView作为入口方法,根据情况调用LayoutManager的预取位置收集方法。
    • LayoutManagercollectInitialPrefetchPositionscollectAdjacentPrefetchPositions方法会通过回调 addPosition添加多个预取项。
    • addPosition方法负责将位置和距离信息存储到数组中。并得出最大预取数量 mPrefetchMaxCountObserved

任务执行流程: flushTasksWithDeadline(deadlineNs)

java 复制代码
// GapWorker.java
private void flushTasksWithDeadline(long deadlineNs) {
        for (int i = 0; i < mTasks.size(); i++) {
            final Task task = mTasks.get(i);
            if (task.view == null) {
                break; // done with populated tasks
            }
            flushTaskWithDeadline(task, deadlineNs);
            task.clear();
        }
    }

private void flushTaskWithDeadline(Task task, long deadlineNs) {
        // 确定任务执行时限
        long taskDeadlineNs = task.immediate ? RecyclerView.FOREVER_NS : deadlineNs;
        // 执行核心预取操作
        RecyclerView.ViewHolder holder = prefetchPositionWithDeadline(task.view,
                task.position, taskDeadlineNs);
        // 处理嵌套 RecyclerView
        if (holder != null && holder.mNestedRecyclerView != null) {
            prefetchInnerRecyclerViewWithDeadline(holder.mNestedRecyclerView.get(), deadlineNs);
        }
    }

... 
        // prefetchPositionWithDeadline调用链核心预取操作
        // 尝试在给定的截止时间(deadlineNs)之前获取一个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;
            //  If there is a changed scrap, try to find from there
            // 1. 预布局阶段的Changed Scrap
            if (mState.isPreLayout()) {
                holder = getChangedScrapViewForPosition(position);
                fromScrapOrHiddenOrCache = holder != null;
            }
            //  Find by position from scrap/hidden list/cache
            // Scrap、Hidden列表或缓存
            if (holder == null) {
                holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
                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());
                }

                final int type = mAdapter.getItemViewType(offsetPosition);
            
                // 3.通过稳定ID从Scrap或缓存中获取
                if (mAdapter.hasStableIds()) {
                    holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                            type, dryRun);
                    if (holder != null) {
                        // update position
                        holder.mPosition = offsetPosition;
                        fromScrapOrHiddenOrCache = true;
                    }
                }
                // 4. ViewCacheExtension(自定义缓存)
                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.");
                        }
                    }
                }

                // 5. RecycledViewPool(回收池)
                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.resetInternal();
                        if (FORCE_INVALIDATE_DISPLAY_LIST) {
                            invalidateDisplayListInt(holder);
                        }
                    }
                }

                // 6. 如果都没有,则创建新的ViewHolde
                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() || holder.needsUpdate() || holder.isInvalid()) {
                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 = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
            }

            // 确保ViewHolder的itemView有正确的LayoutParams(RecyclerView.LayoutParams类型)。如果不存在或不匹配,则生成新的LayoutParams。
            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;
            }
            // 将ViewHolder与LayoutParams关联。
            rvLayoutParams.mViewHolder = holder;
            // mPendingInvalidate:如果从缓存中获取并且刚刚绑定了数据,则为true,表示需要刷新。
            rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
            return holder;
        }

急任务 (immediate = true):

  • 使用 FOREVER_NS (Long.MAX_VALUE) 表示无时间限制
  • 原因:这些任务对应的 item 可能下一帧就进入屏幕,必须完成

非紧急任务:

  • 使用统一的 deadlineNs(通常是下一帧 VSync 时间)
  • 可能在时间耗尽时部分执行 tryGetViewHolderForPositionByDeadline整体流程 方法按顺序尝试从不同的来源获取ViewHolder,直到成功或超时。来源包括:
  1. 预布局阶段的Changed Scrap
  2. Scrap、Hidden列表或缓存
  3. 通过稳定ID从Scrap或mCachedViews中获取
  4. ViewCacheExtension(自定义缓存)
  5. RecycledViewPool(回收池)
  6. 最后,如果都没有,则创建新的ViewHolder

3.3 LayoutManager 布局管理器

LayoutManager 是 RecyclerView 架构中的核心控制器,负责决定列表项的布局方式、滚动行为和复用策略。

3.3.1 核心职责概览
graph TD A[LayoutManager] --> B[布局管理] A --> C[滚动控制] A --> D[回收复用] A --> E[动画支持] A --> F[测量布局] A --> G[状态保存]
3.3.2 布局管理(核心功能)

决定列表项的排列方式,实现多样化布局:

布局类型 实现类 特点
线性布局 LinearLayoutManager 单行/列排列,支持横向/纵向
网格布局 GridLayoutManager 等间距网格,支持跨列
瀑布流 StaggeredGridLayoutManager 错位网格,动态高度
自定义布局 继承 LayoutManager 任意复杂布局(如环形、螺旋)

关键方法

java 复制代码
// 布局入口
void onLayoutChildren(Recycler recycler, State state);

// 布局单个项
void layoutDecorated(View child, int left, int top, int right, int bottom);
3.3.3 滚动控制(核心功能)

管理滚动行为和位置

java 复制代码
// 滚动实现
int scrollVerticallyBy(int dy, Recycler recycler, State state);
int scrollHorizontallyBy(int dx, Recycler recycler, State state);

// 滚动到指定位置
void scrollToPosition(int position);
void smoothScrollToPosition(RecyclerView recyclerView, State state, int position);

滚动特性

  • 支持物理模拟滚动(fling)
  • 精确控制滚动偏移量
  • 处理边界效果(overscroll)
3.3.4 回收复用协调(核心功能)

与 Recycler 协同工作

java 复制代码
// 获取视图
View getViewForPosition(int position);

// 回收视图
void recycleView(View child);

复用流程

sequenceDiagram LayoutManager->>Recycler: 请求视图 (tryGetViewHolder) Recycler->>LayoutManager: 返回缓存的ViewHolder LayoutManager->>Recycler: 回收不再需要的视图 Recycler->>mChangeScrap: 1 Recycler->>mAttachedScrap: 2 Recycler->>mCachedViews: 3.一级缓存 Recycler->>mViewCacheExtension: 4.自定义缓存扩展 Recycler->>RecycledViewPool: 5.存储长期缓存
3.3.5 动画支持(核心功能)

为增删改动画提供布局信息

java 复制代码
// 记录动画前位置
void onLayoutChildren(Recycler recycler, State state) {
    // 保存预布局信息
    layoutForPredictiveAnimations();
}

// 计算动画偏移
void calculateItemDecorationsForChild(View child, Rect outRect);

动画工作流

  1. 记录预布局状态
  2. 执行数据变更
  3. 计算最终布局
  4. 生成动画差值
3.3.6 测量与布局(核心功能)

控制RecyclerView自身尺寸

java 复制代码
// 测量RecyclerView
void onMeasure(Recycler recycler, State state, int widthSpec, int heightSpec);

// 自动处理wrap_content
boolean isAutoMeasureEnabled();

尺寸计算模式

模式 行为
固定尺寸 直接使用给定尺寸
AT_MOST 计算内容尺寸
UNSPECIFIED 计算最小可行尺寸
3.3.7 状态保存与恢复(核心功能)

保持UI状态一致性

java 复制代码
// 保存布局状态
Parcelable onSaveInstanceState();

// 恢复状态
void onRestoreInstanceState(Parcelable state);

保存内容

  • 滚动位置
  • 锚点信息
  • 自定义布局状态
3.3.8 预取优化

实现性能关键优化

java 复制代码
// 收集预取位置
void collectAdjacentPrefetchPositions(...);
void collectInitialPrefetchPositions(...);

预取流程

  1. 预测滚动方向
  2. 识别即将进入的项
  3. 提前绑定数据
3.3.9 设计哲学

LayoutManager 是 RecyclerView 架构中的核心控制器,负责决定列表项的布局方式、滚动行为和复用策略。

  1. 关注点分离
  • Adapter:数据与视图绑定
  • Recycler:视图缓存管理
  • LayoutManager:布局决策核心
    • GapWork 预取
  1. 模板方法模式
java 复制代码
public abstract class LayoutManager {
    // 基本方法(子类实现)
    abstract void onLayoutChildren(Recycler recycler, State state);
    
    // 带默认实现的方法
    public boolean supportsPredictiveItemAnimations() {
        return false;
    }
}
  1. 状态机管理 布局状态流转
css 复制代码
[初始] → [预布局] → [实际布局] → [动画] → [完成]
           ↑              ↓
           └── 增量更新 ←─┘

4.ItemDecoration详解

RecyclerView.ItemDecoration 详解

ItemDecoration 是 RecyclerView 中用于绘制装饰视图(如分割线、间隔、标记等)的核心组件,可在项目周围添加视觉装饰而无需修改适配器或 ViewHolder。


核心方法

  1. getItemOffsets() • 作用:设置项目周围的偏移量(内边距)

• 参数:

outRect: 设置上下左右偏移量

view: 当前项目视图

parent: RecyclerView

state: RecyclerView 状态

• 使用场景:添加项目间距或分割线预留空间

java 复制代码
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
    int position = parent.getChildAdapterPosition(view);
    if (position == RecyclerView.NO_POSITION) return;

    // 排除最后一项的底部间距
    if (position != parent.getAdapter().getItemCount() - 1) {
        outRect.bottom = 20; // 设置底部间距(分割线高度)
    }
}
  1. onDraw() • 作用:在 RecyclerView 画布上绘制装饰(在项目下方绘制)

• 调用时机:在项目绘制之前执行

• 使用场景:绘制背景型装饰(如分割线)

java 复制代码
@Override
public void onDraw(Canvas c, RecyclerView parent, State state) {
    int childCount = parent.getChildCount();
    for (int i = 0; i < childCount; i++) {
        View child = parent.getChildAt(i);
        int pos = parent.getChildAdapterPosition(child);

        // 非最后一项时绘制分割线
        if (pos != parent.getAdapter().getItemCount() - 1) {
            int top = child.getBottom(); // 分割线起点Y
            int bottom = top + 20;       // 分割线高度
            c.drawRect(0, top, parent.getWidth(), bottom, paint);
        }
    }
}
  1. onDrawOver() • 作用:在 RecyclerView 画布上绘制装饰(在项目上方绘制)

• 调用时机:在项目绘制之后执行

• 使用场景:绘制覆盖型装饰(如悬浮标记)

java 复制代码
@Override
public void onDrawOver(Canvas c, RecyclerView parent, State state) {
    // 示例:在第一个项目顶部绘制红色标记
    View firstChild = parent.getChildAt(0);
    if (firstChild != null) {
        c.drawCircle(50, firstChild.getTop(), 10, redPaint);
    }
}

完整自定义分割线实现

java 复制代码
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
    private Paint paint;
    private int height;

    public DividerItemDecoration(Context context) {
        paint = new Paint();
        paint.setColor(ContextCompat.getColor(context, R.color.divider));
        height = context.getResources().getDimensionPixelSize(R.dimen.divider_height);
    }

    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state) {
        if (parent.getChildAdapterPosition(view) != parent.getAdapter().getItemCount() - 1) {
            outRect.bottom = height;
        }
    }

    @Override
    public void onDraw(Canvas c, RecyclerView parent, State state) {
        for (int i = 0; i < parent.getChildCount(); i++) {
            View child = parent.getChildAt(i);
            if (parent.getChildAdapterPosition(child) == parent.getAdapter().getItemCount() - 1) {
                continue;
            }
            c.drawRect(0, child.getBottom(), parent.getWidth(), child.getBottom() + height, paint);
        }
    }
}

// 使用方式
recyclerView.addItemDecoration(new DividerItemDecoration(context));

进阶技巧

  1. 动态颜色分割线

    java 复制代码
    paint.setColor(isSpecialPosition(position) ? specialColor : normalColor);
  2. 分组悬浮标题 • 在 onDrawOver() 中判断分组变化

    • 绘制固定标题栏并跟随滚动

  3. 不对称间距

    java 复制代码
    if (isFirstRow(position)) outRect.top = largeSpace;
    if (isEdgeColumn(position)) outRect.right = largeSpace;
  4. 圆形指示器 • 在 onDraw() 中为特定状态的项目绘制角标


常见问题解决方案

  1. 分割线不显示 • 确认 getItemOffsets() 设置了正确的偏移量

    • 检查 onDraw() 中坐标计算是否正确

  2. 滚动时重复绘制 • 使用 RecyclerView.ItemDecoration 而非直接修改适配器

    • 避免在 onDraw() 中创建新对象

  3. 最后一项多余分割线

    java 复制代码
    if (position == adapter.getItemCount() - 1) return;
  4. 网格布局特殊处理

    java 复制代码
    // 判断是否为最后一行
    if ((position + 1) % spanCount == 0) {
        outRect.bottom = spacing;
    }

性能优化 • Paint 对象复用:在构造函数中初始化并复用 Paint

• 避免 onDraw() 中分配内存:提前计算坐标

• 使用 Canvas.clipRect() 限制绘制区域

• 关闭硬件加速(必要时):

xml 复制代码
android:layerType="software"

通过合理使用 ItemDecoration,可实现丰富视觉效果而无需侵入业务逻辑代码,是 RecyclerView 高级定制化的核心技巧。

5 Adapter详解

在 Android 开发中,RecyclerView.AdapterRecyclerView 组件的核心桥梁,负责将数据集绑定到视图上。下面从多个维度详解 Adapter 的设计原理、实现步骤和优化技巧:


5.1. Adapter 核心职责

  1. 视图创建 :通过 onCreateViewHolder() 将 XML 布局转换为 ViewHolder 对象
  2. 数据绑定 :通过 onBindViewHolder() 将数据映射到视图元素
  3. 数据管理:处理数据更新通知机制(增/删/改)

5.2 Adapter 关键方法解析

5.2.1. 必需实现的方法
java 复制代码
public class CustomAdapter extends RecyclerView.Adapter<CustomAdapter.ViewHolder> {

    // 返回 ViewHolder 实例
    @Override
    public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.item_layout, parent, false);
        return new ViewHolder(view);
    }

    // 绑定数据到 ViewHolder
    @Override
    public void onBindViewHolder(ViewHolder holder, int position) {
        DataItem item = dataList.get(position);
        holder.bind(item); // 将数据映射到视图
    }

    // 返回数据总量
    @Override
    public int getItemCount() {
        return dataList.size();
    }
}
5.2.2. 进阶方法
java 复制代码
// 多布局类型支持(需要重写 getItemViewType)
@Override
public int getItemViewType(int position) {
    return dataList.get(position).isHeader() ? VIEW_TYPE_HEADER : VIEW_TYPE_ITEM;
}

// 局部更新(高效刷新)
public void updateItem(int position) {
    notifyItemChanged(position);
    // 其他局部更新方法:
    // notifyItemInserted(), notifyItemRemoved(), notifyItemRangeChanged()
}

5.3 ViewHolder 设计模式

java 复制代码
class ViewHolder extends RecyclerView.ViewHolder {
    private final TextView titleView;
    private final ImageView iconView;

    public ViewHolder(View itemView) {
        super(itemView);
        titleView = itemView.findViewById(R.id.tv_title);
        iconView = itemView.findViewById(R.id.iv_icon);
    }

    // 数据绑定入口
    void bind(DataItem item) {
        titleView.setText(item.getName());
        Glide.with(itemView).load(item.getImageUrl()).into(iconView);
        
        // 点击事件
        itemView.setOnClickListener(v -> {
            int pos = getAdapterPosition();
            if(pos != RecyclerView.NO_POSITION) {
                onItemClick(pos);
            }
        });
    }
}

设计优势

  • 避免重复调用 findViewById()
  • 封装视图组件访问逻辑
  • 便于添加交互动画

5.4 数据更新最佳实践

5.4.1 DiffUtil 高效刷新
java 复制代码
public class DataDiffCallback extends DiffUtil.Callback {
    private final List<DataItem> oldList, newList;

    // 比较逻辑实现...
    @Override
    public boolean areItemsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).getId() == newList.get(newPos).getId();
    }
    
    @Override
    public boolean areContentsTheSame(int oldPos, int newPos) {
        return oldList.get(oldPos).equals(newList.get(newPos));
    }
}

// 使用 DiffUtil 更新
DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DataDiffCallback(oldList, newList));
dataList = newList;
result.dispatchUpdatesTo(adapter); // 智能执行局部更新
5.4.2 数据更新注意事项
java 复制代码
// 错误方式(直接替换引用)
dataList = newDataList; 
notifyDataSetChanged(); // 全量刷新,丢弃所有缓存

// 正确方式(保持引用不变)
dataList.clear();
dataList.addAll(newDataList);
notifyItemRangeInserted(0, newDataList.size());

5.5 Adapter 高级技巧

5.5.1 多类型布局实现
java 复制代码
@Override
public int getItemViewType(int position) {
    DataItem item = dataList.get(position);
    if(item instanceof HeaderItem) return TYPE_HEADER;
    if(item instanceof AdItem) return TYPE_AD;
    return TYPE_NORMAL;
}

@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    switch(viewType) {
        case TYPE_HEADER: 
            return new HeaderViewHolder(...);
        case TYPE_AD:
            return new AdViewHolder(...);
        default:
            return new NormalViewHolder(...);
    }
}
5.5.2 尾部加载更多实现
java 复制代码
// Adapter 属性
private boolean showLoadingItem = false;

@Override
public int getItemCount() {
    return dataList.size() + (showLoadingItem ? 1 : 0);
}

// 监听滚动
recyclerView.addOnScrollListener(new OnScrollListener() {
    @Override
    public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
        if(!layoutManager.canScrollVertically(1)) { // 到达底部
            showLoadingItem = true;
            notifyItemInserted(getItemCount());
            loadMoreData(); // 触发加载
        }
    }
});

5.6 常见问题解决

  1. 图片闪烁问题

    java 复制代码
    // 在 ViewHolder 中重置图片
    @Override
    public void onViewRecycled(ViewHolder holder) {
        holder.iconView.setImageDrawable(null);
    }
  2. 位置错乱处理

    java 复制代码
    // 使用稳定的 ID 机制
    @Override
    public long getItemId(int position) {
        return dataList.get(position).getId();
    }
    
    // Adapter 构造函数中设置
    setHasStableIds(true);
  3. 视图回收冲突

    java 复制代码
    // 取消异步任务
    @Override
    public void onViewDetachedFromWindow(ViewHolder holder) {
        holder.cancelPendingRequests();
    }

5.7 性能优化清单

  1. 使用 DiffUtil 替代 notifyDataSetChanged()

  2. 避免在 onBindViewHolder() 中创建对象

  3. 复杂布局采用组合视图(merge/include)

  4. 设置合理的 RecyclerView 缓存策略:

    xml 复制代码
    <RecyclerView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:recycledViewPoolSize="10"
        app:itemViewCacheSize="20"/>

5.8 架构演进方向

  1. ViewHolder 泛型化

    java 复制代码
    abstract class BaseAdapter<T> extends RecyclerView.Adapter<BaseViewHolder<T>> {
        public abstract void onBindData(BaseViewHolder<T> holder, T item);
    }
  2. 结合 Data Binding

    xml 复制代码
    <!-- item_layout.xml -->
    <layout>
        <data>
            <variable name="item" type="com.example.DataItem"/>
        </data>
        <TextView android:text="@{item.title}"/>
    </layout>
  3. Paging 3 集成

    java 复制代码
    class PagingAdapter extends PagingDataAdapter<DataItem, ViewHolder> {
        // 自动处理分页加载逻辑
    }

通过以上完整实现,RecyclerView Adapter 能高效处理 10,000+ 项 的数据集,同时保持 60fps 的流畅滚动体验。关键在于合理运用局部更新、视图复用和异步加载技术。

目前作者在魔都求职中, 希望被各位大佬推荐谢谢

相关推荐
参宿四南河三1 天前
Android Compose SideEffect(副作用)实例加倍详解
android·app
火柴就是我1 天前
mmkv的 mmap 的理解
android
没有了遇见1 天前
Android之直播宽高比和相机宽高比不支持后动态获取所支持的宽高比
android
shenshizhong1 天前
揭开 kotlin 中协程的神秘面纱
android·kotlin
vivo高启强1 天前
如何简单 hack agp 执行过程中的某个类
android
沐怡旸1 天前
【底层机制】 Android ION内存分配器深度解析
android·面试
你听得到111 天前
肝了半个月,我用 Flutter 写了个功能强大的图片编辑器,告别image_cropper
android·前端·flutter
KevinWang_1 天前
Android 原生 app 和 WebView 如何交互?
android
用户69371750013841 天前
Android Studio中Gradle、AGP、Java 版本关系:不再被构建折磨!
android·android studio
杨筱毅1 天前
【底层机制】Android低内存管理机制深度解析
android·底层机制