目录

资源去重

最近不知道为什么,我司又开始缩减包体积了,在这个 app 都 100-200M 的年代,在这个 5G 遥遥领先的年代,不知道卷个啥?

那问题不大,既然有需求,我们照做便是

前置思考

Android 包体积由多方面构成,这个我之前的文章里也有说过,所以包体积缩减的手段有很多,这个就不一一介绍了

我们今天主要来思考几个问题:

各个 module 引用了相同的资源会怎样

这在大型工程中实际非常常见,经常有一些 UI 想要复用,但是因为其可能是别人 module 私有的,所以有很多同学会选择 copy 一份资源到自己的 module 下使用。
这就会造成一个问题,文件是同一份文件,只是文件名不同,然后资源打包的时候,一起被打进 apk 中

资源文件如何通过资源 id 引用到的?

这就不得不说 arsc 文件了,我们可以用 android studio 打开 arsc 文件。其可视化的结构类似一个表格 /img/in-post/arsc_table.png
通过资源 id 找到其对应的 name 和 对应的真实资源文件

如何去重?

由上可知,假如我们存在两个相同内容、名称不同的资源,那么其在 arsc 表中存在两条记录,类似如下:

id 名称 资源
id1 name1 resource1
id2 name2 resource2

如果,我们让 name2 指向 resource1,arsc 修改为如下:

id 名称 资源
id1 name1 resource1
id2 name2 resource1

然后将 resource2 删除是不是就好了呢?

理论上是没问题的

那么实际又该怎么操作呢?这就需要我们了解资源打包的过程了

AndResGuard 资源去重

AndResGuard 是微信开源的资源压缩方案,他的年代比较久远,但是在如此久远的时间,微信的开发就已经在做这种事情了,实在是太卷了···

AndResGuard 有两种集成模式

一种是 gradle plugin 方式集成,这种方法因为年久失修,gradle plugin api 改变的问题,似乎已经无法再高版本 agp 上使用了(不过好像也有人在个人版本持续维护,但是其实作用不大,因为只是在处理 api 问题,其实资源打包的过程也与之前不一样了,后面我们再来说)

另一种是 jar 包的集成方式,这种方法倒是不存在 agp 的兼容问题。但是处理的逻辑在新版本都是存在一些小问题的。

AndResGuard 做了什么事情

其实是将 APK 重新解压,如果开启了 merge 功能,它会去重资源,重写 arsc 文件,然后重新打包签名 APK。

在打包的过程中,会把许多资源作为可压缩对象(ZipEntry 有自己的属性,标志其是否可压缩),所以最终的压缩包更小(因为官方有很多资源并没有压缩,AndResGuard 基本全压缩,arsc 文件也压缩,所以后续高版本有个和官方冲突的 arsc 禁止压缩的问题,会导致 apk 无法安装)

AndResGuard是怎么去重的

通过对比资源的 MD5,进而像之前说的一样,修改 arsc 文件实现

首先,修改 arsc 文件是第一个难点,因为需要了解其文件结构。不过 AndResGuard 似乎是自己实现了一套 decoder。但是,这个 decoder 有一些问题。比如,如果存在多个资源名指向同一个资源的时候,会抛出 IndexOutOfBounds 异常,类似这个 issue 我也遇到了

除此之外,当 agp 使用高版本的时候,资源去重的逻辑也是有点问题的

因为高版本 agp 自己存在一个 optimizeResourceTask,这个 task 执行以后,资源不会再像以前一样位于 res/drawable/test1.png,而是变为 res/ABC.png。总结就是:名称被混淆、路径变成一级目录

而 AndResGuard 源码中有一部分根据资源名取判断资源的 type。举个例子 ABC.png 的 type 老版本,因为其路径为 res/drawable 则会被判断为 drawable。而新版本 type 会被判断为 ABC.png。这导致了 merge 功能在高版本上其实是基本失效的

这也是为什么我自己写了一个插件的原因

其实,也可以基于 AndResGuard 修改,但是奈何其源码过于复杂,包括有一些魔法数字,感觉不理解吃透,很容易翻车。而且,在别人的代码里修改,总有一种不稳的感觉。

最终,在网络上看到了 美团的包体积缩减手段APKMonitor 插件相关的文章,虽然他们也都是适配的旧版本 AGP,但是为我提供了一个处理 ARSC 文件的好思路,就是 android-chrunk-utils

那么,已经有了可以随意修改 arsc 文件的库,我们只需要找到对应 task,然后注入自己的 action 就好了。action 里的逻辑也是重复说了很多遍的:找到重复的资源,修改 arsc 文件

这里需要知道一个额外知识点,android 的资源打包后是一个 .ap_ 的文件。这个文件其实就是一个 zip,最终我们需要对其进行解压和重新压缩

Android 官方的资源压缩

正如国内在以前狂卷包体积一样,谷歌官方也开始卷包体积了,所以 AGP 高版本自带了资源混淆功能,这个前面也说了

不过,不确定这里为什么没有直接帮忙把资源去重之类的也做了。查看了 AGP 的更新文档,说是可以开启一个实验属性,打开新的 resource shrinker,但是我使用了以后,并没有进一步的体积缩减

而且 color 资源的路径,似乎因为一些历史原因,会单独保留下来,源码中也是这么写的

/img/in-post/agp_resource_shrinker.png

自己写一个去重插件

主逻辑参考自 APKMonitor

不过,也没啥好创新的,除了一些 api 的修修改、输出文件的修改其他大概没什么问题

逻辑也很简单

首先找到 .ap_ 文件,读取该 zip 文件,找到相同的文件,记录下来

使用 chrunk-utils 库,读取 arsc 文件,将相同的文件的值指向同一份资源

重写 arsc 文件

删除相同的资源文件

重新 zip 压缩为 .ap_ 文件

几个坑点

  1. 高版本 AGP Task 不要去找 processResources,而是去找 OptimizeResources

  2. 如果工程配置了 splits abi,一次产生多个 apk,那么会存在多个 .ap_ 文件,我们需要对每个 ap 文件都处理一遍

代码如下:github

参考

  1. 美团的包体积缩减手段
  2. APKMonitor