RecyclerView 与 ListView 的区别
medium列表RecyclerViewListView缓存DiffUtil
几乎所有 Android 应用都离不开列表展示。RecyclerView 不是 ListView 的简单替代,而是一次架构级重设计——将布局、动画、缓存、Item 装饰等职责彻底解耦,形成了高度可扩展的组件化体系。理解它的缓存机制和优化策略是性能优化的必修课。
架构对比:一体化 vs 组件化
ListView 将所有功能揉在一个类里,而 RecyclerView 做了清晰的职责分离:
ListView(一体化) RecyclerView(组件化)
┌────────────────────┐ ┌──────────────────────┐
│ 布局逻辑 │ │ LayoutManager │ ← 负责布局
│ 滚动处理 │ │ ItemAnimator │ ← 负责动画
│ 回收复用 │ │ ItemDecoration │ ← 负责装饰(分割线)
│ 分割线 │ │ Adapter + ViewHolder│ ← 负责数据绑定
│ 点击事件 │ │ RecycledViewPool │ ← 负责缓存
│ 头部/尾部 │ │ SnapHelper │ ← 负责对齐吸附
└────────────────────┘ └──────────────────────┘
| 特性 | ListView | RecyclerView |
|---|---|---|
| ViewHolder | 可选(需手动实现,不用也不报错) | 强制使用(编译期约束) |
| 布局方向 | 仅垂直列表 | Linear / Grid / StaggeredGrid / 自定义 |
| Item 动画 | 无内置支持 | DefaultItemAnimator 开箱即用 |
| 分割线 | divider 属性(简单但死板) |
ItemDecoration(完全自定义) |
| 点击事件 | setOnItemClickListener |
需自行实现(更灵活,支持子 View 点击) |
| 局部刷新 | 不支持(只有 notifyDataSetChanged) |
notifyItemChanged(pos) / DiffUtil |
| 缓存层级 | 2 级 | 4 级 |
| 嵌套滚动 | 不支持 | 内置 NestedScrollingChild 支持 |
RecyclerView 的四级缓存机制
这是底层的**核心架构分歧**。ListView 只有 2 级缓存(ActiveViews + ScrapViews),RecyclerView 扩展到了 4 级:
滑动列表时,ViewHolder 的复用路径:
查找顺序(从快到慢):
┌─────────────────────────────────────────────────────────────────┐
│ 第1级: mAttachedScrap / mChangedScrap │
│ → 屏幕内的 ViewHolder,数据未变则直接复用,不调 onBindViewHolder │
│ → 适用于 notifyItemChanged 时还在屏幕上的 item │
├─────────────────────────────────────────────────────────────────┤
│ 第2级: mCachedViews (默认容量 2) │
│ → 刚滑出屏幕的 ViewHolder,按 position 匹配 │
│ → position 匹配则直接复用,不调 onBindViewHolder │
│ → 理解为"回退缓存"——用户滑回来能秒加载 │
├─────────────────────────────────────────────────────────────────┤
│ 第3级: ViewCacheExtension(自定义缓存,很少使用) │
│ → 开发者可以自己实现的缓存层 │
├─────────────────────────────────────────────────────────────────┤
│ 第4级: RecycledViewPool (默认每种 viewType 缓存 5 个) │
│ → 按 viewType 分组存放,只保留 ViewHolder 的 View 结构 │
│ → 复用时必须重新调用 onBindViewHolder 绑定数据 │
│ → 多个 RecyclerView 可共享一个 Pool │
└─────────────────────────────────────────────────────────────────┘
命中失败 → onCreateViewHolder() 创建新的 ViewHolder
是否需要重新绑定数据?
mAttachedScrap → 不需要 onBindViewHolder ✅ 最快
mCachedViews → 不需要 onBindViewHolder ✅ 很快
ViewCacheExtension → 取决于实现
RecycledViewPool → 需要 onBindViewHolder ⚠️ 有开销
创建新的 → onCreateViewHolder + onBindViewHolder ❌ 最慢
源码关键路径
// RecyclerView.Recycler.tryGetViewHolderForPositionByDeadline() 简化版
ViewHolder tryGetViewHolderForPositionByDeadline(int position, ...) {
ViewHolder holder = null;
// 1. 从 mChangedScrap 中查找(预布局阶段)
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
}
// 2. 从 mAttachedScrap / mCachedViews 中按 position 查找
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position);
}
// 3. 按 stable id 查找(如果 adapter 设置了 hasStableIds)
if (holder == null) {
holder = getScrapOrCachedViewForId(id, type);
}
// 4. 从 ViewCacheExtension 查找
if (holder == null && mViewCacheExtension != null) {
View view = mViewCacheExtension.getViewForPositionAndType(recycler, position, type);
if (view != null) holder = getChildViewHolder(view);
}
// 5. 从 RecycledViewPool 查找
if (holder == null) {
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal(); // 清除旧数据标记
// 需要重新绑定
}
}
// 6. 都没有 → 创建新的
if (holder == null) {
holder = mAdapter.createViewHolder(this, type);
}
return holder;
}
与 ListView 缓存的对比
ListView 缓存 RecyclerView 缓存
┌──────────────────┐ ┌──────────────────┐
│ ActiveViews │ 屏幕上的 │ mAttachedScrap │ 屏幕上的
│ (不需要绑定) │←——————————→ │ (不需要绑定) │
├──────────────────┤ ├──────────────────┤
│ ScrapViews │ 回收池 │ mCachedViews │ 按 position 缓存
│ (需要重新绑定) │ │ (不需要绑定) │ ← ListView 没有此层!
│ │ ├──────────────────┤
│ │ │ ViewCacheExtension│ 自定义
│ │←——————————→ ├──────────────────┤
│ │ │ RecycledViewPool │ 按 type 回收
│ │ │ (需要重新绑定) │
└──────────────────┘ └──────────────────┘
RecyclerView 多出的 mCachedViews 是性能提升的关键——用户来回滑动时,刚滑出的 item 无需重新绑定数据即可复用。
DiffUtil:智能差量更新
notifyDataSetChanged() 会刷新整个列表,效率低且无动画。DiffUtil 使用 Eugene W. Myers 差分算法计算新旧列表的最小编辑操作:
class QuestionDiffCallback(
private val oldList: List<Question>,
private val newList: List<Question>
) : DiffUtil.Callback() {
override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size
// 判断是否是同一个 item(通常对比 id)
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos].id == newList[newPos].id
}
// 判断内容是否变化(决定是否需要重新绑定)
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
return oldList[oldPos] == newList[newPos]
}
}
// 使用
val diffResult = DiffUtil.calculateDiff(QuestionDiffCallback(oldList, newList))
diffResult.dispatchUpdatesTo(adapter) // 自动调用 notifyItemInserted/Removed/Changed
更推荐 ListAdapter——它封装了 DiffUtil 并在后台线程计算差异:
class QuestionAdapter : ListAdapter<Question, QuestionAdapter.ViewHolder>(DIFF_CALLBACK) {
companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Question>() {
override fun areItemsTheSame(old: Question, new: Question) = old.id == new.id
override fun areContentsTheSame(old: Question, new: Question) = old == new
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { ... }
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position) // ListAdapter 提供的方法
holder.bind(item)
}
}
// 更新列表:自动在后台线程计算 diff
adapter.submitList(newList)
性能优化策略
基础优化
// 1. item 高度固定时,避免每次 notifyXxx 都重新测量
recyclerView.setHasFixedSize(true)
// 2. 增大 CachedViews 容量(默认 2)
recyclerView.setItemViewCacheSize(4)
// 3. 多个 RecyclerView 共享 ViewPool(如 ViewPager + RecyclerView)
val sharedPool = RecyclerView.RecycledViewPool()
recyclerView1.setRecycledViewPool(sharedPool)
recyclerView2.setRecycledViewPool(sharedPool)
// 4. 预取(默认开启,LayoutManager 会在滑动时预取即将出现的 item)
(recyclerView.layoutManager as LinearLayoutManager).initialPrefetchItemCount = 4
onBindViewHolder 中的注意事项
// ❌ Wrong: 每次绑定都创建新的 listener 对象
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.itemView.setOnClickListener { onItemClick(position) } // 每次都 new
}
// ✅ Right: 在 onCreateViewHolder 中设置 listener,通过 adapterPosition 获取位置
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item, parent, false)
val holder = ViewHolder(view)
view.setOnClickListener {
val pos = holder.adapterPosition
if (pos != RecyclerView.NO_POSITION) onItemClick(pos)
}
return holder
}
图片列表优化
// 滑动时暂停图片加载,停下后恢复
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
when (newState) {
RecyclerView.SCROLL_STATE_IDLE -> Glide.with(context).resumeRequests()
else -> Glide.with(context).pauseRequests()
}
}
})
多类型 Item
class MultiTypeAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val TYPE_HEADER = 0
const val TYPE_CONTENT = 1
const val TYPE_FOOTER = 2
}
override fun getItemViewType(position: Int): Int {
return when {
position == 0 -> TYPE_HEADER
position == itemCount - 1 -> TYPE_FOOTER
else -> TYPE_CONTENT
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_HEADER -> HeaderViewHolder(inflateView(R.layout.item_header, parent))
TYPE_FOOTER -> FooterViewHolder(inflateView(R.layout.item_footer, parent))
else -> ContentViewHolder(inflateView(R.layout.item_content, parent))
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is HeaderViewHolder -> holder.bind(headerData)
is ContentViewHolder -> holder.bind(contentList[position - 1])
is FooterViewHolder -> holder.bind(footerData)
}
}
}
生产高频考点总结
| 问题 | 要点 |
|---|---|
| RecyclerView 的四级缓存 | Scrap → CachedViews → Extension → Pool,前两级不需要 rebind |
| CachedViews 与 Pool 的区别 | CachedViews 按 position 匹配(不需要 rebind);Pool 按 viewType(需要 rebind) |
| 与 ListView 缓存的本质差异 | 多了 CachedViews 层——来回滑动时的"免绑定"缓存 |
| DiffUtil 的作用 | 用 Myers 算法计算最小编辑距离,只更新变化的 item,带动画 |
setHasFixedSize(true) 的作用 |
告诉 RecyclerView item 大小固定,跳过 requestLayout() |
为什么没有 setOnItemClickListener |
组件化设计,点击事件交给开发者在 ViewHolder 中处理,更灵活 |
| 多个 RecyclerView 怎么共享缓存 | 共享同一个 RecycledViewPool 实例 |
ListAdapter vs 普通 Adapter |
ListAdapter 内置 DiffUtil,后台线程计算差异,API 更简洁 |