作者:邹阿涛涛涛涛涛涛涛涛
链接:
https://juejin.cn/post/7302982924987039796
本文由作者授权发布。
这篇文章会分享小厂如何做包体积优化相关主题,涉及内容包括:1) Android包体积优化的一种可能是比较标准的推进做法,2) 大致流程的心路历程和思考方式,3) 如何去总结和分享你们进行过的包体积优化项目。本文不仅仅是一篇分享,还是我个人比较喜欢的总结报告写法。
移动 App 特别关注投放转化率指标,而 App 包体积是影响用户新增的重要因素,而 App 的包体积又是影响投放转化率的重要因素。Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%, 当包体积增大到 100MB 时就会有断崖式的下跌 。对于应用商店,普遍有一个流量自动下载的最大阈值,如 应用宝,下载的app超过100M,用流量下载时,会弹窗提示用户是否继续下载,这对下载转化率影响是比较大的。现在流量虽然变得更廉价一点,但是用户的心理是不会变的,当 App 出现在应用市场的相同位置时,包体积越大,用户下载意愿可能越低。而且包体积或直接或间接地影响着下载转化率、安装时间、运行内存、磁盘空间等重要指标,所以投入精力扫除积弊、发掘更深层次的体积优化项是十分必要的。- Google 2016 年公布的研究报告显示,包体积每上升 6MB 就会带来下载转化率降低 1%,当包体积增大到 100MB 时就会有断崖式的下跌。
- 通过插件化,将常规优化后达 120M+的包体积降到了 13M 左右,最小版本降至 4M,包体积缩小至原先的 3.33%。
- 包体积大小,是俞xx直接拍的,就要求 x年x月x日 前削减到100M。
- 包大小做得比较“霸权”“独裁”,新业务超过 1M 要总裁审批,一般在平台组都卡掉了。
那具体应该如何进行判断呢?有个比较简单的方法就是对标,找到对标对象进行对标。
- 如果我们App跟竞品App包体积差不多或略高,那就有必要进行包体积优化。
- 优化到跟业内人气App中包体积最小的那一档差不多即可。
上述判断还是基于用户视角,假如你的 用户需求 比较简单,好几个app都可以满足, 你会安装200M的产品,还是50M的产品。再有,假如用户在App商店无意间看到你们的App,有点兴趣体验体验,但是看到包体积是200M,他有多大概率会继续下载,换成50M呢?
我们在做包体积优化前,一定要定好我们的大方向,我们怎么优化,能做哪些,哪些可以现在做,哪些没必要现在做。1. 抓各个环节
我们最终是要优化App的包体积,那么App包组成部分有哪些,我们针对每一个部分去研究如何减少其体积,就可以达到我们最终的效果。2. 系统化方案先行,再来实操
优化Android包体积这个事情,有一定的探索性,但是很多内容或者说手段都是业内在广为流传的,既然如此,我们应该总结业内可优化手段,并逐一进行分析,研究是否适合你们App,是否需要应用到你们App。如果没有方案的埋头扎进去,往往会因为陷入细节,而丢失了全局视野。3. 明确风险收益比及成本收益比
方案与方案之间是有区别的。如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗? 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?4. 明确指标以及形成一套监控防劣化体系
干任何一件以优化为目标的事情时,一定要明确优化的指标,我们要做App的包体积优化,那么我们的指标为:减少App安装包 apk 的大小。当我们指标明确之后,我们还需要对现有指标进行监控,这样有两个好处:那我们就可以在某个关键的时间节点进行包体积的统计和上报,一般时间节点有:- 监控每次打包的包体积,可以行成指标曲线进行持续观察。
5. 把包体积这个指标刻在脑子里
自动化能发现已经发生的问题,但是把包体积这个指标刻在脑子里,能避免问题的发生。
1. Android Apk结构
- Dex:.class 文件处理后的产物,Android 系统的可执行文件。
- Resource:资源文件,主要包括 layout、drawable、animator,通过 R.XXX.id 引用。
- Assets:资源文件,通过 AssetManager 进行加载。
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支持分架构打包: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的单个体积。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%。针对这种情况,选择动态下发。我们采用ASM的方案,对代码中所有的 System.load、System.loadLibrary 进行hook,进入到我们自己的逻辑,这样我们就可以走下面流程:
这里需要注意的一点就是,当动态下发的so没有下载、解压、校验、加载完之前,如果用户进入到了相关的业务场景,必须有兜底机制。比如在样例App的场景中,使用了 opencv 库来做图片的二维码识别,当so没下载下来时,要识别二维码就会被兜底到 zxing。而且由于我们有较好的Hook框架的封装,所以我们需要hook时,仅仅需要进行配置即可:这里可以参考我之前的博客和github上demo项目:基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)
https://juejin.cn/post/7288178532861755432
AndroidSimpleHook-github
https://github.com/scuzoutao/AndroidSimpleHook
5. 大文件压缩优化,对内置的未压缩大文件进行,压缩文件用高压缩率的压缩算法
假如apk里有内置的大文件,可以通过对其进行压缩从而减少包体积,压缩时可以选用高压缩率的算法。6. 代码优化
每减少一个ENUM可以减少大约1.0到1.4 KB的大小,假如有10000个枚举对象,那不就减少了14M?美滋滋啊,但实际上具体还是要看项目代码情况来考虑,毕竟不是所有的项目里都有 10000 个枚举。7. 资源优化
去除无用资源文件可以通过lint工具来做,也可以通过微信开源的 ApkChecker来完成。https://github.com/Tencent/matrix图片压缩、转webp
图片压缩可以使用TinyPng,AndroidStudio也有相关插件,官方术语就是:使用智能的无损压缩技术来减少图片文件的大小,通过智能的选择颜色的数量,减少存储的字节,但是效果基本是和压缩前一样的。图片着色器
相同图片只是颜色不同的话,完全可以只放一个图片,在内存里操作 Drawable,完成颜色替换。图片动态下发
如果本地有大图,且使用要求为未压缩,或者压缩之后仍然很大,可以适当的选择动态下载该图。resources.arsc资源混淆
resources.arsc这个文件是存放在APK包中的,他是由AAPT工具在打包过程中生成的,他本身是一个资源的索引表,里面维护着资源ID、Name、Path或者Value的对应关系,AssetManager通过这个索引表,就可以通过资源的ID找到这个资源对应的文件或者数据。通过对apk 中的resources.arsc进行内容修改,来对apk进行深度压缩。这里可以采用微信的AndResGuard方案。https://github.com/shwenzhang/AndResGuard8. 三方库优化
移除无用三方库
移除无用三方库需要人肉扫描 build.gradle 文件,一个一个的去检查依赖的三方库是否被我们代码所使用。功能重复的三方库整合
特别常见的case,RN 用的图片加载库是 Fresco,客户端用的图片加载库是 Glide,他们都是用来加载图片,可以通过删除一个库,让项目依赖的库少一个。一个三方库往往不会被用到全部功能,比如曾经很火的 XUtils。
https://github.com/wyouflf/xUtils3
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 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
-keepattributes SourceFile, LineNumberTable
这样就会保留 Dex 中的 debug 与行号信息。根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右。10. ReDex
ReDex 是 Facebook 开发的一个 Android 字节码的优化工具。它提供了 .dex 文件的读写和分析框架,并提供一组优化策略来提升字节码。官方提供预期优化效果:对dex文件优化为 8%https://github.com/facebook/redex11. R 文件瘦身
当 Android 应用程序被编译,会自动生成一个 R 类,其中包含了所有 res/ 目录下资源的 ID。包括布局文件layout,资源文件,图片(values下所有文件)等。在写java代码需要用这些资源的时候,你可以使用 R 类,通过子类+资源名或者直接使用资源 ID 来访问资源。R.java文件是活动的Java文件,如MainActivity.java的和资源如strings.xml之间的胶水https://github.com/bytedance/ByteX/blob/master/shrink-r-plugin/README-zh.md12. 可能的更多方案
除了我上面列到的一些,市面上还有一些其他的方案,有复杂的有不复杂的,有收益高的有收益低的,大家可以在掘金上搜索Android包体积优化,就能搜到大部分了,当然,在大厂里,还会有很多很极致的方案,比如:思想是这么个思想,大家在实操的时候,思路就是先调研方案,调研完成之后再选型。
- 如果一个方案能减少包体积2M,但是线上可能会崩溃,你会做吗?
- 如果一个方案能减少包体积2M,但是开发成本要一个月,你会做吗?
这里我们在示例App的基础上,对每个手段进行仔细分析,包括:
就这样,当我们制定完成我们的目标方案之后,就可以放手干了。
https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md3. - 去除 DebugItem 包含的 debug信息与行号信息: 链接https://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.mdhttps://github.com/shwenzhang/AndResGuard/blob/master/README.zh-cn.md
当我们进行了一系列的或大或小的改动之后,如何描述最终优化效果?给两张对比图不就行了,无图言X。
大家在进行一些有挑战性或者是比较有意义的项目时,其实可以多进行总结,总结的好处有什么我就不多解释了,懂的都懂哈。做的好的方面:
也可以告诉自己及读者几句话:
- 这是一个系统的需要持续去投入人力的事情,万万不可有了一定结果之后放松警惕。
- 做事不能太讲究所谓的方法论,不然会掉入陷阱,但是确实要讲究方法论。
有些事情你做好了,可能仅仅是因为做这个事情的人是你,如果是别人来做,也能将这件事情做好。
一般来说,进行总结之后,都得来一些展望,给未来的自己挖点坑,给总结的读者画点饼。比如我们这里就可以这样继续装模作样的展望一下:上面已经反复提及了,当前这一期的优化工作,重点考量的指标是风险收益比及成本收益比,所以一些极致的或者成本收益比较高的优化手段并没有被采用,所以后续还是有很多事情可以深入的干下去。- 去除 DebugItem 包含的 debug信息与行号信息
...
这里我就发散性的随便总结下吧。。。也不深入纠结了。1. 包体积优化是个庞大的工程项目,不仅仅需要优化,还需要防劣化,优化过程中还会涉及到业务冲突,说白了就是某些东西从APK包中移除了,或多或少会有些影响,还需要去跟业务方达成意见一致。2. 大家不管在做什么优化课题时,最好是分步骤分工期的去进行,不要一口吃成胖子,如果上来就追求完全极致的优化效果,往往会带来两个负面风险:1) 优化工期拉长,时间成本成倍增加,2)可能影响线上App或者线下各种工具的运行稳定性。3. 系统化的调研、成本 + 风险 + 收益的总和考虑非常重要,任何优化项目开始进行或者进行过程中,都需要牢牢的印在脑子里,每日三省你身。4. 遇到困难不要畏惧,各种优化项目往往会遇到很多阻力,比如方案实现太难、业务沟通太难等等,一块石头硬磕磕不动的时候换个方向磕,换方向也磕不动那就换块石头磕,比如假设业务方沟通不动,那就换个角度,把你和业务方放在同一角色上,给业务方找收益或者。5. 做的项目是啥或者说研究的方向是啥其实不是最重要的,我们这种普通程序员更重要的是解决问题的能力,因为你们做的事情,换个人用同样的时间成本或者更多的时间成本,往往也能做好,所以是你做好的这件事情其实没那么重要,更重要的是遇到其他问题或者有其他的疑难杂症和系统性问题时,知道你一定能做好。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!