作者:三雒
链接:
https://juejin.cn/post/7207423263344083004
本文由作者授权发布。
我们在做Android包体积优化时候会将Apk拖入AS中分析,很自然发现Apk是由Dex、So、资源文件(resource.arsc,xml,asests等)三大部分组成,针对每一部分都可以进行相应的深入优化。但是我们往往会忽略Apk文件本身也是可以优化的,有点身入其中,不识庐山真面目的意思。
APK文件本身是一个ZIP文件,理解ZIP格式,从ZIP文件入手优化APK也是包体积优化不可忽略的一部分。
ZIP文件作为一个压缩文件的归档格式,在大家在日常工作和学习中广泛使用,可谓是计算机文件传输家族的顶梁柱,对于它的深入了解我认为是非常必要的,接下来我们来看一下ZIP文件的格式组成。按照 ZIP标准 中,一个ZIP文件的整体格式如下,主要由三大部分组成数据区、中央目录记录区、中央目录记录尾部区。
https://pkware.cachefly.net/webdocs/APPNOTE/APPNOTE-6.2.0.txt
----- 数据区
[local file header 1]
[file data 1]
[data descriptor 1]
.
.
.
[local file header n]
[file data n]
[data descriptor n]
----- 中央目录记录区
[archive decryption header] (EFS)
[archive extra data record] (EFS)
[central directory]
----- 中央目录记录尾部区
[zip64 end of central directory record]
[zip64 end of central directory locator]
[end of central directory record]
如下使用010 editor解析后的一个ZIP文件,该文件中只包含3个webp文件,基本上也可以看出是按照上述三大部分进行解析的。
数据区
我们在日常使用都中都会往ZIP文件中放入多文件,直观地感觉是所有的文件被作为一个整体被压缩的,但其实每个文件的数据都是单独压缩的。这也不奇怪我们在使用编程语言API读取文件时候是可以随机读取出任何一个Entry的,如果是所有文件整体压缩的话,是很难高效单独读取的,因为压缩算法的原理一般解压数据是需要先解压前面的,才能解压出后面的,这意味着只想解压一个存储靠后的文件效率时候非常低的,需要几乎把所有文件全部解压。铺垫了这么多,来看看一个文件压缩后在ZIP中存储的相关信息对应的结构,主要由如下三个子部分:[local file header]
[file data]
[data descriptor]
local file header
2个字节,记录解压缩文件所需的最低支持的ZIP规范版本,apk解压版本默认是20, 即Deflate压缩方式。当前最低功能版本定义如下:(压缩包记录的解压版本都是需要版本*10,比如:2.0 * 10 = 20)1.0 - 默认值
1.1 - 文件是卷标
2.0 - 文件是一个文件夹(目录)
2.0 - 使用 Deflate 压缩来压缩文件
2.0 - 使用传统的 PKWARE 加密对文件进行加密
2.1 - 使用 Deflate64™ 压缩文件
2.5 - 使用 PKWARE DCL Implode 压缩文件
2.7 - 文件是补丁数据集
4.5 - 文件使用 ZIP64 格式扩展
4.6 - 使用 BZIP2 压缩文件压缩
5.0 - 文件使用 DES 加密
5.0 - 文件使用 3DES 加密
5.0 - 使用原始 RC2 加密对文件进行加密
5.0 - 使用 RC4 加密对文件进行加密
5.1 - 文件使用 AES 加密进行加密
5.1 - 使用更正的 RC2 加密对文件进行加密
5.2 - 使用更正的 RC2-64 加密对文件进行加密
6.1 - 使用非 OAEP 密钥包装对文件进行加密
6.2 - 中央目录加密
记录当前文件的压缩方式,有如下12种,其中0表示原文件存放不压缩,8表示使用Deflate算法压缩。JDK 7的Zip实现只支持0和8两种,其他的均不支持。对于Andorid Apk而言,大部分文件都是使用Defalte压缩,也有一些情况下为了提升文件的运行时加载速度是选择不压缩的,比如resource.arsce, so文件等, 在不压缩的情况下可以直接mmap,加快IO的速度。0 - The file is stored (no compression)
1 - The file is Shrunk
2 - The file is Reduced with compression factor 1
3 - The file is Reduced with compression factor 2
4 - The file is Reduced with compression factor 3
5 - The file is Reduced with compression factor 4
6 - The file is Imploded
7 - Reserved for Tokenizing compression algorithm
8 - The file is Deflated
9 - Enhanced Deflating using Deflate64™
10 - PKWARE Data Compression Library Imploding
11 - Reserved by PKWARE
12 - File is compressed using BZIP2 algorithm
file data
file data紧跟在local file header之后,存储文件的具体数据,根据其压缩方式不同可能是源文件本身数据也可能是压缩后的数据。data descriptor
只有当 local file header的 general purpose bit flag 字段第3位bit置1时,data descriptor才会存在。它是字节对齐的,紧跟在文件数据的最后一个字节之后。当且仅当无法在ZIP 文件中查找时才使用此描述符,例如:当输出的ZIP文件是标准输出或不可查找设备时使用文件描述。说人话就是,看看就好,正常情况下都不需要使用。
中央目录记录区是由一系列的file header所组成,一个file header对应数据区中的一个压缩文件。[file header 1]
.
.
.
[file header n]
[digital signature]
中央目录记录尾部主要作用是用来定位中央目录记录区的开始位置,同时记录压缩包的注释内容。
上述我们已经对ZIP文件有了基本了解,知道三大部分中中央目录记录尾部只有固定数量的字节,是很小的,因此从ZIP视角看主要是针对中央目录记录区、数据区进行优化。
中央目录记录区优化
中央目录区是由一系列的file header组成的,占用的空间大致受file heaer大小和数量两个因素影响。资源混淆
从减少单个file header大小的角度出发,分析其中包含的的信息格式,字段大小基本上是固定的,有些无从下手,经过一通猜测有两个地方我们可能是可以优化的,因为它们的内容长度可变,一个是file name, 另一个是file comment。但对于Android Apk文件而言一般打包过程中并不会写入file comment. 那么file name到底能不能优化呢?Apk中的很多file name我们是可以自定义的,比如res目录下的文件res/xxhdpi/test.webp,我们完全可以叫做res/xxhdpi/a.wep,这样就比原来的字符更加短,所占用的空间也更加少。那我们是不是可以将其缩短为r/a.webp或者更极端缩短为a.webp,答案是可以的,这也就是我们耳熟能详资源混淆。由于我们代码或者编译过后的xml中基本上都是使用资源id来进行资源加载的,而资源id和资源文件路径的对应关系时候存储在resoure.arsc文件中的,这样就给了我们可乘之机,我们通过修改文件路径,并且同时修改resource.arsc文件,即可保证运行时资源加载的正确性。这个优化除了优化ZIP中央目录记录区之外,也同时能优化resource.arsc文件大小。shrikResources
从减少file header的数量角度出发,主要就是尽可能删除APK内的无用文件,由于Apk中数量最多的是资源文件,所以shrinkResources对这部分有明显的贡献,最好开启模式效果更好。不过删除无用文件的收益主要还是来自文件大小本身,减少file header只是其”隐形“的附加的收益。总的来说中央目录记录区占用的大小并不是很大,优化空间也比较有限。数据区优化
数据区是占用空间的大头,同样受单个大小和数量两个因素影响。提升压缩率
单个大小的主体是文件压缩后的数据,从ZIP的视角看就是如何提高压缩率。上面我知道ZIP支持很多压缩方法,总体切入点有两个。1. APK内并不是所有的文件都是压缩的,有些文件是直接Store的,可以考虑将Sotore改为压缩状态。2. APK使用的Deflate其实并不是压缩率最高的算法,可以考虑更换压缩率更高的算法。不过更换压缩算法的话需要考虑解压器,Android(JDK) 的ZIP实现只支持Store和Deflate两种,这就限制APK只能使用Deflate算法,那这样就没有优化空间了么?不然,Deflate算法只是个标准,具体的实现也是有优劣的,JDK的Deflate压缩并不算很优,使用更优Defalte算法也是尝试的方向之一。Store改为压缩
public class PackagingUtils {
/**
* List of file formats which are already compressed or don't compress well, same as the one
* used by aapt.
*/
public static final ImmutableList DEFAULT_AAPT_NO_COMPRESS_EXTENSIONS =
ImmutableList.of(
".jpg", ".jpeg", ".png", ".gif", ".opus", ".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");
}
如上代码所示,考虑到运行时的性能,AGP在package阶段针对如上的文件格式不进行压缩,这些文件以Store形式存放到APK中。从包体积的角度考虑的话可以配置这些类型的文件压缩,AndResguard也提供了这项能力,如下我们的应用已经配置了其中四种格式的文件进行压缩。andResGuard {
use7zip = true
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"resources.arsc",
]
}
但仍然有一些文件是未压缩的,通过unzip -lv test.apk > zipinfo.txt来查看文件的压缩状态。
如上有.webp , .mp3 , .jar文件没有进行压缩,但经过尝试发现仅有jar文件能压缩并有效。
- webp本身就是一种数据高度压缩的文件格式,很多webp经过Defalte压缩之后会更大,因此一些ZIP压缩器在压缩过程中选择将其以Stored形式存放。
- mp3倒是压缩之后可以获取80KB的收益,但由于MediaPlayer在播放assets或者raw目录下的mp3时候经常会用到如下两个api,会在native层直接mmap,而mmap要求不压缩并且四字节对齐,否则会报错。
更优Defalte算法
按照 7z官网的说法 7-Zip 创建的 zip 格式比大多数其它压缩软件创建的都小 2-10%,因此AndResguard使用命令 7z a -tzip out.apk ./apkdir/* -mx=9 对APK进行重新压缩,此时使用的仍然是Deflate算法,压缩等级为最大9,得到的APK确实小了2%左右。我目前并未对Deflate以及7z的实现进行深入研究,按照微信的说法7z使用了大字典优化。
https://sparanoid.com/lab/7z/
我们也测试和验证了另一个对ZIP重压缩的库advzip,其使用libdeflate算法,发现其比7z压缩之后的更小,可以再小1%左右,已经在我们的应用上做了验证。压缩前后信息对比如下图,左侧为7z压缩之后的,右侧为advzip压缩之后的。其中Defl:X表示压缩的最好,Defl:N表示正常压缩,从压缩前后entry的size上也可以看出收益。
https://github.com/amadvance/advancecomp
https://github.com/ebiggers/libdeflate
advzip 库重压缩会把apk内所有的文件都压缩,不支持配置一些文件不压缩,这个需要修改代码扩展一下功能。删除无用文件
从减少数量上看,主要还是无用文件删除,这个主要还是依赖APK内部文件所对应的优化手段去实现,本文不做详细讨论。文件合并
从提升ZIP文件整体压缩视角看,还有另一个切入点文件合并,因为ZIP文件是单个文件压缩,无损压缩的方式只有重复数据压缩、编码压缩两种,而多个文件合并到一起之后重复数据会更多,而且编码压缩需要的字典也只需要一份,因此总体上能提高压缩率。该部分对于Dex这种大小有限制的文件并没有什么空间,如果能将一些So文件合并或者业务上的资源文件合并应该会有些优化效果,目前为止这部分并没有重大效果的优化实践。
本文先对ZIP文件格式做了简单的介绍,并在ZIP文件的视角下分析可能对Apk体积优化的地方。通过逐个对ZIP文件进行拆分以及挖掘,引出了的资源混淆、shrinkResources、提升压缩率,文件合并等优化。可能很创新的地方不是很多,很多知识都是旧的知识,但是以一种更加系统的分析方式呈现出来,希望能对大家有所帮助,也希望后来者能有更多的探索和创新吧。参考文档
ZIP压缩算法详细分析及解压实例解释
https://www.cnblogs.com/esingchan/p/3958962.html
zip 的压缩原理与实现
https://blog.csdn.net/21aspnet/article/details/232316
浅析ZIP格式
https://thismj.cn/2019/02/14/qian-xi-zip-ge-shi/
压缩包Zip格式详析
https://blog.csdn.net/qq_43278826/article/details/118436116
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!