BoosterDemo
滴滴 Booster 内置的资源压缩 task booster-task-compression 实现了如下功能:
1.删除冗余资源,保留尺寸最大的图片
2.有损压缩图片资源,内置两种压缩方案:
1.pngquant 有损压缩(需要自行安装 pngquant 命令行工具)
2.cwebp 有损压缩(已内置)
3.修改资源索引文件 resources.arsc、webp 图片等的 zipEntry method,置为 DEFLATED,减少压缩包大小
运行 demo 的时候发现删除冗余资源的时候,console 提示删除图片失败,如下图:
win7 系统 booster_version = '0.8.0' ,该问题已提issue#19
Booster 对资源索引文件 resources.arsc 的压缩,只是单一设置 ZipEntry.method,这是否运行时性能有影响,有大佬 这样讨论过: resources.arsc 压缩会影响性能吗? 、 Google I/O 2016 笔记:APK 瘦身的正确姿势,尚未定论。
针对 resources.arsc 的优化,美团还提出如下手段:
1.开启资源混淆
2.对重复的资源优化
3.无用资源优化
对重复的资源优化和对被 shrinkResources 优化掉的资源进行处理的原理见:美团博客 Android App 包瘦身优化实践
这里根据美团讲述的原理在 Booster 定制 task 实现对重复的资源优化和对无用资源优化,详见工程module TaskCompression。
一、对重复的资源优化
重复资源的筛选条件为 资源的 zipEntry.crc 相等,最先出现的资源压缩包产物 ap_ 文件是在 processResTask 中,尽可能早的删除重复资源, 可以减少后续 task 的执行时间,hook 在 processResTask 之后,如下:
variant.processResTask?.doLast{
variant.removeRepeatResources(it.logger,results)
}
这里我按照同 zipEntry.crc 和同资源目录(不同资源目录可能有相同的 crc 资源,造成误删,不过可能性较小)去分类收集重复资源:
private fun File.findDuplicatedResources():Map<Key,ArrayList<DuplicatedOrUnusedEntry>>{
var duplicatedResources = HashMap<Key,ArrayList<DuplicatedOrUnusedEntry>>(100)
ZipFile(this).use { zip ->
zip.entries().asSequence().forEach { entry ->
val lastIndex : Int = entry.name.lastIndexOf('/')
val key = Key(entry.crc.toString(),if(lastIndex == -1) "/" else entry.name.substring(0,lastIndex))
if(!duplicatedResources.containsKey(key)){
val list : ArrayList<DuplicatedOrUnusedEntry> = ArrayList(20)
duplicatedResources[key] = list
}
val list = duplicatedResources[key]
list?.add(DuplicatedOrUnusedEntry(entry.name,entry.size,entry.compressedSize,DuplicatedOrUnusedEntryType.duplicated))
}
}
duplicatedResources.filter {
it.value.size >= 2
}.apply{
duplicatedResources = this as HashMap<Key, ArrayList<DuplicatedOrUnusedEntry>>
}
return duplicatedResources
}
重复的资源优化的实现整体思路:
1.从 ap文件中解压出 resources.arsc 条目,并收集该条目的 ZipEntry.method,为后续按照同 ZipEntry.method 把改动后的 resources.arsc 添加到 ap文件中
2.收集重复资源
3.根据收集的重复资源,保留重复资源的第一个,从删除 ap_ 文件中删除其他重复资源的 zipEntry
4.使用通过[android-chunk-utils]修改 resources.arsc 全局 StringChunk,把这些重复的资源都重定向到没有被删除的第一个资源
5.按照同 ZipEntry.method 把改动后的 resources.arsc 添加到 ap_ 文件中
源码见:doRemoveRepeatResources 方法
验证: 分别在 App/lib module 显示三张图片,重复资源如下:
查看没集成重复的资源优化的 apk,如图:
使用工具查看集成重复的资源优化的 apk,如图:
集成重复的资源优化打包,控制和输出报告都可以看到如下输出:
可以知道删除哪些重复资源,压缩包减少了多少 kb。
二、无用资源优化
通过 shrinkResources true 来开启资源压缩,资源压缩工具会把无用的资源替换成预定义的版本而不是移除, 那么 google 出于什么原因这样做了? ResourceUsageAnalyzer 注释是这样说的的:
/**
* Whether we should create small/empty dummy files instead of actually
* removing file resources. This is to work around crashes on some devices
* where the device is traversing resources. See http://b.android.com/79325 for more.
*/
注释上说了适配解决某些设备 crash 问题,查看issue,发现发生 crash 的设备基本上都是三星手机,如果删除无用资源,需要考虑该 issue 问题。
如果采用人工移除的方式会带来后期的维护成本,在 Android 构建工具执行 package${flavorName}Task 之前通过修改 Compiled Resources 来实现自动去除无用资源。
具体流程如下: * 收集资源包(Compiled Resources 的简称)中被替换的预定义版本的资源名称,通过查看资源包 (Zip 格式)中每个 ZipEntry 的 CRC-32 checksum 来寻找被替换的预定义资源,预定义资源的 CRC-32 定义在 ResourceUsageAnalyzer, 下面是它们的定义:
// A 1x1 pixel PNG of type BufferedImage.TYPE_BYTE_GRAY
public static final long TINY_PNG_CRC = 0x88b2a3b0L;
// A 3x3 pixel PNG of type BufferedImage.TYPE_INT_ARGB with 9-patch markers
public static final long TINY_9PNG_CRC = 0x1148f987L;
// The XML document <x/> as binary-packed with AAPT
public static final long TINY_XML_CRC = 0xd7e65643L;
// The XML document <x/> as a proto packed with AAPT2
public static final long TINY_PROTO_XML_CRC = 3204905971L;
从定义中没有看到 webp、jpg、jpeg 相关的 crc,那么这些没有定义 crc-32 的资源在 ZipEntry 中 crc 为多少了,用预定义资源替换未使用的地方的实现如下:
private void replaceWithDummyEntry(JarOutputStream zos, ZipEntry entry, String name)throws IOException {
// Create a new entry so that the compressed len is recomputed.
byte[] bytes;
long crc;
if (name.endsWith(DOT_9PNG)) {
bytes = TINY_9PNG;
crc = TINY_9PNG_CRC;
} else if (name.endsWith(DOT_PNG)) {
bytes = TINY_PNG;
crc = TINY_PNG_CRC;
} else if (name.endsWith(DOT_XML)) {
switch (format) {
case BINARY:
bytes = TINY_BINARY_XML;
crc = TINY_BINARY_XML_CRC;
break;
case PROTO:
bytes = TINY_PROTO_XML;
crc = TINY_PROTO_XML_CRC;
break;
default:
throw new IllegalStateException("");
}
} else {
//没有预定资源格式,crc =0,数据为空
bytes = new byte[0];
crc = 0L;
}
JarEntry outEntry = new JarEntry(name);
if (entry.getTime() != -1L) {
outEntry.setTime(entry.getTime());
}
if (entry.getMethod() == JarEntry.STORED) {
outEntry.setMethod(JarEntry.STORED);
outEntry.setSize(bytes.length);
outEntry.setCrc(crc);
}
zos.putNextEntry(outEntry);
zos.write(bytes);
zos.closeEntry();
...
}
可以得出筛选无使用资源的条件为 crc in 如下集合中:
val unusedResourceCrcs = longArrayOf(
ResourceUsageAnalyzer.TINY_PNG_CRC,
ResourceUsageAnalyzer.TINY_9PNG_CRC,
ResourceUsageAnalyzer.TINY_BINARY_XML_CRC,
ResourceUsageAnalyzer.TINY_PROTO_XML_CRC,
0 //jpg、jpeg、webp 等
)
如果集成了
打印 packageAndroidTask 的 inputFiles,如下:
分别查看箭头目录下的文件,有*.ap_ 文件,
而从上面两图中可以了解到 shrinkResources 影响到 packageAndroidTask 的 inputFiles,没有开启 shrinkResources, packageAndroidTask 从 processedResTask 产物中读取 ap文件,开启 shrinkResources,从 res_stripped 目录下读取 ap文件, 根据其 stripped 名,也猜测出 ap_ 文件中已经进行了预定资源替换未使用资源了,可以压缩软件查看未使用资源的 zipEntry.crc 进行验证,如下图:
可以看到没有使用的 webp、jpg 资源的 ZipEntry.crc 为 0;如果集成了 Booster 内置的 booster-task-compression, 会把 png 格式转换成 webp 格式,没使用的 png 最后的 crc 会变为 0.
删除无用资源方案想到两种:
方案一:删除所有无用资源文件,以及删除资源索引文件 resources.arsc 中 global StringChunk 有关无用资源的数据项。
缺点:删除了 global StringChunk 中的数据项,改变了后续数据项的索引值,好比删除 List 中的元素,后续的元素索引值减一一样,牵一发动全身,需要同步其他 chunk 索引到 global StringChunk 数据项的索引值。否则会出现资源显示混乱,甚至 crash;同时需要考虑上述 issue 问题。对 resources.arsc 越大出现问题的概率越大
方案二:无用资源根据 crc 分类,再按照重复资源优化,没有删除 global StringChunk 数据项,没有改变数据项的索引值,不需要改动其他 chunk,同时不会出现上述 issue 问题。
下面对方案二具体实现,方案一就不做讨论了。
无用资源优化的代码实现整体思路:
1.从 ap文件中解压出 resources.arsc 条目,并收集该条目的 ZipEntry.method,为后续按照同 ZipEntry.method 把改动后的 resources.arsc 添加到 ap文件中
2.收集无用资源
3.把收集的无用资源根据 crc 进行分类,在按照重复资源优化处理
源码见:doRemoveUnusedResources 方法
集成无用资源优化打包,控制和输出报告都可以看到如下输出:
可以知道删除哪些无用资源,压缩包减少了多少 kb,无用资源优化减少的 size 没多少。
以上重复资源优化和无用资源优化,没有经过大量设备测试,仅供参考学习。
推荐阅读:
滴滴 Booster 移动 App 质量优化框架-学习之旅 一
关注公众号: