一、包体积优化的必要性
安装包大小与下载转化率的关系大致是成反比的,即安装包越大,下载转换率就越差。Google 曾在2019的谷歌大会上给出过一个统计结论,包体积体大小每上升6MB,应用下载转化率就会下降1%,在不同地区的表现可能会有所差异。
APK 减少10MB,在不同国家转化率增长
(注:数据来自于 googleplaydev:Shrinking APKs, growing installs)
二、游戏中心 APK 组成
APK 包含以下目录:
- META-INF/:包含 CERT.SF 、CERT.RSA 签名文件、MANIFEST.MF 清单文件。
- assets/:包含应用的资源。
- res/:包含未编译到 resources.arsc 中的资源。
- lib/:支持对应 CPU 架构的 so 文件。
- resources.arsc:资源索引文件。
- classes.dex:可以理解的dex文件就是项目代码编译为 class 文件后的集合。
- AndroidManifest.xml:包含核心 Android 清单文件。此文件列出了应用的名称、版本、访问权限和引用的库文件。
发现占包体积比较大的主要是 lib、res、assets、resources 这几个部分,优化主要也从这几个方面入手。
三、包体积检测工具
Matrix-ApkChecker 作为 Matrix 系统的一部分,是针对Android 安装包的分析检测工具,根据一系列设定好的规则检测 APK 是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。
配置游戏中心的 Json,主要检测 APK 是否经过了资源混淆、不含 Alpha 通道的 PNG 文件、未经压缩的文件类型、冗余的文件、无用资源等信息。
对于生成的检测文件进行分析,可以优化不少体积。
工具 Matrix Apkcheck 介绍:
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker
四、包体积优化措施
4.1 不含 Alpha 通道的 PNG 大图
项目中存在较多这种类型的图,可以替换为 JPG 或者 WebP 图,能减少不少体积。
4.2 代码做减法
随着业务的迭代,很多业务场景是不会再使用了,涉及到相关的资源和类文件都可以删除掉,相应的 APK 中 res 和 dex 都会相应减少。游戏中心这次去掉了些经过迭代后没有使用的业务场景和资源。
4.3 资源文件最少化配置
针对内销的项目,本地的 string.xml 或者 SDK 中的 string.xml 文件中的多语言,是根本用不到的。这部分资源可以优化掉,能减少不少体积。
在APP的 build.gradle 中下添加 resConfigs "zh-rCN", "zh-rTW", "zh-rHK"。这样配置不影响英文、中文、中国台湾繁体、中国香港繁体语言的展示。
资源文件最少化配置前
资源文件最少化配置后
4.4 配置资源优化
很多项目为了适配各种尺寸的分辨率,同一份资源可能在不同的分辨率的目录下放置了各种文件,然后现在主流的机型都是 xxh 分辨率,游戏游戏中心针对了内置的 APK,配置了优先使用"xxhdpi", "night-xxhdpi"。
这么配置如果 xxhdpi、night-xxhdpi 存在资源文件,就会优先使用该分辨率目录下文件,如果不存在则会取原来分辨率目录下子资源,能避免出现资源找不到的情形。
defaultConfig {
resConfigs isNotBaselineApk ? "" : ["xxhdpi", "night-xxhdpi"]
}
左右滑动查看完整代码
4.5 内置包去除v1签名
同样对于内置包来说,肯定都是 Android 7 及以上的机型了,可以考虑去掉v1签名。
signingConfigs {
gameConfig {
if (isNotBaselineApk) {
print("v1SigningEnabled true")
v1SigningEnabled true
} else {
print("v1SigningEnabled false")
v1SigningEnabled false
}
v2SigningEnabled true
}
}
去掉v1签名后,上图的三个文件在 APK 中会消失,也能较少 600k 左右的体积。
4.6 动效资源文件优化
发现项目中用了不少的 GIF、Lottie 文件、SVG 文件,占用了很大一部分体积。考虑将这部分替换成更小的动画文件,目前游戏中心接入了 PAG 方案。替换了部分 GIF 图和 Lottie 文件。
PAG 文件采用可扩展的二进制文件格式,可单文件集成图片音频等资源,导出相同的 AE 动效内容,在文件解码速度和压缩率上均大幅领先于同类型方案,大约为 Lottie 的0.5倍,SVG 的0.2倍。
实际上可能由于设计导出的 Lottie 或者 GIF 不规范,在导出 PAG 文件时会提醒优化点,实际部分资源的压缩比率达到了80~90%,部分动效资源从几百K降到了几十K。
具体可以参考 PAG 官网:
https://github.com/Tencent/libpag/blob/main/README.zh_CN.md
游戏中心这边将比较大的 GIF 图,较多的 Lottie 图做过 PAG 替换。
举例:
(1)游戏中心的榜单排行页上的头图,UI那边导出的符合效果的 GIF 大小为701K,替换为 PAG 格式后同样效果的图大小为67K,只有原来的1/10不到。
(2)游戏中心的入口空间 Lottie 动效优化。
一份 Lottie 动效大概是这样的,一堆资源问题加上 Json 文件。像上述动效的整体资源为112K,同样的动效格式转换为 PAG 格式后,资源大小变成6K,只有原大小的5%左右。之后新的动效会优先考虑使用 PAG。
4.7 编译期间优化图片
以游戏中心 App 为例,图片资源约占用了25%的包体积,因此对图片压缩是能立杆见效的方式。
WebP 格式相比传统的 PNG 、JPG 等图片压缩率更高,并且同时支持有损、无损、和透明度。
思路就是在是在 mergeRes 和 processRes 任务之间插入 WebP 压缩任务,利用 Cwebp 对图片在编译期间压缩。
(注:图片来源于https://booster.johnsonlee.io/zh/guide/shrinking/png-compression.html#pngquant-provider)
已有的解决方法:
(1)可以采用滴滴的方案 booster,booster-task-compression-cwebp 。
参考链接:https://github.com/didi/booster
(2)公司内部官网模块也有类似基于 booster 的插件,基于 booster 提供的 API 实现的图片压缩插件。压缩过后需要对所有页面进行一次点检,防止图片失真,针对失真的图片,可以采用白名单的机制。
4.8 动态化加载so
同样以游戏中心为例,so的占比达到了45.1%,可以对使用场景较少和较大的so进行动态化加载的策略,在需要使用的场景下载到本地,动态去加载。
使用的场景去服务端下载到本地加载的流程可以由以下流程图表示。
流程可以归纳为下载、解压、加载,主要问题就是解决so加载问题。
载入so库的传统做法是使用:
System.loadLibrary(library);
经常会出现 UnsatisfiedLinkError,Relinker 库能大幅减小报错的概率:
ReLinker.loadLibrary(context, "mylibrary")
具体可以参考:
https://github.com/KeepSafe/ReLinker
按需加载的情形,风险与收益是并存的,有很多情况需要考虑到,比如下载触发场景、网络环境、加载失败是否有降级策略等等,也需要做好给用户的提示交互。
4.9 内置包只放64位so
目前新上市的手机 CPU 架构都是arm64-v8a, 对应着 ARMV8 架构,所以在打包的时候针对内置项目,只打包64位so进去。
ndk {
if ("64" == localMultilib)
abiFilters "arm64-v8a"
else if ("32" == localMultilib)
abiFilters "armeabi"
else
abiFilters "armeabi", "arm64-v8a"
}
//其中localMultilib为配置项变量
String localMultilib = getLocalMultilib()
String getLocalMultilib() {
def propertyKey = "LOCAL_MULTILIB"
def propertyValue = rootProject.hasProperty(propertyKey) ? rootProject.getProperty(propertyKey) : "both"
println " --> ${project.name}: $propertyKey[$propertyValue], $propertyKey[${propertyValue.class}]"
return propertyValue
}
左右滑动查看完整代码
4.10 开启代码混淆、移除无用资源、ProGuard 混淆代码
android {
buildTypes {
release {
minifyEnabled true
shrinkResources true
}
}
}
shrinkResources 和 minifyEnabled 必须同时开启才有效。
特别注意:这里需要强调一点的是开启之后无用的资源或者图片并没有真正的移除掉,而是用了一个同名的占位符号。
可以通过 ProGuard 来实现的,ProGuard 会检测和移除代码中未使用的类、字段、方法和属性,除此外还可以优化字节码,移除未使用的代码指令,以及用短名称混淆类、字段和方法。
proguard-android.txt 是 Android 提供的默认混淆配置文件,在配置的 Android sdk /tools/proguard 目录下,proguard-rules.pro是我们自定义的混淆配置文件,我们可以将我们自定义的混淆规则放在里面。
android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'
}
}
}
左右滑动查看完整代码
4.11 R文件内联优化
如果我们的 App 架构如下:
编译打包时每个模块生成的 R 文件如下:
R_lib1 = R_lib1;
R_lib2 = R_lib2;
R_lib3 = R_lib3;
R_biz1 = R_lib1 + R_lib2 + R_lib3 + R_biz1(biz1本身的R)
R_biz2 = R_lib2 + R_lib3 + R_biz2(biz2本身的R)
R_app = R_lib1 + R_lib2 + R_lib3 + R_biz1 + R_biz2 + R_app(app本身R)
左右滑动查看完整代码
可以看出各个模块的R文件都会包含下层组件的R文件内容,下层的模块生成的id除了自己会生成一个R文件外,同时也会在全局的R文件生成一个,R文件的数量同样会膨胀上升。多模块情况下,会导致 APK 中的 R 文件将急剧的膨胀,对包体积的影响很大。
由于App模块目前的R文件中的资源ID全部是 final 的, Java 编译器在编译时会将 final 常量进行 inline 内联操作,将变量替换为常量值,这样项目中就不存在对于 App 模块R文件的引用了,这样在代码缩减阶段,App 模块R文件就会被移除,从而达到包体积优化的目的。
基于以上原理,如果我们将 library 模块中的资源 ID 也转化为常量的话,那么 library 模块的R文件也可以移除了,这样就可以有效地减少我们的包体积。
现在有不少开源的R文件内联方法,比如滴滴开源的 booster 与字节开源的 bytex 都包含了R文件内联的插件。
booster 参考:
bytex 参考:
https://github.com/bytedance/ByteX/blob/master/access-inline-plugin/README-zh.md
五、优化效果
5.1 优化效果
上述优化措施均在游戏中心实际中采用,以游戏中心某个相同的版本为例子,前后体积对比如下图所示:
(1)包体积优化的比例达到了31%,包体积下降了20M左右,从长久来说对应用的转换率可以提升3%的点左右。
(2)启动速度相对于未优化版本提升2.2%个点。
5.2 总结
(1)读者想进行体积优化之前,需先分析下 APK 的各个模块占比,主要针对占比高的部分进行优化,比如:游戏中心中 lib、res、assets、resources 占比较高,就针对性的进行了优化;
(2)动效方案的切换、so动态加载、编译期间图片优化等措施是长久的,相比于未进行优化,时间越长可能减少的体积越明显;
(3)资源文件最小化配置、配置资源优化,简单且效果显著;
(4)后续会对 dex 进行进一步探索,目前项目中代码基本上都在做加法,越来越复杂,很少有做减法,导致 dex 逐渐增大,目前还在探索怎么进一步缩小 dex 体积。