23 | 包体积优化(下):资源优化的进阶实践
下载APP
关闭
渠道合作
推荐作者
23 | 包体积优化(下):资源优化的进阶实践
2019-02-12 张绍文 来自北京
《Android开发高手课》
课程介绍
讲述:冯永吉
时长11:34大小10.57M
上一期我们聊了 Dex 与 Native Library 的优化,是不是还有点意犹未尽的感觉呢?那安装包还有哪些可以优化的地方呢?
请看上面这张图,Assets、Resource 以及签名 metadata 都是安装包中的“资源”部分,今天我们就一起来看看如何进一步优化资源的体积。
AndResGuard 工具
在我们的安装包中,资源相关的文件具体有下面这几个,它们都是我们需要优化的目标文件。
接下来我们先来复习一下这个工具的核心实现,然后再进一步思考还有哪些地方需要继续优化。
1. 资源混淆
ProGuard 的核心优化主要有三个:Shrink、Optimize 和 Obfuscate,也就是裁剪、优化和混淆。当初我在写 AndResGuard 的时候,希望实现的就是 ProGuard 中的混淆功能。
资源混淆的思路其实非常简单,就是把资源和文件的名字混淆成短路径:
那么这样的实现究竟对哪些资源文件有优化作用呢?
resources.arsc。因为资源索引文件 resources.arsc 需要记录资源文件的名称与路径,使用混淆后的短路径 res/s/a,可以减少整个文件的大小。
ZIP 文件索引。ZIP 文件格式里面也需要记录每个文件 Entry 的路径、压缩算法、CRC、文件大小等信息。使用短路径,本身就可以减少记录文件路径的字符串大小。
资源文件有一个非常大的特点,那就是文件数量特别多。以微信 7.0 为例,安装包中就有 7000 多个资源文件。所以说,资源混淆工具仅仅通过短路径的优化,就可以达到减少 resources.arsc、签名文件以及 ZIP 文件大小的目的。
既然移动优化已经到了“深水区”,正如 Dex 和 Library 优化一样,我们需要对它们的格式以及特性有非常深入的研究,才能找到优化的思路。而我们要做的资源优化也是如此,要对 resources.arsc、签名文件以及 ZIP 格式需要有非常深入的研究与思考。
2. 极限压缩
AndResGuard 的另外一个优化就是极限压缩,它的极限压缩功能体现在两个方面:
更高的压缩率。虽然我们使用的还是 Zip 算法,但是利用了 7-Zip 的大字典优化,APK 的整体压缩率可以提升 3% 左右。
压缩更多的文件。Android 编译过程中,下面这些格式的文件会指定不压缩;在 AndResGuard 中,我们支持针对 resources.arsc、PNG、JPG 以及 GIF 等文件的强制压缩。
这里可能会有一个疑问,为什么 Android 系统会专门选择不去压缩这些文件呢?
压缩效果并不明显。这些格式的文件大部分本身已经压缩过,重新做 Zip 压缩效果并不明显。例如 PNG 和 JPG 格式,重新压缩只有 3%~5% 的收益,并不是十分明显。
读取时间与内存的考虑。如果文件是没有压缩的,系统可以利用 mmap 的方式直接读取,而不需要一次性解压并放在内存中。
Android 6.0 之后 AndroidManifest 支持不压缩 Library 文件,这样安装 APK 的时候也不需要把 Library 文件解压出来,系统可以直接 mmap 安装包中的 Library 文件。
android:extractNativeLibs=“true”
简单来说,我们在启动性能、内存和安装包体积之间又做了一个抉择。在上一期中我就讲过对于 Dex 和 Library 来说,最有效果的方法是使用 XZ 或者 7-Zip 压缩,对于资源来说也是如此,一些比较大的资源文件我们也可以考虑使用 XZ 压缩,但是在首次启动时需要解压出来。
进阶的优化方法
学习完 AndResGuard 工具的混淆和压缩功能的实现原理后,可以帮助我们加深对安装包格式以及 Android 资源编译的原理的认识。
但 AndResGuard 毕竟是几年前的产物,那现在又有哪些新的进阶优化方法呢?
1. 资源合并
在资源混淆方案中,我们发现资源文件的路径对于 resources.arsc、签名信息以及 ZIP 文件信息都会有影响。而且因为资源文件数量非常非常多,导致这部分的体积非常可观。
那我们能不能把所有的资源文件都合并成同一个大文件,这样做肯定会比资源混淆方案效果更好。
事实上,大部分的换肤方案也是采用这个思路,这个大资源文件就相当于一套皮肤。因此我们完全可以把这套方案推广开来,但是实现起来还是需要解决不少问题的。
资源的解析。我们需要模拟系统实现资源文件的解析,例如把 PNG、JPG 以及 XML 文件转换为 Bitmap 或者 Drawable,这样获取资源的方法需要改成我们自定义的方法。
那为什么我们不像 SVG 那样,直接把这些解析完的所有 Drawable 全部丢到系统的缓存中呢?这样代码就无需做太多修改?之所以没这么做主要是考虑对内存的影响,如果我们把全部的资源文件一次性全部解析,并且丢到系统的缓存中,这部分会占用非常大的内存。
资源的管理。考虑到内存和启动时间,所有的资源也是用时加载,我们只需要使用 mmap 来加载“Big resource File”。同时我们还要实现自己的资源缓存池 ResourceCache,释放不再使用的资源文件,这部分内容你可以参考类似 Glide 图片库的实现。
我在逆向 Facebook 的 App 的时候也发现,它们的资源和多语言基本走的完全是自己的流程。在“UI 优化”时我就说过,我们先在系统的框架下尝试做了很多的优化,但是渐渐发现这样的方式依然要受系统的各种制约,这时就要考虑去突破系统的限制,把所有的流程都接管过来。
当然我们也需要在性能和效率之间寻找平衡点,要看自己的应用当前更重视性能提升还是开发效率。
2. 无用资源
AndResGuard 中的资源混淆实现的是 ProGuard 的 Obfuscate,那我们是否可以同样实现资源的 Shrink,也就是裁剪功能呢?应用通过长时间的迭代,总会有一些无用的资源,尽管它们在程序运行过程不会被使用,但是依然占据着安装包的体积。
事实上,Android 官方早就考虑到这种情况了,下面我们一起来看看无用资源优化方案的演进过程。
第一阶段:Lint
然后我们直接选择“Remove All Unused Resources”,就可以轻松删除所有的无用资源了。既然它是第一阶段的方案,那 Lint 方案扫描具体的缺点是什么呢?
Lint 作为一个静态扫描工具,它最大的问题在于没有考虑到 ProGuard 的代码裁剪。在 ProGuard 过程我们会 shrink 掉大量的无用代码,但是 Lint 工具并不能检查出这些无用代码所引用的无用资源。
第二阶段:shrinkResources
所以 Android 在第二阶段增加了“shrinkResources”资源压缩功能,它需要配合 ProGurad 的“minifyEnabled”功能同时使用。
如果 ProGuard 把部分无用代码移除,这些代码所引用的资源也会被标记为无用资源,然后通过资源压缩功能将它们移除。
是不是看起来很完美,但是目前的 shrinkResources 实现起来还有几个缺陷。
没有处理 resources.arsc 文件。这样导致大量无用的 String、ID、Attr、Dimen 等资源并没有被删除。
没有真正删除资源文件。对于 Drawable、Layout 这些无用资源,shrinkResources 也没有真正把它们删掉,而是仅仅替换为一个空文件。为什么不能删除呢?主要还是因为 resources.arsc 里面还有这些文件的路径,具体你可以查看这个issues。
所以尽管我们的应用有大量的无用资源,但是系统目前的做法并没有真正减少文件数量。这样 resources.arsc、签名信息以及 ZIP 文件信息这几个“大头”依然没有任何改善。
那为什么 Studio 不把这些资源真正删掉呢?事实上 Android 也知道有这个问题,在它的核心实现ResourceUsageAnalyzer中的注释也写得非常清楚,并尝试解决这个问题提供了两种思路。
如果想解答系统为什么不能直接把这些资源删除,我们需要先回过头来重温一下 Android 的编译流程。
由于 Java 代码需要用到资源的 R.java 文件,所以我们就需要把 R.java 提前准备好。
在编译 Java 代码过程,已经根据 R.java 文件,直接将代码中资源的引用替换为常量,例如将 R.String.sample 替换为 0x7f0c0003。
.ap_ 资源文件的同步编译,例如 resources.arsc、XML 文件的处理等。
如果我们在这个过程强行把无用资源文件删除,resources.arsc 和 R.java 文件的资源 ID 都会改变(因为默认都是连续的),这个时候代码中已经替换过的 0x7f0c0003 就会出现资源错乱或者找不到的情况。
因此系统为了避免发生这种情况,采用了折中的方法,并没有二次处理 resources.arsc 文件,只是仅仅把无用的 Drawable 和 Layout 文件替换为空文件。
第三阶段:realShrinkResources
那怎么样才能真正实现无用资源的删除功能呢?ResourceUsageAnalyzer 的注释中就提供了一个思路,我们可以利用 resources.arsc 中 Public ID 的机制,实现非连续的资源 ID。
简单来说,就是 keep 住保留资源的 ID,保证已经编译完的代码可以正常找到对应的资源。
但是重写 resources.arsc 的方法会比资源混淆更加复杂,我们既要从这个文件中抹去所有的无用资源相关信息,还要 keep 住所有保留资源的 ID,相当于把整个文件都重写了。
正因为异常复杂,所以目前 Android 还没有提供这套方案的完整实现。我最近也正在按照这个思路来实现这套方案,希望完成后可以尽快开源出来。
总结
今天我们回顾了 AndResGuard 工具的实现原理,也学习了两种资源优化的进阶方式。特别是无用资源的优化,你可以看到尽管是无所不能的 Google,也并没有把方案做到最好,依然存在一些妥协的地方。
其实这种不完美的地方还有很多很多,也正是有了这些不完美的地方,才会出现各种各样优秀的开源方案。也因此我们才会不断思考如何突破系统的限制,去实现更多、更底层的优化。
课后作业
对于 Android 的编译流程,你还有不理解的地方吗?对于安装包中的资源,你还有哪些好的优化方案?欢迎留言跟我和其他同学一起讨论。
不知道你有没有想过,其实“第三阶段”的无用资源删除方案也并不是终极解决方案,因为它并没有考虑到无用的 Assets 资源。
但是对于 Assets 资源,代码中会有各种各样的引用方式,如果想准确地识别出无用的 Assets 并不是那么容易。当初在 Matrix 中,我们尝试提供了一套简单的实现,你可以参考UnusedAssetsTask。
希望你在课后也可以进一步思考,我们可以如何识别出无用的 Assets 资源,在这个过程中会遇到哪些问题?
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。
分享给需要的人,Ta购买本课程,你将得18元
生成海报并分享
赞 6
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
22 | 包体积优化(上):如何减少安装包大小?
下一篇
24 | 想成为Android高手,你需要先搞定这三个问题
精选留言(13)
- 星辰大海2019-04-30哈哈,shwen哥,重写arsc文件我们在matrix里其实已经实现了,借你的文章广告下。
作者回复: 牛逼
共 4 条评论23 - Zain Zhu2020-05-24硬着头皮在看,一直写业务代码都废掉了啊6
- Danny _Jiang2019-06-12Matrix中的UnusedAssetTask也有一定的缺陷,因为只是调用DexFileFactory.loadDexFile加载dex文件,所以只会去搜索java文件中的引用。 如果在assets目录下有一个.json文件,此.json文件中记录assets文件夹中的其它图片路径,然后在java代码中通过AssetManager读取这个json文件之后,循环遍历出它所引用的图片,对于这种方式Matrix是检测不到的,会将它置为unused. 想问一下作者,对于这种情况有什么好的建议优化matrix吗展开
作者回复: 对于xml是有做处理的,json这个属于特殊情况,只能放到白名单里面了
共 2 条评论2 - 功夫熊猫2019-02-27极限压缩这部分说的更高效率的压缩是指压缩的什么,最后APK的压缩算法?如果是那么系统安装如何解压?还是说优化了现有的zip算法?
作者回复: 依然是zip算法,只是优化了字典
1 - 你我的宿命2020-05-12了解到resources.arsc真正用途,图片资源压缩的本质问题在哪里,assets中的无用资源记录到下一步的优化计划中
- Swing2020-04-12“所有的资源也是用时加载,我们只需要使用 mmap 来加载“Big resource File”。同时我们还要实现自己的资源缓存池 ResourceCache,释放不再使用的资源文件,这部分内容你可以参考类似 Glide 图片库的实现。” 使用mmap,意味着 将整个的 大文件 缓存到了 pagecache里?那系统内存占用还是不可避免啊。 而且 本身可能我目前只是用 三五个文件,你一下把所有文件都加载进来了,这不是增加系统负担吗??? 求解惑展开共 1 条评论
- 古月弓虽19932020-04-05请问文中使用的hex编辑器是什么编辑器呢共 1 条评论1
- Tony2019-12-09绍文大师,我看完了你写的这篇文章,但是有个疑问,直接删除无用资源文件的话,java代码或者kotlin代码中资源引用例如0x73c8745为什么会错乱或找不到呢,虽然资源ID是顺序分配的,但是并不会影响java代码中的引用把?对这个不是很理解啊
作者回复: 删除的无用代码都是没有Java引用的,你看起来有Java引用,可能是因为那些Java代码被proguard了
共 4 条评论 - joker°2019-05-08请问 shrinkResources 输入的无用文件相关代码在哪里看呢?实测无用 drawable 文件会压缩为一个内容仅为“<x/>”文件,但是无用 layout 文件并无任何改动
作者回复: 指的是源码吗?源码可以看build-system里面
- Carlo2019-03-25有没有一个好的,通用的linter或者真正的resourceshirk 工具?
作者回复: 现在专栏完结了,真正的无用资源删除等我有空实现一下
- 奚岩2019-02-23Lint 只能静态分析 ,但是有些动态生成的就没法识别,比如我了 kotlin-android-extensions, 有些 id 使用了,但是 Lint 却不知道,有没有什么工具可以 分析编译后 代码? 有个插件 statistics 可以统计代码。
作者回复: shrinkresource看的就是java字节码
- 张晴天天天天天☁️☁...2019-02-20老师您好,我从网上了解到shrinkResources=true时可能会导致一些应用崩溃的问题,那还要去使用它吗?
作者回复: 是因为没有排除某些会使用的文件,需要加白名单
- Egos2019-02-15所有资源文件合成一个大文件,是一个zip?
作者回复: 不是,是一个自定义的二进制文件