RecyclerView 源码分析

RecyclerView 源码分析

写在前面:
RecyclerView 是一个越用越优雅的控件,相信大家对于 RecyclerView 的使用也已经比较熟悉了。其功能的高度解耦化,规范 ViewHolder 的写法,以及对动画友好支持,都是它与传统控件 ListView 的区别。而无论 ListView 还是 RecyclerView,本质上都是在有限的屏幕之上,展示大量的内容。所以复用的逻辑,就成了它们最最重要的核心原理,本文主要目的就是探究 RecyclerView 的复用原理。

RecyclerView 的几大模块

  • LayoutManager
    负责 RecyclerView 中,控制 item 的布局方向

  • RecyclerView.Adapter
    为 RecyclerView 承载数据

  • ItemDecoration
    为 RecyclerView 添加分割线

  • ItemAnimator
    控制 RecyclerView 中 item 的动画

刚刚我们提到 RecyclerView 的高度解耦,就是通过以上对象各司其职,来实现 RecyclerView 的基本功能。RecyclerView 无论多么复杂,本质上也是一个自定义 View,本文的重点就是缓存原理分析,不过在此之前,我们还是简单地分别介绍下以上中各个模块的源码。

ItemDecoration

    public void addItemDecoration(ItemDecoration decor, int index) {
        if (mLayout != null) {
            mLayout.assertNotInLayoutOrScroll("Cannot add item decoration during a scroll  or"
                    + " layout");
        }
        if (mItemDecorations.isEmpty()) {
            setWillNotDraw(false);
        }
        if (index < 0) {
            mItemDecorations.add(decor);
        } else {
            // 指定添加分割线在集合中的索引
            mItemDecorations.add(index, decor);
        }
        markItemDecorInsetsDirty();
        // 重新请求 View 的测量、布局、绘制
        requestLayout();
    }

mItemDecorations 是一个 ArrayList,我们将 ItemDecoration 也就是分割线对象,添加到其中。接着我们看下 markItemDecorInsetsDirty 这个方法做了些什么。

    void markItemDecorInsetsDirty() {
        final int childCount = mChildHelper.getUnfilteredChildCount();
        for (int i = 0; i < childCount; i++) {
            final View child = mChildHelper.getUnfilteredChildAt(i);
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true;
        }
        mRecycler.markItemDecorInsetsDirty();
    }

这个方法首先遍历了 RecyclerView 和 LayoutManager 的所有子 View,将其子 View 的 LayoutParams 中的 mInsetsDirty 属性置为 true。接着调用了 mRecycler.markItemDecorInsetsDirty(),Recycler 是 RecyclerView 的一个内部类,就是它管理着 RecyclerView 的复用逻辑。这个我们一会再细谈。

        void markItemDecorInsetsDirty() {
            final int cachedCount = mCachedViews.size();
            for (int i = 0; i < cachedCount; i++) {
                final ViewHolder holder = mCachedViews.get(i);
                LayoutParams layoutParams = (LayoutParams) holder.itemView.getLayoutParams();
                if (layoutParams != null) {
                    layoutParams.mInsetsDirty = true;
                }
            }
        }

mCachedViews 见名知意,也就是 RecyclerView 缓存的集合,相信你也看到了,RecyclerView 的缓存单位是 ViewHolder。我们在 ViewHolder 中取出 itemView,然后获得 LayoutParams,将其 mInsetsDirty 字段一样置为 true。

mInsetsDirty 字段的作用其实是一种优化性能的缓存策略,添加分割线对象时,无论是 RecyclerView 的子 view,还是缓存的 view,都将其置为 true,接着就调用了 requestLayout 方法。

这里简单说一下 requestLayout 方法用一种责任链的方式,层层向上传递,最后传递到 ViewRootImpl,然后重新调用 view 的 measure、layout、draw 方法来展示布局。

我们在 RecyclerView 中搜索 mItemDecorations 集合,看看他是在什么时刻操作 ItemDecoration 这个分割线对象的。

onDraw 中:

    @Override
    public void onDraw(Canvas c) {
        super.onDraw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDraw(c, this, mState);
        }
    }

draw 方法中:

    @Override
    public void draw(Canvas c) {
        super.draw(c);

        final int count = mItemDecorations.size();
        for (int i = 0; i < count; i++) {
            mItemDecorations.get(i).onDrawOver(c, this, mState);
        }
    }

可以看到在 View 的以上两个方法中,分别调用了 ItemDecoration 对象的 onDraw onDrawOver 方法。

这两个抽象方法,由我们继承 ItemDecoration 来自己实现,他们区别就是 onDraw 在 item view 绘制之前调用,onDrawOver 在 item view 绘制之后调用。

所以绘制顺序就是 Decoration 的 onDraw,ItemView的 onDraw,Decoration 的 onDrawOver。

(好像越写越多…收不住了…)

我们在 onDrawonDrawOver 方法中就可以绘制 drawable 对象了。此时分割线就展现出来了。还记得刚才的 mInsetsDirty 字段吗?在添加分割线的时候,无论是 RecyclerView 子 View,还是缓存中的 View,其 LayoutParams 中的 mInsetsDirty 属性,都被置为 true。 我们来解释一下这个字段的作用:

    Rect getItemDecorInsetsForChild(View child) {
        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
        if (!lp.mInsetsDirty) {
            // 当 mInsetsDirty 为 false,说明 mDecorInsets 缓存可用
            return lp.mDecorInsets;
        }

        if (mState.isPreLayout() && (lp.isItemChanged() || lp.isViewInvalid())) {
            // changed/invalid items should not be updated until they are rebound.
            return lp.mDecorInsets;
        }
        final Rect insets = lp.mDecorInsets;
        insets.set(0, 0, 0, 0);
        final int decorCount = mItemDecorations.size();
        for (int i = 0; i < decorCount; i++) {
   
   
            mTempRect.set(0, 0, 0, 0);
            mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState);
            insets.left += mTempRect.left;
            insets.top += mTempRect.top;
            insets.right += mTempRect.right;
            insets.bottom += mTempRect.bottom;
        }
        lp.mInsetsDirty = false;
        return insets;
    }

来解释一下这段代码,首先 getItemDecorInsetsForChild 方法是在 RecyclerView 进行 measureChild 时调用的。目的就是为了取出 RecyclerView 的 ChildView 中的分割线属性 — 在 LayoutParams 中缓存的 mDecorInsets 。而 mDecorInsets 就是 Rect 对象, 其保存记录的是所有添加分割线需要的空间累加的总和,由分割线的 getItemOffsets 方法影响。

最后在 measureChild 方法里,将分割线 ItemDecoration 的尺寸加入到 itemView 的 padding 中。

但是大家都知道缓存并不是总是可用的,mInsetsDirty 这个 boolean 字段来记录它的时效性,当 mInsetsDirty 为 false 时,说明缓存可用,直接取出可以,当 mInsetsDirty 为 true 时,说明缓存的分割线属性就需要重新计算了。

到此,关于 RecyclerView 添加分割线 ItemDecoration 的源码分析,也就基本结束了。

ItemAnimator

如果有人问我,在什么情况下你绝对会选择 RecyclerView,而不是 ListView?如果需求对 Item 的动画有一定要求,这绝对是我选择 RecyclerView 的重要原因之一。ListView 如果要做 Item 的增删动画,那可要费很大劲儿,而 RecyclerView 自身对动画就有很好的支持。

    public void setItemAnimator(ItemAnimator animator) {
        if (mItemAnimator != 
### Android RecyclerView 源码解析 #### 一、RecyclerView 的核心概念 `RecyclerView` 是 Android 中用于高效展示大量数据的组件之一。它通过视图复用机制减少内存消耗并提升性能。其主要组成部分包括 `ViewHolder`、`Adapter` 和 `LayoutManager`。 - **ViewHolder**: 负责保存视图中的子控件引用,避免重复查找操作。 - **Adapter**: 提供绑定数据到视图的功能。 - **LayoutManager**: 控制布局方式(线性、网格或其他自定义形式),管理视图的位置和回收逻辑[^1]。 --- #### 二、缓存与视图复用机制 `RecyclerView` 使用两种类型的缓存来优化性能: 1. **Scrap 缓存 (Active Views)** Scrap 缓存存储的是当前屏幕上不可见但仍可能被重新使用的视图。这些视图通常由 `Recycler` 管理,在滚动过程中会被快速重用。 2. **RecycledViewPool** 当某个视图完全离开屏幕范围时,会进入 `RecycledViewPool`。这是一个全局共享池,允许不同列表间复用相同的 ViewHolder 实例。例如,调用了如下代码: ```java getRecycledViewPool().putRecycledView(holder); ``` 这表示将指定的 `holder` 放入回收池中以便后续使用。 --- #### 三、removeAndRecycleView 方法分析 当需要从父容器中移除某项 View 并将其放入回收队列时,可以调用 `removeAndRecycleView` 方法。以下是其实现细节: ```java public void removeAndRecycleView(View child, Recycler recycler) { removeView(child); // 移除该 View recycler.recycleView(child); // 将 View 加入回收流程 } ``` 此方法的作用在于先将目标 View 从 UI 层面删除 (`removeView`),随后交由 `Recycler` 处理 (`recycleView`)。后者负责判断是否应立即将其销毁还是暂时保留于 Scrap 或 Pool 中待命[^2]。 --- #### 四、layoutChunk 方法剖析 `layoutChunk` 是 `RecyclerView` 布局过程的核心部分之一,主要用于逐个填充新项目至可见区域。它的典型调用场景如下所示: ```java layoutChunk(recycler, state, layoutState, layoutChunkResult); ``` 具体功能分解如下: - 参数说明: - `Recycler`: 提供可复用的视图实例集合。 - `state`: 表示当前状态对象,记录诸如剩余空间大小等信息。 - `layoutState`: 定义了如何获取下一个要显示的数据位置及其方向。 - `layoutChunkResult`: 存储本次执行的结果反馈。 - 执行流程概述:根据传入参数决定下一步渲染策略;如果存在可用资源,则尝试加载新的 Item 到界面上[^3]。 --- #### 五、总结 通过对上述几个关键函数的理解可以看出,`RecyclerView` 不仅实现了灵活多样的外观定制能力,还凭借高效的内部算法显著降低了运行开销。无论是局部更新还是整体刷新操作均能保持流畅体验。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值