性能优化(一):复用、缓存、减少布局层级
各位同学,今天我们来聊聊 RecyclerView 的性能优化。说实话,这话题我讲了不下几十遍了,但每次都有新感悟。RecyclerView 作为 Android 最核心的列表组件,用得好不好,直接决定了 App 的流畅度。
我记得刚入行那会儿,带我的老大哥跟我说过一句话:「列表卡顿,十有八九是复用没做好。」当时我不太理解,直到自己踩了坑才明白——嗯,这话一点不假。
复用机制:RecyclerView 的灵魂
RecyclerView 为什么叫 Recycler(回收)?说白了,它的核心思想就是「别浪费」。你滑动列表时,划出屏幕的 item 不会被销毁,而是被回收起来,等新 item 出现时直接拿来用。
我给大家画个图,看看 RecyclerView 的复用流程到底长什么样:
你看,RecyclerView 内部维护了一个回收池(Recycler),里面分了两层缓存:mCachedViews 和 mRecyclerPool。前者存的是刚刚滑出去的 ViewHolder,拿来就能直接用;后者是更底层的对象池,需要重新绑定数据。
核心原则:能复用就别创建。onCreateViewHolder 调用越少,列表越流畅。
缓存层级:不止一层
很多人以为 RecyclerView 就一层缓存,其实不然。我给大家拆解一下,RecyclerView 的缓存分四层:
| 层级 | 名称 | 作用 | 是否需要重新 bind |
|---|---|---|---|
| 第1层 | mAttachedScrap | 屏幕内正在显示的 item | 不需要 |
| 第2层 | mCachedViews | 刚滑出屏幕的 item(默认2个) | 不需要 |
| 第3层 | mViewCacheExtension | 开发者自定义缓存 | 看实现 |
| 第4层 | mRecyclerPool | 对象池,按 viewType 分类 | 需要 |
这里有个细节:mCachedViews 默认只缓存 2 个 ViewHolder。为什么是 2?因为大多数情况下,用户滑动时只会看到 1-2 个 item 进出屏幕。但如果你列表 item 高度很小,或者用户滑动很快,2 个可能不够用。
我的建议:如果列表 item 高度较小(比如 50dp 以下),可以把 mCachedViews 的大小调大一些。调用 recyclerView.setItemViewCacheSize(4) 或者 6,能明显减少 bind 的调用次数。
减少布局层级:能省则省
布局层级深了,measure 和 layout 阶段就会变慢。你想想看,一个 item 里面套了 5 层 LinearLayout,每次滑动都要重新测量,不卡才怪。
我曾经接手过一个项目,列表 item 的布局文件有 7 层嵌套。打开一看,好家伙,一个简单的卡片用了三个 LinearLayout 套在一起。我把它改成了 ConstraintLayout,层级降到了 2 层,滑动帧率从 40fps 直接飙到了 58fps。
给大家几个实战建议:
- 能用 ConstraintLayout 就用——它一个布局能搞定的事情,别用多层嵌套
- 避免使用 RelativeLayout 嵌套——RelativeLayout 的 measure 成本比 LinearLayout 高
- merge 标签用起来——如果 item 根布局是 FrameLayout,直接用 merge 减少一层
- ViewStub 延迟加载——不常用的视图,用 ViewStub 占位,需要时再 inflate
注意:不要为了减少层级而强行使用 ConstraintLayout。如果布局本身很简单(比如就一个 TextView),用 FrameLayout 就够了。过度设计也是性能杀手。
实战:优化一个列表 item
咱们来看一个实际例子。假设你有一个社交 App 的动态列表,每个 item 包含头像、昵称、时间、内容、图片、点赞按钮。常见的写法是这样的:
<!-- 优化前:5层嵌套 -->
<LinearLayout> <!-- 外层 -->
<LinearLayout> <!-- 头像+昵称行 -->
<ImageView />
<TextView />
</LinearLayout>
<TextView /> <!-- 时间 -->
<TextView /> <!-- 内容 -->
<FrameLayout> <!-- 图片容器 -->
<ImageView />
</FrameLayout>
<LinearLayout> <!-- 按钮行 -->
<Button />
</LinearLayout>
</LinearLayout>
优化后,用 ConstraintLayout 降到 2 层:
<!-- 优化后:2层嵌套 -->
<androidx.constraintlayout.widget.ConstraintLayout>
<ImageView android:id="@+id/avatar" />
<TextView android:id="@+id/name" />
<TextView android:id="@+id/time" />
<TextView android:id="@+id/content" />
<ImageView android:id="@+id/image" />
<Button android:id="@+id/likeBtn" />
</androidx.constraintlayout.widget.ConstraintLayout>
你看,原来 5 层变成了 2 层。measure 次数从 5 次降到了 2 次,而且 ConstraintLayout 的 measure 是一次性完成的,不会像 LinearLayout 那样逐层递归。
避坑指南
最后,我把自己踩过的坑分享给大家:
- 我曾经在 onBindViewHolder 里做耗时操作——比如解析 JSON、加载大图。结果列表一滑动就卡成 PPT。后来我把图片加载放到了 Glide 里,JSON 解析提前在数据层完成。
- 我曾经忘记设置 setHasFixedSize(true)——如果列表 item 高度固定,一定要设置这个。否则每次数据变化都会重新测量所有 item,性能损耗很大。
- 我曾经在 item 里用 wrap_content 嵌套 wrap_content——这会导致多次 measure。能写死的高度就写死,别偷懒。
总结一下:性能优化三把斧——复用要到位、缓存要利用、层级要精简。这三件事做好了,列表流畅度至少提升 50%。
好了,这一章就到这里。记住,优化不是一蹴而就的,需要你在实际项目中不断调试、测量、改进。下一章我们聊聊「性能优化(二):DiffUtil 与异步更新」,到时候见。