5、ViewHolder 模式:为什么需要 ViewHolder?如何自定义 ViewHolder?
讲 RecyclerView,绕不开 ViewHolder。很多新手朋友刚接触时,会觉得这玩意儿多此一举——不就是显示个列表吗?直接 findViewById 不就行了?嗯,我刚开始也是这么想的。直到我在一个项目里,用最原始的方式写了一个几百项的列表,滑动起来卡得让人崩溃……
说白了,ViewHolder 就是 RecyclerView 高性能的基石。没有它,你的列表就是个「每次滑动都重新创建一切」的笨重家伙。
为什么需要 ViewHolder?
先想想看,列表滑动时发生了什么?
- 新的 item 进入屏幕,需要创建布局
- 旧的 item 滑出屏幕,需要回收
- 每个 item 里的 TextView、ImageView 都要 findViewById
问题就出在 findViewById 上。这个方法会从布局树的根节点开始,递归查找你传入的 id。一次两次还好,但列表快速滑动时,每秒可能触发几十次甚至上百次查找。你想想看,这得浪费多少性能?
核心痛点:findViewById 是耗时操作,频繁调用会导致 UI 线程卡顿,掉帧、卡顿随之而来。
ViewHolder 模式就是来解决这个问题的。它的思路很简单:把 findViewById 的结果缓存起来。每个 item 的布局创建时,只调用一次 findViewById,之后就把这些 View 引用存在 ViewHolder 里。下次复用这个 item 时,直接从 ViewHolder 拿,不用再找了。
我在项目中遇到过这样一个场景:一个聊天列表,每条消息都有头像、昵称、时间、内容、状态图标……如果每次绑定数据都 findViewById,那画面简直不敢想。用了 ViewHolder 之后,滑动流畅度提升非常明显。
ViewHolder 的工作原理
RecyclerView 内部维护了一个回收池。当 item 滑出屏幕时,它不会被销毁,而是被回收。新的 item 滑入时,直接从回收池里拿一个旧的 item 来复用。这时候,ViewHolder 就派上用场了——它带着之前缓存好的 View 引用,直接绑定新数据就行。
小提示:RecyclerView 默认会尝试复用 ViewHolder。如果你没有正确实现 ViewHolder,它就只能每次都创建新的,回收机制就形同虚设了。
来看一张图,帮你理解整个流程:
如何自定义 ViewHolder?
自定义 ViewHolder 其实很简单。你只需要继承 RecyclerView.ViewHolder,在构造函数里把 item 布局里的子 View 都 findViewById 一遍,存成成员变量就行。
来看一个实际例子。假设我们要做一个商品列表,每个 item 显示商品图片、名称和价格:
class ProductViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivProduct: ImageView = itemView.findViewById(R.id.iv_product)
val tvName: TextView = itemView.findViewById(R.id.tv_name)
val tvPrice: TextView = itemView.findViewById(R.id.tv_price)
fun bind(product: Product) {
// 加载图片
Glide.with(itemView.context)
.load(product.imageUrl)
.into(ivProduct)
// 设置文本
tvName.text = product.name
tvPrice.text = "¥${product.price}"
}
}
然后在 Adapter 里这样用:
class ProductAdapter(private val productList: List<Product>) :
RecyclerView.Adapter<ProductViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProductViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_product, parent, false)
return ProductViewHolder(view)
}
override fun onBindViewHolder(holder: ProductViewHolder, position: Int) {
holder.bind(productList[position])
}
override fun getItemCount(): Int = productList.size
}
个人习惯:我喜欢在 ViewHolder 里写一个 bind() 方法,把数据传进去,让 ViewHolder 自己负责填充。这样 Adapter 的 onBindViewHolder 就变得非常干净,只需要一行 holder.bind(data) 就够了。
ViewHolder 的最佳实践
用了一段时间 ViewHolder 之后,我总结了几条经验,分享给你:
- 不要在 ViewHolder 里做耗时操作——比如网络请求、数据库查询。ViewHolder 只负责 UI 绑定,数据应该提前准备好。
- 避免在 bind() 里创建新对象——比如每次绑定都 new 一个 OnClickListener。可以复用同一个 listener,或者用 lambda 表达式。
- 注意图片加载的复用问题——RecyclerView 复用 item 时,图片可能会闪烁或错乱。用 Glide 或 Coil 这类库时,它们会自动处理,但如果你自己加载图片,记得在 bind() 里取消之前的加载任务。
我曾经踩过的一个坑:在一个聊天列表里,每条消息的 ViewHolder 里都 new 了一个 OnClickListener。列表长的时候,内存里瞬间多了几百个 listener 对象。虽然不至于崩溃,但 GC 频繁触发,滑动明显变卡。后来改成在 ViewHolder 的 init 块里只创建一次 listener,问题就解决了。
ViewHolder 的类型与多布局
有时候,一个列表里会有多种不同的 item 布局。比如聊天列表里,有文本消息、图片消息、语音消息。这时候就需要多种 ViewHolder。
RecyclerView 支持通过 getItemViewType() 返回不同的类型,Adapter 会根据类型创建不同的 ViewHolder:
override fun getItemViewType(position: Int): Int {
return when (messages[position].type) {
MessageType.TEXT -> TYPE_TEXT
MessageType.IMAGE -> TYPE_IMAGE
MessageType.VOICE -> TYPE_VOICE
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_TEXT -> TextMessageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_text_message, parent, false)
)
TYPE_IMAGE -> ImageMessageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_image_message, parent, false)
)
TYPE_VOICE -> VoiceMessageViewHolder(
LayoutInflater.from(parent.context)
.inflate(R.layout.item_voice_message, parent, false)
)
else -> throw IllegalArgumentException("Unknown viewType: $viewType")
}
}
每种 ViewHolder 各自缓存自己布局里的 View,互不干扰。这样既灵活又高效。
总结一下
ViewHolder 模式,说白了就是「用空间换时间」——用一点内存缓存 View 引用,换来 findViewById 的零调用。在 RecyclerView 这种高频复用的场景下,这个交换非常划算。
自定义 ViewHolder 也不复杂:继承 RecyclerView.ViewHolder,在构造函数里缓存子 View,在 bind() 方法里填充数据。记住不要在 bind() 里做耗时操作和创建新对象,你的列表就能跑得很流畅。
嗯,ViewHolder 就讲到这里。下一节我们聊聊 Adapter 的更多细节,到时候你会看到 ViewHolder 和 Adapter 是怎么配合得更紧密的。