这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法。
一、前言
移动 App 特别关注投放转化率指标,而 App 包体积是影响用户新增的重要因素,而 App 的包体积又是影响投放转化率的重要因素。
Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%, 当包体积增大到 100MB 时就会有断崖式的下跌 。对于应用商店,普遍有一个流量自动下载的最大阈值,如 应用宝,下载的app超过100M,用流量下载时,会弹窗提示用户是否继续下载,这对下载转化率影响是比较大的。
现在流量虽然变得更廉价一点,但是用户的心理是不会变的,当 App 出现在应用市场的相同位置时,包体积越大,用户下载意愿可能越低。
而且包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分必要的。
某手:
- 1M = 一年kw级推广费
某条极速版:
- Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。
- 通过插件化,将常规优化后达 120M+的包体积降到了 13M 左右,最小版本降至 4M,包体积缩小至原先的 3.33%。
某德:
- 包体积大小,是俞xx直接拍的,就要求 x年x月x日 前削减到100M。
某淘:
- 包大小做得比较"霸权""独裁",新业务超过 1M 要总裁审批,一般在平台组都卡掉了。
二、评估优化需求
在开展工作前,我们首先得先有两点判断:
- 是否需要进行优化
- 优化到多少算符合预期
那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。
对于小厂来说,一般对标对象有:
- 竞品App
- 业内人气App
基于对标对象,我们可以粗略的有如下判断:
- 如果我们App跟竞品App包体积差不多或略高,那就有必要进行包体积优化。
- 优化到跟业内人气App中包体积最小的那一档差不多即可。
上述判断还是基于用户视角,假如你的 用户需求 比较简单,好几个app都可以满足, 你会安装200M的产品,还是50M的产品。再有,假如用户在App商店无意间看到你们的App,有点兴趣体验体验,但是看到包体积是200M,他有多大概率会继续下载,换成50M呢?
三、包体积优化基本思想
我们在做包体积优化前,一定要定好我们的大方向,我们怎么优化,能做哪些,哪些可以现在做,哪些没必要现在做,
1. 抓各个环节
我们最终是要优化App的包体积,那么App包组成部分有哪些,我们针对每一个部分去研究如何减少其体积,就可以达到我们最终的效果。
2. 系统化方案先行,再来实操
优化Android包体积这个事情,有一定的探索性,但是很多内容或者说手段都是业内在广为流传的,既然如此,我们应该总结业内可优化手段,并逐一进行分析,研究是否适合你们App,是否需要应用到你们App。如果没有方案的埋头扎进去,往往会因为陷入细节,而丢失了全局视野。
3. 明确风险收益比及成本收益比
方案与方案之间是有区别的。如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗? 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?
4. 明确指标以及形成一套监控防劣化体系
干任何一件以优化为目标的事情时,一定要明确优化的指标,我们要做App的包体积优化,那么我们的指标为:减少App安装包 apk 的大小。
当我们指标明确之后,我们还需要对现有指标进行监控,这样有两个好处:
- 明确优化收益
- 防止劣化
那我们就可以在某个关键的时间节点进行包体积的统计和上报,一般时间节点有:
- App发版打包时(粒度更粗)
- 开发分支有新的 commit 合入时(粒度更细)
两种粒度各有各的好处,但是目标是一样的:
- 监控每次打包的包体积,可以行成指标曲线进行持续观察
- 在包体积增加超出预期时进行及时报警
5. 把包体积这个指标刻在脑子里
自动化能发现已经发生的问题,但是把包体积这个指标刻在脑子里,能避免问题的发生。
四、自家App包体积一览
1. Android Apk结构
APK 主要由五个部分组成,分别是:
- Dex:.class 文件处理后的产物,Android 系统的可执行文件
- Resource:资源文件,主要包括 layout、drawable、animator,通过 R.XXX.id 引用
- Assets:资源文件,通过 AssetManager 进行加载
- Library:so 库存放目录
- META-INF:APK 签名有关的信息
2. 你们Apk各个部分都长啥样,长多大?
这里选取示例App某个版本的安装包来做举例分析,下面是包结构图:
浅浅分析一波包内内容
成分 | 体积 | 备注 |
---|---|---|
assets文件夹 | 77.8M | 能看到Assets文件夹里,有着75M的RN bundle |
lib | 75.2M | 由于我们App是兼容包,即同时兼容64位、32位,所以lib目录很大 |
dex | 16M | 这部分涉及到我们自己的代码及三方库代码 |
res | 6.3M | 这里包含各种图片资源、布局文件等 |
resources.arsc | 1.2M | |
其他 | 若干 |
五、优化方案草案
通过调研业内常规、非常规手段,以及结合你们App的包体积现状,可以提前对优化包体积做出比较详尽的可实现、低风险、高收益方案,注意这几个点非常重要:
- 可实现 - 可实现可以简单理解为实现成本低,一般各种性能稳定性指标都是循序渐进的推进,所以往往一期优化时,选的实操方案都是实现成本比较低的,这样能相对快速的得到比较符合心里预期的优化效果。
- 低风险 - 线上风险必须控制在可接受的程度,这里的风险不仅仅是说App运行的性能稳定性风险,还需要判断是否会增加线上问题排查的难度,当然还会有其他的我没提到的风险项。
- 高收益 - 不解释
所以基于我们需要的是可实现、低风险、高收益的方案,我们可以基于上面我贴的APK案例,来大致预演可能会采用哪些方案:
1. 缩减动态化方案内置代码包、资源包
一般的小厂都会比较大量的采用如RN、H5等动态化方案,不可避免的App内就会有一堆内置文件,我们看到我们示例的App中,RN内置包占了整个包体积超过 30%。当出现这种情况时,可以针对内置代码包、资源包单独立子项去推进。
那么如何进行推进呢?有同学就会说了,业务方不让我把这些玩意儿从APK里面删掉,说影响他们打开页面速度,影响页面打开速度就会影响一级指标影响收入。
这时为了说服业务方,我们就得拿出一些证据,用来证明内置包的全部移除或者部分移除并不会对业务产生影响,或者说影响足够小。那就可以采取如下一些推进步骤:
- 明确全部内置包或者部分内置包不内置的影响,假如内置包是 RN 的页面bundle,那给业务方两个数据基本上就能够说明影响
-
- 页面bundle现下比例,假如因为本地没有内置的bundle,打开页面需要同步进行等待下载完成才能加载的话,现场下载比例是个比较有说服力的数据。
- 线上bundle更新耗时,我们可以统计用户启动App后的一段指定时间,90分位能下载多少个bundle,50分位能下载多少个,10分位、5分位能下载多少个,来告诉业务方,老用户、新用户、老用户新登录等各种场景,到达业务页面的时候,有多少比例的用户能完成bundle更新。
- 明确什么样的资源需要内置,同样用RN页面bundle举例,假如App的首页就是RN页面,那这玩意儿就必须内置了,假如一个页面在犄角旮旯,日pv才不到100,那就完全可以不需要内置。
- 给出内置资源名单
- 拿着内置名单和上面明确的不内置影响统计,找业务方拉会, 这一步最好是从上往下进行推进,而不是同级推进
2. 分架构打包
分架构打包能减少libs文件夹体积,libs文件夹里会包含不同架构的 so 库集合。
首先我们最终apk包是要上传到应用商店的,应用商店得支持双包上传。答案确实是支持,且应用商店推荐双包上传。
Android 官方也是有相关的api支持分架构打包:
php
splits {
// 基于不同的abi架构配置不同的apk
abi {
// 必须为true,打包才会为不同的abi生成不同的apk
enable true
// 默认情况下,包含了所有的ABI。
// 所以使用reset()清空所有的ABI,再使用include指定我们想要生成的架构armeabi-v7a、arm-v8a
reset()
// 逗号分隔列表的形式指定 Gradle 应针对哪些 ABI 生成 APK。只与 reset() 结合使用,以指定确切的 ABI 列表。
include "armeabi-v7a", "arm64-v8a"
// 是否生成通用的apk,也就是包含所有ABI的apk。如果设为 true,那么除了按 ABI 生成的 APK 之外,Gradle 还会生成一个通用 APK。
universalApk true
}
}
这里需要注意的是,线上并不是所有的手机都支持 64位 的安装包,应用商店可以双包上传,线上灰度更新可以下发32位的安装包或者是 32/64 兼容包。
3. So 压缩
分架构打包是减少so的数量,so压缩是减少so的单个体积。
ini
android:extractNativeLibs="true"
android:extractNativeLibs = true时,gradle打包时会对工程中的so库进行压缩,最终生成apk包的体积会减小。
但用户在手机端进行apk安装时,系统会对压缩后的so库进行解压,从而造成用户安装apk的时间变长。
若开发人员未对android:extractNativeLibs进行特殊配置,android:extractNativeLibs默认值:
-
minSdkVersion < 23 或 Android Gradle plugin < 3.6.0情况下,打包时 android:extractNativeLibs=true
-
minSdkVersion >= 23 并且 Android Gradle plugin >= 3.6.0情况下,打包时android:extractNativeLibs=false
4. 大so动态下发
我们能看到有些so库单个体积超大,放在apk里,就算能压缩,压缩后体积仍然很大,可能会占到 app体积超过 10%。针对这种情况,选择动态下发。
动态下发的so如何进行加载
我们采用ASM的方案,对代码中所有的 System.load、System.loadLibrary 进行hook,进入到我们自己的逻辑,这样我们就可以走下面流程:
- 下载so库
- 解压so库
- 校验so库
- 加载so库
这里需要注意的一点就是,当动态下发的so没有下载、解压、校验、加载完之前,如果用户进入到了相关的业务场景,必须有兜底机制。比如在样例App的场景中,使用了 opencv 库来做图片的二维码识别,当so没下载下来时,要识别二维码就会被兜底到 zxing。
而且由于我们有较好的Hook框架的封装,所以我们需要hook时,仅仅需要进行配置即可:
这里可以参考我之前的博客和github上demo项目:
基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)
5. 大文件压缩优化,对内置的未压缩大文件进行,压缩文件用高压缩率的压缩算法
假如apk里有内置的大文件,可以通过对其进行压缩从而减少包体积,压缩时可以选用高压缩率的算法。
6. 代码优化
- 去除无用代码、资源
去除无用代码我们可以用官方的Lint检查工具
- 去除无用三方库
- 减少ENUM的使用
每减少一个ENUM可以减少大约1.0到1.4 KB的大小,假如有10000个枚举对象,那不就减少了14M?美滋滋啊,但实际上具体还是要看项目代码情况来考虑,毕竟不是所有的项目里都有 10000 个枚举。
7. 资源优化
- 无用资源文件清理
去除无用资源文件可以通过lint工具来做,也可以通过微信开源的 ApkChecker来完成。
图片压缩、转webp
图片压缩可以使用TinyPng,AndroidStudio也有相关插件,官方术语就是:
使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。
图片着色器
相同图片只是颜色不同的话,完全可以只放一个图片,在内存里操作 Drawable,完成颜色替换。
图片动态下发
如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。
resources.arsc资源混淆
resources.arsc这个文件是存放在APK包中的,他是由AAPT工具在打包过程中生成的,他本身是一个资源的索引表,里面维护者资源ID、Name、Path或者Value的对应关系,AssetManager通过这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。
通过对apk 中的resources.arsc进行内容修改,来对apk进行深度压缩。这里可以采用微信的AndResGuard方案。
8. 三方库优化
移除无用三方库
移除无用三方库需要人肉扫描 build.gradle 文件,一个一个的去检查依赖的三方库是否被我们代码所使用。
功能重复的三方库整合
特别常见的case,RN 用的图片加载库是 Fresco,客户端用的图片加载库是 Glide,他们都是用来加载图片,可以通过删除一个库,让项目依赖的库少一个。
- 修改三方库源码,不需要的代码进行剔除
一个三方库往往不会被用到全部功能,比如曾经很火的 XUtils github.com/wyouflf/xUt...
XUtils是一个工具大杂烩,但是假如我只用它来加载图片,其他工具是不是就完全无用,可以进行剔除。
9. 去除 DebugItem 包含的 debug信息与行号信息
在讲解什么是 deubg 信息与行号信息之前,我们需要先了解 Dex 的一些知识。
我们都知道,JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。
所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。
为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
diff
-keepattributes SourceFile, LineNumberTable
这样就会保留 Dex 中的 debug 与行号信息。根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右
10. ReDex
ReDex 是 Facebook 开发的一个 Android 字节码的优化工具。它提供了 .dex 文件的读写和分析框架,并提供一组优化策略来提升字节码。官方提供预期优化效果:对dex文件优化为 8%
11. R 文件瘦身
当 Android 应用程序被编译,会自动生成一个 R 类,其中包含了所有 res/ 目录下资源的 ID。包括布局文件layout,资源文件,图片(values下所有文件)等。在写java代码需要用这些资源的时候,你可以使用 R 类,通过子类+资源名或者直接使用资源 ID 来访问资源。R.java文件是活动的Java文件,如MainActivity.java的和资源如strings.xml之间的胶水
通过R文件常量内联,达到R文件瘦身的效果。
12. 可能的更多方案
除了我上面列到的一些,市面上还有一些其他的方案,有复杂的有不复杂的,有收益高的有收益低的,大家可以在掘金上搜索Android包体积优化,就能搜到大部分了,当然,在大厂里,还会有很多很极致的方案,比如:
- 去掉kotlin生成的许多模板代码、判空代码
- 去除布局文件里不需要的冗余内容
- ...
思想是这么个思想,大家在实操的时候,思路就是先调研方案,调研完成之后再选型。
六、基于风险收益比及成本收益比敲定最终实现方案
这一步的重点是:明确风险收益比及成本收益比
方案与方案之间是有区别的。
- 如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗?
- 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?
这里我们在示例App的基础上,对每个手段进行仔细分析,包括:
-
预期效果
-
成本
-
风险
就这样,当我们制定完成我们的目标方案之后,就可以放手干了。
手段 | 预期效果 | 成本 | 是否要做 | 进度 | 备注 |
---|---|---|---|---|---|
重点优化项 | |||||
- 缩减RN 内置bundle | 预期效果:177.43M -> 114.43M | 中 | ✅ | RN 内置bundle缩减,xxxx版本带上 | |
- 分架构打包,64位、32位分开打包 | 预期效果:32位:117.43M -> 71.9M 64位:117.43M -> 87.6M | 低 | ✅ | xxxx | |
- so压缩方案 | 预期效果:32位:71.9M -> 55.5M 64位:87.6M -> 58.3M | 低 | ✅ | xxxx | |
- 大so文件动态下发 | 预期效果:32位:55.5M -> 50.7M 64位:58.3M -> 51.7M | 中 | ✅ | xxxx | |
大文件优化 | |||||
- zip优化,对内置的压缩文件替换压缩算法 | 预期针对 assets 文件 | ❌ | 针对不同类型文件选取不同高压缩率算法 | ||
代码优化 (dex文件为 15.6M) | |||||
- 去除无用代码 | Android Lint | ✅ | xxx | ||
- 减少ENUM的使用 | 全部代码 enum类 一共60个,就算全删了也只是减少 84k | ✅ | xxx | 每减少一个ENUM可以减少大约1.0到1.4 KB的大小 | |
资源优化 (目前res目录大小为 6.3M,emoji目录大小为 770k) | |||||
- 无用资源文件清理 | Android Lint | ✅ | xxx | 用ApkChecker再跑一次 | |
- 图片压缩、转webp | TinyPng | ✅ | xxx | ||
- 图片着色器 | ✅ | xxx | |||
- 图片动态下发 | 主要是针对比较大的图,实际上经过TinyPng 压缩后,图片大小已经大大减小 | ✅ | xxx | ||
- resources.arsc资源混淆 | AndResGuard两年不维护,花了一小时没完全跑起来,但看到了大致优化效果,1.3M -> 920k | ❌ | github.com/shwenzhang/... | ||
三方库优化 (dex文件为 15.6M) | |||||
- 移除无用三方库 | ✅ | 检查一下 | |||
- 移除无用三方so库 | ✅ | ||||
- 功能重复三方库整合 | ✅ | ||||
- 修改三方库源码,不需要的代码进行剔除 | ✅ | ||||
极致优化,附 ByteX 插件情况 | |||||
- 去除 DebugItem 包含的 debug信息与行号信息 | ❌ | mp.weixin.qq.com/s/_gnT2kjqp... | |||
- ReDex | 对dex文件优化为 8%,即在当前dex总和 15.6M的基础上,可以减少 1.2M | ❌ | Dex 压缩,首次启动解压dexhttps://github.com/facebook/redexhttps://juejin.cn/post/7104228637594877965 | ||
- R 文件瘦身 | ❌ | 现成方案:github.com/bytedance/B... failed for task ':app:transformClassesWithShrinkRFileForQaRelease'.> java.lang.RuntimeException: This feature requires ASM7 |
七、确定优化效果
当我们进行了一系列的或大或小的改动之后,如何描述最终优化效果?给两张对比图不就行了,无图言X。
八、总结
大家在进行一些有挑战性或者是比较有意义的项目时,其实可以多进行总结,总结的好处有什么我就不多解释了,懂的都懂哈。
比如我们这里可以装模作样的这样总结一下:
做的好的方面
- 足够系统化
- 前置调研足够充分
- 风险、收益、成本考虑足够充分
- 各方面沟通足够充分
- 优化决心足够大
也可以告诉自己及读者几句话
-
这是一个系统的需要持续去投入人力的事情,万万不可有了一定结果之后放松警惕
-
别人能做的,我们也能做,只要有足够的决心去做
-
做事不能太讲究所谓的方法论,不然会掉入陷阱,但是确实要讲究方法论
-
有些事情你做好了,可能仅仅是因为做这个事情的人是你,如果是别人来做,也能将这件事情做好
九、展望
一般来说,进行总结之后,都得来一些展望,给未来的自己挖点坑,给总结的读者画点饼。比如我们这里就可以这样继续装模作样的展望一下:
上面已经反复提及了,当前这一期的优化工作,重点考量的指标是风险收益比及成本收益比,所以一些极致的或者成本收益比较高的优化手段并没有被采用,所以后续还是有很多事情可以深入的干下去。
-
resources.arsc资源混淆
-
去除 DebugItem 包含的 debug信息与行号信息
-
ReDex
-
R 文件瘦身
-
So unwind 优化
-
kotlin相关优化
-
...
十、真正的总结
这里我就发散性的随便总结下吧。。。也不深入纠结了。
- 包体积优化是个庞大的工程项目,不仅仅需要优化,还需要防劣化,优化过程中还会涉及到业务冲突,说白了就是某些东西从APK包中移除了,或多或少会有些影响,还需要去跟业务方达成意见一致。
- 大家不管在做什么优化课题时,最好是分步骤分工期的去进行,不要一口吃成胖子,如果上来就追求完全极致的优化效果,往往会带来两个负面风险:1) 优化工期拉长,时间成本成倍增加,2)可能影响线上App或者线下各种工具的运行稳定性。
- 系统化的调研、成本 + 风险 + 收益的总和考虑非常重要,任何优化项目开始进行或者进行过程中,都需要牢牢的印在脑子里,每日三省你身。
- 遇到困难不要畏惧,各种优化项目往往会遇到很多阻力,比如方案实现太难、业务沟通太难等等,一块石头硬磕磕不动的时候换个方向磕,换方向也磕不动那就换块石头磕,比如假设业务方沟通不动,那就换个角度,把你和业务方放在同一角色上,给业务方找收益或者。
- 做的项目是啥或者说研究的方向是啥其实不是最重要的,我们这种普通程序员更重要的是解决问题的能力,因为你们做的事情,换个人用同样的时间成本或者更多的时间成本,往往也能做好,所以是你做好的这件事情其实没那么重要,更重要的是遇到其他问题或者有其他的疑难杂症和系统性问题时,知道你一定能做好。