You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

214 lines
14 KiB
Markdown

2 years ago
# 23 | 包体积优化(下):资源优化的进阶实践
上一期我们聊了Dex与Native Library的优化是不是还有点意犹未尽的感觉呢那安装包还有哪些可以优化的地方呢
![](https://static001.geekbang.org/resource/image/30/46/30d73f5021ac8b4333db3e49a31c8a46.png)
请看上面这张图Assets、Resource以及签名metadata都是安装包中的“资源”部分今天我们就一起来看看如何进一步优化资源的体积。
## AndResGuard工具
在美团的一篇文章[《Android App包瘦身优化实践》](https://tech.meituan.com/2017/04/07/android-shrink-overall-solution.html)中也讲到了很多资源优化相关的方法例如WebP和SVG、R文件、无用资源、资源混淆以及语言压缩等。
在我们的安装包中,资源相关的文件具体有下面这几个,它们都是我们需要优化的目标文件。
![](https://static001.geekbang.org/resource/image/dd/b7/dd5c7efb6074ff0f2bd18296f9ecf1b7.png)
想使用好[AndResGuard](https://github.com/shwenzhang/AndResGuard)工具需要对安装包格式以及Android资源编译的原理有很深地理解它主要有两个功能一个是资源混淆一个是资源的极限压缩。
接下来我们先来复习一下这个工具的核心实现,然后再进一步思考还有哪些地方需要继续优化。
**1\. 资源混淆**
ProGuard的核心优化主要有三个Shrink、Optimize和Obfuscate也就是裁剪、优化和混淆。当初我在写AndResGuard的时候希望实现的就是ProGuard中的混淆功能。
资源混淆的思路其实非常简单,就是把资源和文件的名字混淆成短路径:
```
Proguard -> Resource Proguard
R.string.name -> R.string.a
res/drawable/icon -> res/s/a
```
那么这样的实现究竟对哪些资源文件有优化作用呢?
* **resources.arsc**。因为资源索引文件resources.arsc需要记录资源文件的名称与路径使用混淆后的短路径res/s/a可以减少整个文件的大小。
* **metadata签名文件**。[签名文件MF与SF](https://cloud.tencent.com/developer/article/1354380)都需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小。
![](https://static001.geekbang.org/resource/image/25/5c/25792171ce386fff9d5c0b73d382ce5c.png)
* **ZIP文件索引**。ZIP文件格式里面也需要记录每个文件Entry的路径、压缩算法、CRC、文件大小等信息。使用短路径本身就可以减少记录文件路径的字符串大小。
![](https://static001.geekbang.org/resource/image/54/a8/54760d2eab5e7199572e02ed70377fa8.png)
资源文件有一个非常大的特点那就是文件数量特别多。以微信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等文件的强制压缩。
```
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
```
这里可能会有一个疑问为什么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文件信息都会有影响。而且因为资源文件数量非常非常多导致这部分的体积非常可观。
那我们能不能把所有的资源文件都合并成同一个大文件,这样做肯定会比资源混淆方案效果更好。
![](https://static001.geekbang.org/resource/image/e6/0d/e6a587ffa43b7dfb7887ace3d973a30d.png)
事实上,大部分的换肤方案也是采用这个思路,这个大资源文件就相当于一套皮肤。因此我们完全可以把这套方案推广开来,但是实现起来还是需要解决不少问题的。
* **资源的解析**。我们需要模拟系统实现资源文件的解析例如把PNG、JPG以及XML文件转换为Bitmap或者Drawable这样获取资源的方法需要改成我们自定义的方法。
```
// 系统默认的方式
Drawable drawable = getResouces().getDrawable(R.drawable.loading);
// 新的获取方式
Drawable drawable = CustomResManager.getDrawable(R.drawable.loading);
```
那为什么我们不像SVG那样直接把这些解析完的所有Drawable全部丢到系统的缓存中呢这样代码就无需做太多修改之所以没这么做主要是考虑对内存的影响如果我们把全部的资源文件一次性全部解析并且丢到系统的缓存中这部分会占用非常大的内存。
* **资源的管理**。考虑到内存和启动时间所有的资源也是用时加载我们只需要使用mmap来加载“Big resource File”。同时我们还要实现自己的资源缓存池ResourceCache释放不再使用的资源文件这部分内容你可以参考类似Glide图片库的实现。
我在逆向Facebook的App的时候也发现它们的资源和多语言基本走的完全是自己的流程。在“UI优化”时我就说过我们先在系统的框架下尝试做了很多的优化但是渐渐发现这样的方式依然要受系统的各种制约这时就要考虑去突破系统的限制把所有的流程都接管过来。
当然我们也需要在性能和效率之间寻找平衡点,要看自己的应用当前更重视性能提升还是开发效率。
**2\. 无用资源**
AndResGuard中的资源混淆实现的是ProGuard的Obfuscate那我们是否可以同样实现资源的Shrink也就是裁剪功能呢应用通过长时间的迭代总会有一些无用的资源尽管它们在程序运行过程不会被使用但是依然占据着安装包的体积。
事实上Android官方早就考虑到这种情况了下面我们一起来看看无用资源优化方案的演进过程。
**第一阶段Lint**
从Eclipse时代开始我们就开始使用[Lint](https://cloud.tencent.com/developer/article/1014614)这个静态代码扫描工具它里面就支持Unused Resources扫描。
![](https://static001.geekbang.org/resource/image/f0/80/f09d7215a06d330bb19d72869df80580.png)
然后我们直接选择“Remove All Unused Resources”就可以轻松删除所有的无用资源了。既然它是第一阶段的方案那Lint方案扫描具体的缺点是什么呢
Lint作为一个静态扫描工具它最大的问题在于没有考虑到ProGuard的代码裁剪。在ProGuard过程我们会shrink掉大量的无用代码但是Lint工具并不能检查出这些无用代码所引用的无用资源。
**第二阶段shrinkResources**
所以Android在第二阶段增加了“shrinkResources”资源压缩功能它需要配合ProGurad的“minifyEnabled”功能同时使用。
如果ProGuard把部分无用代码移除这些代码所引用的资源也会被标记为无用资源然后通过资源压缩功能将它们移除。
```
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
```
是不是看起来很完美但是目前的shrinkResources实现起来还有几个缺陷。
* **没有处理resources.arsc文件**。这样导致大量无用的String、ID、Attr、Dimen等资源并没有被删除。
![](https://static001.geekbang.org/resource/image/15/97/1587d1a3bd95ad0f318bfd5731c0bc97.png)
* **没有真正删除资源文件**。对于Drawable、Layout这些无用资源shrinkResources也没有真正把它们删掉而是仅仅替换为一个空文件。为什么不能删除呢主要还是因为resources.arsc里面还有这些文件的路径具体你可以查看这个[issues](https://issuetracker.google.com/issues/37010152)。
所以尽管我们的应用有大量的无用资源但是系统目前的做法并没有真正减少文件数量。这样resources.arsc、签名信息以及ZIP文件信息这几个“大头”依然没有任何改善。
那为什么Studio不把这些资源真正删掉呢事实上Android也知道有这个问题在它的核心实现[ResourceUsageAnalyzer](https://android.googlesource.com/platform/tools/base/+/studio-master-dev/build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/ResourceUsageAnalyzer.java)中的注释也写得非常清楚,并尝试解决这个问题提供了两种思路。
![](https://static001.geekbang.org/resource/image/85/a6/854c91e20d7724bd61b0f5376cebf5a6.png)
如果想解答系统为什么不能直接把这些资源删除我们需要先回过头来重温一下Android的编译流程。
![](https://static001.geekbang.org/resource/image/89/cf/8929932b9db83b06444c54435948d2cf.png)
* 由于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保证已经编译完的代码可以正常找到对应的资源。
![](https://static001.geekbang.org/resource/image/c6/14/c670fcb26d8acffec355ec7ba539fb14.png)
但是重写resources.arsc的方法会比资源混淆更加复杂我们既要从这个文件中抹去所有的无用资源相关信息还要keep住所有保留资源的ID相当于把整个文件都重写了。
正因为异常复杂所以目前Android还没有提供这套方案的完整实现。我最近也正在按照这个思路来实现这套方案希望完成后可以尽快开源出来。
## 总结
今天我们回顾了AndResGuard工具的实现原理也学习了两种资源优化的进阶方式。特别是无用资源的优化你可以看到尽管是无所不能的Google也并没有把方案做到最好依然存在一些妥协的地方。
其实这种不完美的地方还有很多很多,也正是有了这些不完美的地方,才会出现各种各样优秀的开源方案。也因此我们才会不断思考如何突破系统的限制,去实现更多、更底层的优化。
## 课后作业
对于Android的编译流程你还有不理解的地方吗对于安装包中的资源你还有哪些好的优化方案欢迎留言跟我和其他同学一起讨论。
不知道你有没有想过其实“第三阶段”的无用资源删除方案也并不是终极解决方案因为它并没有考虑到无用的Assets资源。
但是对于Assets资源代码中会有各种各样的引用方式如果想准确地识别出无用的Assets并不是那么容易。当初在Matrix中我们尝试提供了一套简单的实现你可以参考[UnusedAssetsTask](https://github.com/Tencent/matrix/blob/master/matrix/matrix-android/matrix-apk-canary/src/main/java/com/tencent/matrix/apk/model/task/UnusedAssetsTask.java)。
希望你在课后也可以进一步思考我们可以如何识别出无用的Assets资源在这个过程中会遇到哪些问题
欢迎你点击“请朋友读”,把今天的内容分享给好友,邀请他一起学习。最后别忘了在评论区提交今天的作业,我也为认真完成作业的同学准备了丰厚的“学习加油礼包”,期待与你一起切磋进步哦。