资源优化之图片压缩

说到包体瘦身,图片压缩是第一个绕不开的坎儿。我翻过不少APK,里面最大的空间占用往往就是那几十上百张图片。你想想看,一张高清照片动辄几兆,放到手机里就成了包体膨胀的元凶。

为什么图片是包体大户?

一张典型的1080p截图约2-3MB,一张1920×1080的PNG图标可能也要几百KB。Android资源目录里如果塞满了未经压缩的PNG和JPEG,包体直奔30MB以上是常事。说白了,图片压缩就是“用更小的体积,保住差不多的显示效果”。

第一步:选对格式

格式选错了,后面再怎么压也打折扣。我建议按这个优先级来:

格式适用场景压缩率透明度
WebP照片、图标、UI素材比PNG小25%-35%支持
AVIF对包体要求极致的场景比WebP再小约20%支持
PNG需要无损、边缘清晰的图标较高支持
JPEG照片、无需透明的背景图中等不支持

我个人习惯优先用WebP。原因很简单:兼容性够用(Android 4.2+原生支持),压缩率又漂亮。除非项目必须兼容4.0以下,否则我基本不再放PNG。

第二步:工具压一压

格式选好了,原始图片还得再压一道。我用得最多的三个工具:

  • cwebp:Google官方WebP编码器,命令行批量处理。
  • pngquant:专门压PNG,能把256色图压到几十KB。
  • TinyPNG:在线/API服务,适合非技术同学。

坦白说,我项目中更依赖cwebp,因为可以写到构建脚本里自动执行。下面是一个简单的脚本片段:

# 批量转换 PNG 到 WebP,质量 80
for img in *.png; do
  cwebp -q 80 "$img" -o "${img%.png}.webp"
done

嗯,注意质量值别设太低。80左右肉眼基本看不出差异,体积能压一半。

第三步:Android 资源目录的玄机

图片放进drawable后,还有一招——利用资源限定符。我见过不少人直接把一套大图扔进drawable-mdpi里,结果高密度设备上图片被放大后模糊,低密度设备上又浪费内存。正确的做法是:

  • 只放一套高密度图(比如xxhdpi或xxxhdpi)
  • 用代替简单图标
  • 大背景图用.9.png或WebP

这里有个容易踩的坑:.9.png文件如果被WebP转换工具处理过,拉伸区域会失效。我曾经因为批量脚本把.9.png也转了,结果界面变形,排查了半天才发现。所以千万不要对.9.png做有损压缩,最多用无损模式。

第四步:构建时自动压缩

如果你用Android Gradle插件,开启minifyEnabled是不够的,得加上图片压缩的配置。我在build.gradle里这样配:

android {
    buildTypes {
        release {
            // 启用资源混淆(和压缩)
            minifyEnabled true
            shrinkResources true
            // 注意:shrinkResources 会移除未引用的资源
        }
    }
}

但这只删文件,不压缩体积。要真正压缩图片,需要借助第三方插件或任务。我推荐用(注:原文为链接,但此处按规范不输出链接,仅文字描述) Facebook 的 redex,或者简单地在构建前跑一遍cwebp脚本。更省事的是用(同上) Lossless JPEG/PNG Optimization 插件。图省事的话,直接扔给TinyPNG的Gradle插件也行。

第五步:运行时选图

图片进了APK,下载时还能再省一步。Glide这样的图片加载库,支持在下载时指定格式。比如:

Glide.with(context)
    .load(url)
    .format(DecodeFormat.PREFER_RGB_565) // 不对!用这个会丢颜色
    // 正确做法:用 .diskCacheStrategy(DiskCacheStrategy.RESOURCE) 
    // 配合服务端返回 WebP 或 AVIF
    .into(imageView);

注意,PREFER_RGB_565会让图片偏色,我建议别用。更好的办法是让后端直接返回WebP或AVIF格式的图片,配合Glide的自动解码,既省流量又省包体。

核心要点:图片压缩要从“源头→构建→运行时”三阶段发力,单靠一个环节是压不狠的。

避坑指南

  • 我曾经在WebP转换时用了-q 50,结果UI上某些渐变色出现了明显色块,设计师找上门来。后来改到75才平衡。
  • 注意Android 4.0以下设备不支持WebP,如果项目必须兼容,可以用libwebp库回退。
  • shrinkResources虽然能删掉无用图片,但注意它会根据代码引用判断,如果用反射或动态加载图片,最好显式声明keep.xml。
小技巧:用pngcrush把PNG的元数据(EXIF、ICC profile)全部清理掉,能再省5%-10%空间。这个操作不影响视觉效果,我通常直接加到CI脚本里。
警告:不要对启动页、Logo等关键图片过度压缩。质量低于60可能会产生肉眼可见的伪影,影响品牌形象。

知识结构图

下面这张图把这章的核心逻辑串起来了——从原始图片到最终APK,每一步都能压一点。

图片压缩知识体系 原始图片(PNG/JPEG) 格式选择(WebP优先) 工具压缩(cwebp/pngquant) 资源目录建议(xxxhdpi) 构建脚本+shrinkResources 最终APK(体积减半) 每一步可压20~40%

总结几个硬指标

我一般给团队定的目标:

  • 所有图标用WebP,质量≥75
  • 单张图片不超过500KB(除启动图外)
  • 构建脚本必须跑一遍批量压缩
  • 运行时网络图片尽量要求后端转WebP/AVIF

嗯,做到这些,APK包体轻松压掉30%~50%。你想想看,本来30MB的包,最后可能只有18MB,用户下载心理门槛直接降低一档。

对了,还有个小细节:记得在proguard-rules.pro里保留WebP相关的类,不然混淆后可能会解码失败。我吃过这亏,后来老老实实加了-keep class com.google.webp.** { *; }。