11、图片优化:图片格式选择、Glide与Coil对比、图片压缩策略、三级缓存原理

图片优化,说白了就是跟内存和流量较劲。我做了这么多年Android性能优化,最头疼的就是图片。一张大图没处理好,直接OOM,用户骂娘,你还得背锅。今天咱们就把图片优化这块掰开揉碎了讲清楚。

一、图片格式怎么选?WebP还是AVIF?

先聊聊格式。我个人习惯,能上WebP就上WebP。为什么?因为它在Android生态里支持得最好,压缩率比PNG高30%左右,而且支持透明通道。我在项目中遇到过,一套UI切图从PNG换成WebP,包体积直接少了40%。

格式 压缩率 透明通道 Android支持 适用场景
PNG 支持 原生 需要无损、透明图
JPEG 不支持 原生 照片、大背景图
WebP 支持 Android 4.0+ 推荐首选
AVIF 极高 支持 Android 12+ 高端机型、新项目

AVIF是后起之秀,压缩率比WebP再高20%左右。但注意,它需要Android 12以上才原生支持。你想想看,如果你的App还要兼容老机型,用AVIF就得自己解码,那性能反而得不偿失。我建议,新项目可以上AVIF,老项目老老实实用WebP。

我的习惯:服务端下发图片时,根据客户端上报的Android版本,动态返回WebP或AVIF。这样既照顾了老机型,又让新机型享受更好的压缩。

二、Glide vs Coil:到底选哪个?

这两个库我都深度用过。Glide是老牌劲旅,Coil是后起之秀。说实话,没有绝对的好坏,看你的场景。

Glide的优势

  • 生态成熟,踩坑的人多,网上资料丰富
  • 支持GIF、视频帧、自定义解码器
  • 内存管理做得不错,有三级缓存

Coil的优势

  • 纯Kotlin实现,协程原生支持
  • 包体积小,只有Glide的1/3左右
  • API更简洁,链式调用很舒服

我记得有一次,项目里图片加载特别频繁,Glide的Bitmap池偶尔会抖动。换成Coil后,因为它的内存管理基于Kotlin协程,反而更稳定。但如果你项目里还有大量Java代码,Glide会更友好。

避坑指南:我曾经在Glide里踩过一个坑——默认的磁盘缓存策略是缓存原始图片。如果你加载的图片尺寸很大,磁盘缓存会迅速膨胀。记得设置.diskCacheStrategy(DiskCacheStrategy.RESOURCE)来缓存缩放后的图片。

三、图片压缩策略:别一股脑全压

图片压缩不是越狠越好。你想想看,一张缩略图压到10KB没问题,但一张全屏背景图压到10KB,那画质就惨不忍睹了。我的策略是分场景处理:

  1. 缩略图/列表图:质量压缩到60%-70%,尺寸按控件大小裁剪。用Glide的.override()指定宽高。
  2. 详情页大图:质量压缩到80%左右,尺寸不超过屏幕宽高。用BitmapFactory.Options的inSampleSize做采样。
  3. 背景图/启动图:质量压缩到90%,尺寸按需裁剪。这类图用户看得最久,画质不能太差。
// 我的常用压缩代码片段
fun compressBitmap(bitmap: Bitmap, maxSize: Int): Bitmap {
    val ratio = minOf(
        maxSize.toFloat() / bitmap.width,
        maxSize.toFloat() / bitmap.height
    )
    if (ratio >= 1f) return bitmap
    
    val matrix = Matrix().apply { setScale(ratio, ratio) }
    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}
注意:Bitmap.createBitmap()会创建新对象,频繁调用容易OOM。建议配合复用池使用,或者直接用Glide的transform。

四、三级缓存原理:一张图的生命周期

三级缓存,说白了就是内存、磁盘、网络三层。我画了个图,你看一眼就明白了:

三级缓存流程图 内存缓存 LruCache / 弱引用 未命中 磁盘缓存 DiskLruCache 未命中 网络加载 HTTP / CDN 加载流程 1. 先查内存缓存 2. 未命中查磁盘缓存 3. 仍未命中走网络 4. 网络加载成功后 写入磁盘 + 内存 5. 下次直接从缓存读 速度:内存 > 磁盘 > 网络

流程其实很简单:

  • 第一步:要加载一张图,先查内存缓存。内存里有没有?有就直接用,没有就下一步。
  • 第二步:查磁盘缓存。磁盘里有没有?有就加载到内存,同时返回给UI。没有就下一步。
  • 第三步:走网络下载。下载成功后,先写入磁盘缓存,再写入内存缓存,最后返回给UI。

这里有个细节:内存缓存用的是LruCache,说白了就是最近最少使用算法。当内存不够时,最久没用的图片会被淘汰。磁盘缓存用的是DiskLruCache,原理类似,但存的是文件。

我的经验:内存缓存的大小,我一般设为App可用内存的1/8。太大容易OOM,太小又频繁触发磁盘IO。你可以用ActivityManager.getMemoryClass()来获取可用内存,然后计算。

嗯,图片优化这块,说白了就是空间换时间,时间换体验。格式选对了,库用对了,缓存策略搞清楚了,你的App在图片加载这块基本就稳了。我曾经见过一个项目,图片加载慢得离谱,一查发现磁盘缓存没开,每次都要从网络拉。开了缓存后,加载速度提升了3倍不止。

小技巧:Glide默认的磁盘缓存大小是250MB。如果你的App图片特别多,可以适当调大,比如500MB。但别超过1GB,否则用户存储空间受不了。

最后说一句,图片优化没有银弹。你得根据自己App的实际情况,去调参数、选方案。多测、多试、多观察,慢慢就能找到最适合你的那条路。


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