有时候,当你回头看看自己走了多远时,才能更好的衡量自己的进步。
引言
优化目的
首先我们需要了解,优化包体积大小的目的是什么?总结有以下几点:
-
下载转化率
以 Google Play 的 数据统计 每减少 10M 大小,有 1.5% 的下载转化率提升。
-
推广成本
包体积对渠道推广成本和厂商预装单价会有比较大的影响。
-
应用性能
具体表现在安装时间、运行内存、ROM 空间。应用在首次安装启动时,过多的 odex 文件编译会严重影响冷启动时间;应用在运行时,Resource 资源、Library 以及 Dex 类加载这些都会占用不少内存;安装包在解压时,占用 ROM 空间可能会翻倍,对 ROM 空间占用也会有一定压力。
-
编译耗时
过多的 dex 和 res 会影响编译时长。
现状梳理
在最近几个版本中,我们针对资源做了一点微小的工作。之所以选择资源这一块,是因为资源占比是最大的,以 v5.10.0 版本的数据来看:
类别 | 大小(单位:M) | 占比 |
---|---|---|
res | 65.9 | 40.0% |
assets | 9.4 | 5.7% |
resources.arsc | 10.4 | 6.3% |
资源总计占比达到了 52%,所以我们就决定从资源入手。
当然更主要的原因是 :
-
较简单、不易出问题
相较于 dex 和 so 的优化,资源的优化手段较为简单,能删则删,不能删就压缩,没有什么黑科技的手段,也就不易出问题。dex 一般涉及到字节码优化,比如删除冗余的赋值指令、dex 重分包等等;so 的优化需要良好的基建配套支持,这些都很容易出线上问题,我们目前没有完善的灰度流程和热修复方案,所以一旦出了问题就直接尬住了。
-
易量化、所见即所得
包体积的优化收益很容易量化,前后一对比就知道了,不会受其他环境影响。其他的比如启动优化,线下测试可能收益不错,上线了之后就不及预期了,线上环境很复杂,可能需要考虑到很多控制变量。
所以,前期还是先找软柿子捏。
手段
版本 | 优化点 | 大小(单位:M) | 优化效果(单位:M) | 说明 |
---|---|---|---|---|
v5.11 | / | 168.8 | / | 基线版本 |
v5.12 | png 转 webp | 129.5 | -39.53 | 输出的报告是减少了 51M,差值的 12M 是 AAPT2 自带的 crunchPngs 压缩 PNG 导致 |
v5.14 | 删除无用 assets 资源 | 148.6 | -0.9 | 删除了 33 个无用 assets 资源,减少占比 -10% |
v5.16 | APK 增量分析 | / | / | 防裂化手段 |
图片压缩
目前 NIO APP minSdk = 24,可以全量使用 webp,根据 官方文档 的数据,无损的 webp 图片比 png 可减少 26% 的大小。具体的创建 webp 操作可见官方文档:创建 WebP 图片
这种方式存在两个问题:
- 需要推动 AAR 的各个业务方去转化
- 无法处理第三方库里面的图片
实现原理
一劳永逸的做法是在打 release 包时,自动转化所有的 png 图片,核心操作步骤是:
- BaseVariant#getAllRawAndroidResources() 可获取所有的资源文件,过滤出待转化的 png 图片
- 使用 cwebp 工具转化所有的图片
注意事项
需要注意根据 Google Play 图标设计规范,需要将启动图标加入豁免,还有就是过滤掉 .9 图。
无用 assets 资源删除
shrinkResources 无法处理 assets 资源,如何找出未被使用的 assets 资源呢?
实现原理
实现原理类似于 Matrix#UnusedAssetsTask,该方案的是:搜索 smali 文件中引用字符串常量的指令,判断引用的字符串常量是否某个 assets 文件的名称。而我们的做法是:
所以核心问题就变成了:如何获取所有的引用字符串?
我们的做法是:在 assembleRelease 后解析 resources.txt 文件,匹配出所有的引用字符串,所以该任务是依赖于 assembleReleaseTask。
其实思路都是来源于 AGP 的 ResourceUsageAnalyzer.java 实现:
注意事项
有以下情况 assets 资源需要加入白名单:
- Dead Code 引用的 assets 资源(比如 XxLottieCheckBox 用到了 xx_checkbox_ok.json)
- Layout 文件中引用的 assets 资源,典型的就是 lottie 资源文件
- 其他资源文件(比如 raw 类型的文件)引用的 assets 资源
APK 增量分析
APK 每个版本都有近 10M 的增量,但是无法知道这些增量来源于哪里?不利于 App 包体积良性增长。
如果在某个业务中引用了较大的资源文件,如何能够及时发现呢?
那么,这正是这个任务所要解决的问题。我们在 jenkins 上配置了这个任务,jenkins 打包后会生成一个分析报告:
解析 APK
这一步主要的目标是,解析 Apk 生成文件列表。它涉及解析依赖、解 dex 文件、解混淆。
其中比较麻烦的是资源的混淆问题。
不同于类的反混淆,资源混淆是没有默认生成的 mapping 文件的。那如何去解决这个问题呢?
我们首先想到的是使用 md5 对比 resources-release.ap_ 和 resources-release-optimize.ap_ 文件,这可以解决绝大部分的资源混淆问题。因为 OptimizeResourcesTask 默认只做 “–shorten-resource-paths” 处理,即缩短资源路径,不会对文件内容处理,所以可以通过对比混淆前后资源文件的 md5 值,得到混淆前后资源名的映射。但有一种情况例外,即资源未被使用,在 ShrinkResourcesTask 时被重写成了空文件。
这种情况下,就无法使用 md5 对比了。那还有什么办法呢?
其实 AAPT2 提供了生成资源 mapping 文件的命令行参数,见:AAPT2 – 优化选项,但是该参数,没法通过 gradle.properties 或 aaptOptions 来指定,所以我们最终解决方案就是,使用 AAPT2 对 resources-release.ap_ 文件进行再处理一次,获取到 mapping 文件即可。混淆规则可见:ResourcePathShortener。
增量分析
增量分析这一步的目标是,对比上一个版本的文件列表,输出差异。那上一个版本的文件列表是如何存储的呢?
其实是存储在了 {projectDir}/lavender-plugin/apk/previous.json 下,在第一次运行该任务时,就会把当前版本的 Apk 文件列表存储至此。
可视化
如何高效的推进?它涉及两个问题:
- 如何定位问题所属业务方?
- 报告有无更加友好的方式?
映射关系
在找到问题所在后,如何定位到所属业务方呢?其实是我们维护了一个 AAR artifact id 和负责人之间的映射关系,类似如下:
报告形式
json 报告可视化太差了,有无友好的方式?当然有的我们用 Kotlin/JS 写了一个 html 模版:
产物分析
以上获取所有图片资源、获取 assets 资源、获取 APK 文件等等,都是一句话带过了。其实这些都是基于产物分析,也就是在 Gradle 构建阶段,这些产物都是可以拿到的。核心的代码如下:
// ArtifactType: AAR、APK、CLASSES、MANIFEST、ANDROID_RES、ASSETS
internal fun ApplicationVariantImpl.getArtifactFiles(artifactType: AndroidArtifacts.ArtifactType): List<File> {
return variantData.variantDependencies.getArtifactCollection(
AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH,
AndroidArtifacts.ArtifactScope.ALL,
artifactType
).artifacts.map { artifact ->
artifact.file
}
}
所以,我们写的很多 Task 都是基于此的。
唯一需要注意的是,Task 之间的依赖关系。比如,系统的 mergeResTask 需要依赖我们自定义的图片压缩 Task,如果我们要列出所有 AAR 里面的权限信息,那么我们写的自定义 Task 就要依赖系统的 mergeManifestTask。