26、性能优化(二):图片加载优化、异步绑定数据

各位同学,咱们接着聊性能优化。上一章我们讲了布局和内存方面的坑,这一章我重点说说图片加载和数据绑定。这两个问题,说白了就是「卡顿」的两大元凶。你想想看,用户滑动列表时,如果图片一张张地闪白、加载转圈,或者数据绑定导致界面一卡一卡的,体验能好吗?

我在项目中遇到过好几次,明明代码逻辑没问题,但列表就是滑不动。后来一查,全是图片加载和主线程绑定的锅。嗯,今天咱们就把这两个问题彻底讲透。

图片加载优化:别让图片拖垮你的列表

图片加载是 RecyclerView 性能的头号杀手。为什么?因为图片占内存大,解码耗时,而且很容易在主线程上搞事情。

我个人习惯,在项目里统一用 Glide 或 Coil 这样的图片加载库。别自己写 BitmapFactory.decodeFile,那玩意儿坑太多。但光用库还不够,你得知道怎么配置才能发挥最大性能。

1. 图片尺寸裁剪

很多后端返回的图片都是原图,动不动 1920x1080。你列表里的 ImageView 才 100dp 宽,加载原图不是浪费吗?

Glide 里可以这样指定尺寸:

Glide.with(context)
    .load(url)
    .override(200, 200)  // 强制裁剪到 200x200
    .into(imageView)

Coil 更简洁:

imageView.load(url) {
    size(200, 200)
}

这样内存占用能减少 80% 以上。我曾经接手过一个项目,列表里加载 1080p 的图片,内存直接飙到 300MB。加上 override 之后,降到 60MB,滑动流畅多了。

2. 内存缓存与磁盘缓存

图片加载库默认都有缓存,但你要确认缓存策略对不对。Glide 默认内存缓存是 LRU,磁盘缓存是 250MB。如果你列表里的图片经常变化,可以调整缓存大小:

Glide.get(context).setMemoryCategory(MemoryCategory.HIGH)

Coil 的缓存配置:

val imageLoader = ImageLoader.Builder(context)
    .memoryCachePolicy(CachePolicy.ENABLED)
    .memoryCache {
        MemoryCache.Builder()
            .maxSizePercent(0.25)  // 占用 25% 的应用内存
            .build()
    }
    .build()

这里有个坑:不要无限制地增大缓存。我见过有人把内存缓存设到 80%,结果其他组件 OOM 了。一般 20%-30% 就够用。

3. 图片格式选择

WebP 格式比 PNG 小 30%-50%,而且 Android 原生支持。如果后端支持,尽量让后端返回 WebP。Glide 和 Coil 都自动支持 WebP,你不需要额外配置。

如果后端只能返回 JPEG,那也没关系。Glide 默认会用 RGB_565 格式解码,比 ARGB_8888 省一半内存。但要注意,RGB_565 不支持透明度,如果你的图片有透明区域,得手动指定:

Glide.with(context)
    .load(url)
    .format(DecodeFormat.PREFER_ARGB_8888)
    .into(imageView)

核心原则:图片加载的优化,本质上是「用最小的资源,展示最好的效果」。尺寸裁剪、缓存策略、格式选择,这三板斧下去,90% 的图片问题都能解决。

异步绑定数据:别让主线程干脏活

图片加载搞定了,接下来是数据绑定。很多人在 onBindViewHolder 里直接做耗时操作,比如解析 JSON、计算复杂公式、读写数据库。这会导致列表滑动时掉帧。

为什么会这样?因为 onBindViewHolder 是在主线程执行的。你想想看,用户手指一滑,RecyclerView 要立刻调用 onBindViewHolder 来更新界面。如果你在里面做耗时操作,主线程就被卡住了,掉帧就来了。

1. 使用 DiffUtil 异步计算差异

DiffUtil 默认是在主线程计算的。如果你的列表数据量很大(比如超过 1000 条),计算差异本身就会卡住主线程。这时候要用 AsyncListDiffer:

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() {
    private val differ = AsyncListDiffer(this, MyDiffCallback())

    fun submitList(list: List<MyItem>) {
        differ.submitList(list)  // 自动在后台线程计算差异
    }

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        val item = differ.currentList[position]
        // 这里只做 UI 绑定,不做耗时操作
        holder.bind(item)
    }
}

AsyncListDiffer 内部用 AsyncTask 在后台线程计算差异,计算完再在主线程通知更新。这样就不会卡主线程了。

2. 数据预处理:把脏活提前干完

我建议在数据进入 Adapter 之前,就把所有需要计算的东西算好。比如你的列表项需要显示「距离现在多久」,别在 onBindViewHolder 里算,而是在数据层算好:

data class MyItem(
    val id: Int,
    val title: String,
    val timestamp: Long,
    val timeAgo: String  // 提前算好,比如 "3分钟前"
)

这样 onBindViewHolder 里只需要直接赋值,没有任何计算。我曾经在一个聊天列表里这么干过,滑动性能从 45fps 提升到 60fps。

3. 使用 ViewBinding 减少 findViewById

ViewBinding 比 findViewById 快,因为它内部做了缓存。而且它生成的绑定代码是编译期确定的,没有反射开销。

class MyViewHolder(private val binding: ItemMyBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(item: MyItem) {
        binding.title.text = item.title
        binding.timeAgo.text = item.timeAgo
    }
}

如果你还在用 findViewById,赶紧换成 ViewBinding。性能提升虽然不大,但积少成多。

小技巧:在 onBindViewHolder 里,尽量只做赋值操作。任何 if-else、循环、字符串拼接,都尽量提前处理好。记住:onBindViewHolder 是高频调用,每毫秒都很宝贵。

知识体系总览

下面这张图总结了本章的核心内容,你可以对照着检查自己的项目:

RecyclerView 性能优化(二) 图片加载优化 尺寸裁剪:override(200, 200) 减少内存占用 80% 缓存策略:内存 + 磁盘 内存缓存 20%-30% 图片格式:WebP / RGB_565 比 PNG 小 30%-50% 统一使用 Glide / Coil 避免自己写 BitmapFactory 异步绑定数据 AsyncListDiffer 异步计算 后台线程计算差异 数据预处理 提前算好 timeAgo 等字段 ViewBinding 替代 findViewById 编译期绑定,无反射 onBindViewHolder 只做赋值 不做 if-else、循环、拼接

注意:异步绑定数据不是让你在 onBindViewHolder 里开线程。那样反而更糟,因为线程切换有开销,而且容易造成数据不一致。正确的做法是:在数据层异步处理,在 UI 层直接使用处理好的结果

实战建议

说了这么多,我总结几条实战中可以直接用的建议:

  • 图片加载:统一用 Coil(Kotlin 原生)或 Glide,配置好尺寸裁剪和缓存策略。别偷懒用 ImageView 的 scaleType 硬撑。
  • 数据绑定:用 AsyncListDiffer 替代 notifyDataSetChanged,用 ViewBinding 替代 findViewById。
  • 性能监控:用 Android Studio 的 Profile 工具,看看 onBindViewHolder 的执行时间。如果超过 16ms,就得优化了。
  • 避坑:我曾经在 onBindViewHolder 里调用了 Room 数据库查询,结果列表一滑动就卡死。后来把查询移到 ViewModel 里,用 LiveData 观察数据变化,问题就解决了。

嗯,这一章的内容就到这里。记住:性能优化不是一蹴而就的,而是要在每个细节上抠。图片加载和数据绑定是 RecyclerView 性能的两大支柱,把这两个搞定了,你的列表就能飞起来。


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