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 小时前
安卓10.0系统修改定制化____系列 ROM解打包 修改 讲解 导读篇
android·安卓10系统修改
叽哥2 小时前
flutter学习第 14 节:动画与过渡效果
android·flutter·ios
BoomHe2 小时前
车载 XCU 的简单介绍
android
锅拌饭2 小时前
RecyclerView 缓存复用导致动画失效问题
android
程序员老刘3 小时前
操作系统“卡脖子”到底是个啥?
android·开源·操作系统
拭心3 小时前
一键生成 Android 适配不同分辨率尺寸的图片
android·开发语言·javascript
2501_915918414 小时前
iOS 文件管理全流程实战,从开发调试到数据迁移
android·ios·小程序·https·uni-app·iphone·webview
青莲8434 小时前
深拷贝 vs 浅拷贝
android
一枚小小程序员哈4 小时前
基于Android的音乐播放器/基于android studio的音乐系统/音乐管理系统
android·ide·android studio