专栏名称: 鸿洋
你好,欢迎关注鸿洋的公众号,每天为您推送高质量文章,让你每天都能涨知识。点击历史消息,查看所有已推送的文章,喜欢可以置顶本公众号。此外,本公众号支持投稿,如果你有原创的文章,希望通过本公众号发布,欢迎投稿。
目录
相关文章推荐
鸿洋  ·  从ZIP文件看包体积优化 ·  3 天前  
鸿洋  ·  Android 认识AMS与App端框架 ·  4 天前  
鸿洋  ·  再学安卓 - SystemServer ·  5 天前  
鸿洋  ·  Android性能优化之绑定RenderTh ... ·  6 天前  
51好读  ›  专栏  ›  鸿洋

从ZIP文件看包体积优化

鸿洋  · 公众号  · android  · 2025-01-10 08:45

正文


本文作者


作者:三雒

链接:

https://juejin.cn/post/7207423263344083004

本文由作者授权发布。


1
序言


我们在做Android包体积优化时候会将Apk拖入AS中分析,很自然发现Apk是由Dex、So、资源文件(resource.arsc,xml,asests等)三大部分组成,针对每一部分都可以进行相应的深入优化。但是我们往往会忽略Apk文件本身也是可以优化的,有点身入其中,不识庐山真面目的意思。


APK文件本身是一个ZIP文件,理解ZIP格式,从ZIP文件入手优化APK也是包体积优化不可忽略的一部分。


2
ZIP格式简介


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


file header中存储的信息如下:

中央目录记录尾部区
中央目录记录尾部主要作用是用来定位中央目录记录区的开始位置,同时记录压缩包的注释内容。


3
APK体积优化分析


上述我们已经对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文件能压缩并有效。
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文件合并或者业务上的资源文件合并应该会有些优化效果,目前为止这部分并没有重大效果的优化实践。


4
总结


本文先对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^)┛明天见!