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 工作流程 数据源 List<ItemData> Adapter onCreateViewHolder ViewHolder 缓存 View 引用 回收池 (Recycled Pool) 复用 ViewHolder onBindViewHolder 填充数据到 View 数据从数据源 → Adapter → 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 是怎么配合得更紧密的。


公众号:蓝海资料掘金营,微信 deep3321