• 深入分析APK文件格式


    前言

    最近在升级项目的 Gradle 及 AGP 版本,在进行APK编辑再压缩时遇到了前后压缩比不一致的问题,所以抽空又一总结了下 APK (ZIP) 文件格式。

    无论是使用 7z 进行包体积优化,或是快速构建多渠道包,又或是V2、V3签名等都是基于 APK (ZIP) 文件格式进行的,因此对 APK (ZIP) 格式的了解也颇为必要 。

    ZIP文件格式

    未进行(V2、V3)签名的 APK 就是一个标准的 ZIP 文件,所以本文会先介绍 ZIP 文件格式,后续再介绍签名后的 APK 与 标准 ZIP 文件有何差异。

    首先看看 ZIP 文件格式预览:

        [local file header 1]
        [file data 1]
        [data descriptor 1]
        . 
        .
        .
        [local file header n]
        [file data n]
        [data descriptor n]
        [central directory]
        [end of central directory record]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    对结构描述并划分一下,看起来清晰很多:

    ZIP 文件由两大区块组成:数据区、中心目录区

    下面分别介绍一下两大区块:

    数据区

    先看数据区,数据区可以理解为一个数组,数组的每一项由 [local file header][file data][data descriptor] 这三项组成。

    Local file header

    首先看看 local file header (文件头) 的结构定义:

    OffsetBytesDescription
    04Local file header signature标识 (0x04034b50)
    42Version needed to extract解压文件所需 pkware最低版本
    62General purpose bit flag通用比特标志位
    82Compression method压缩方式
    102File last modification time文件最后修改时间
    122File last modification date文件最后修改日期
    144CRC-32CRC-32校验码
    184Compressed size压缩后的大小
    224Uncompressed size未压缩的大小
    262File name length (n)文件名长度
    282Extra field length (m)扩展区长度
    30nFile name文件名
    30+nmExtra field扩展区

    这里需要注意的是每个 Entry 的文件头 都有自己的 Compression method —— 压缩方式,也就是说 ZIP 文件内的不同 Entry 可以选择不同的压缩方式,并不要求里面的文件都采用同一种压缩方式。

    不同的压缩方式对应不同的值,具体如下:

    compression method: (2 bytes)
        (see accompanying documentation for algorithm
        descriptions)
        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(tm)
       10 - PKWARE Data Compression Library Imploding
       11 - Reserved by PKWARE
       12 - File is compressed using BZIP2 algorithm
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    APK中,使用的是 0-Stored (不压缩)和 8-Deflated (Deflated 压缩算法压缩)

    File data

    Flie data 则是文件压缩或直接存储后的二进制数据。

    Data descriptor

    Data descriptor 区块,只有通用比特标志位的第三位bit为1时才会出现此区块。

    Data descriptor:
      
      crc-32                          4 bytes
      compressed size                 4 bytes
      uncompressed size               4 bytes
            
    This descriptor exists only if bit 3 of the general
    purpose bit flag is set (see below).  It is byte aligned
    and immediately follows the last byte of compressed data.
    This descriptor is used only when it was not possible to
    seek in the output .ZIP file, e.g., when the output .ZIP file
    was standard output or a non seekable device.  For Zip64 format
    archives, the compressed and uncompressed sizes are 8 bytes each.
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    案例展示

    进过上面的理论学习,接下来可以找个APK瞅瞅;

    可以看出:在该APK文件中,classes2.dex 文件的压缩方式为 0x0008 (Deflated)。

    中心目录区

    在 ZIP 文件中,数据区后面紧接着的就是中心目录区,ZIP 里的每个文件 (Entry) 在数据区和中心目录区都分别有一条对应的数据记录 (File Record) 和目录记录 (Dir Record)。(完成V2、V3签名的 APK 中,签名区块则位于数据区与中心目录区之间,签名区块这一点单独讲,先看看中心目录区。)

    中心目录区由多条目录文件头 (Fille header) 和一条目录尾部 (End of central directory record) 组成,

    File header

    Fille header 格式定义如下:

    OffsetBytesDescription
    04Central directory file header标识(0x02014b50)
    42Version made by压缩所用的pkware版本
    62Version needed to extract解压所需pkware的最低版本
    82General purpose bit flag通用位标记
    102Compression method压缩方法
    122File last modification time文件最后修改时间
    142File last modification date文件最后修改日期
    164CRC-32CRC-32校验码
    204Compressed size压缩后的大小
    244Uncompressed size未压缩的大小
    282File name length (n)文件名长度
    302Extra field length (m)扩展域长度
    322File comment length (k)文件注释长度
    342Disk number start文件开始位置的磁盘编号
    362Internal file attributes内部文件属性
    384External file attributes外部文件属性
    424relative offset of local header数据文件头的偏移地址
    46nFile name文件名
    46+nmExtra field扩展域
    46+n+mkFile comment文件注释内容

    从 File header 的格式定义中不难看出:目录区文件头比数据区文件头多了几个字段,例如:数据文件头的偏移地址。其他字段基本是重复的。

    目录记录 (Dir Record) 只有文件头,因此比起 数据记录 (File Record) 要小很多,解析起来也比较快,所以通过中心目录区可以快速获取压缩文件部分信息而不用解析整个 ZIP ,例如:ZIP 中是否含有某个文件。

    同时目录记录里包含了对应的文件记录的偏移量,这样能通过目录快速定位并解压 ZIP 内的单个文件,避免从数据区从头遍历解析。

    案例展示

    End of central directory record

    上面提到,通过中心目录区可以优化数据的查找和解析过程,那如何快速知道中心目录区的起始位置呢?

    目录尾部对中心目录进行了简要描述,中心目录区的起始位置就在目录尾 (End of central directory record) 当中,其格式如下:

    OffsetBytesDescription
    04End of central dir signature标记(0x06054b50)
    42Number of this disk当前磁盘编号
    62Number of the disk with the start of the central directory中心目录开始位置的磁盘编号
    82Total number of entries in the central directory on this disk该磁盘上所记录的中心目录数量
    102Total number of entries in the central directory中心目录总数
    124Size of central directory (bytes)中心目录的大小
    164offset of start of central directory with respect to the starting disk number中心目录起始位置偏移量
    202.ZIP file comment length(n)注释长度
    22n.ZIP file comment注释内容

    通过目录尾部的中心目录起始位置偏移,能快速获取目录位置。

    案例展示

    所以很有意思的一点:在某些场景下,ZIP 也可以从后往前解析的,而且能提效。常用的 ZipFile 正是使用了从后往前解析的方式。首先找到尾部,通过尾部,获取中心目录区的偏移量和长度,然后通过中心目录区域信息定位实际的数据区获取最终的数据。

    ZipFile

    这一步分析可能比较复杂,若阅读困难可以先略过~ 详细过程如下:

    1)创建ZipFile文件

    这一步主要是从目录尾部获取中心目录起始偏移等信息,并建立目录数据Hash与其起始偏移地址映射关系;
    在 ZipFile 的构造函数中会调用 ZipFile#open 函数:

    [java.util.zip.ZipFile#open] ->
    [xref/libcore/ojluni/src/main/native/zip_util.c#ZIP_Open]->
    [xref/libcore/ojluni/src/main/native/zip_util.c#ZIP_Open_Generic] ->
    [xref/libcore/ojluni/src/main/native/zip_util.c#ZIP_Put_In_Cache0] ->
    [xref/libcore/ojluni/src/main/native/zip_util.c#readCEN]

    通过进一步阅读可以了解到 [readCEN]函数中只是对中心目录做了粗略的解析。只有在 getEntry 时才会仔细解析单条目录记录,其解析逻辑如下:

    再看看尾部数据获取,从文件末尾查找能与尾部数据标志 0x06064B50 匹配的4个字节。

    2)通过Hash查找并解析Entry

    首先看下 ZipFile#getEntry(String ) 是如何查找并解析出 Entry 的:



    今天周末,再深究一下,看看newEntry的解析。

    这里就和我们看到头文件(File Header)的字段对应上了,头文件内容基本解析出来并放入ze 对象,ze 对象的指针做为函数的返回值。

    看到这里,能体会到 ZipFile 做的优化还是挺细致的,ZipFile 创建的时候只做目录的粗解析,在获取具体 文件(Entry) 信息时只解析需要的头文件,到目前为止只获取了 Entry 基本信息,并没有去解析数据区。

    3)二进制数据流读取

    再看看 ZipFile 如何获取文件的二进制数据流的?

    通过上面的 getEntry,我们已经能拿到 Entry 在中心目录区的头文件对象了,因此java.util.zip.ZipFile#getInputStream(ZipEntry entry)获取数据流时,通过 Entry 对应的指针获取数据记录 (File Record) 的起始地址直接读取数据即可。

    [java.util.zip.ZipFile#getInputStream(ZipEntry entry)] ->
    [java.util.zip.ZipFile.ZipFileInputStream#read(byte[], int, int)] ->
    [xref/libcore/ojluni/src/main/native/java_util_zip_ZipFile.c#ZipFile_read>] ->
    [xref/libcore/ojluni/src/main/native/zip_util.c#ZIP_Read]

    APK文件格式

    前文提到:未进行(V2、V3)签名的 APK 就是一个标准的 ZIP 文件,完成V2、V3签名的 APK 中,签名区块则位于数据区与中心目录区之间。

    APK Signing Block

    APK 签名区块的结构如下:

    偏移(Bytes)字节数描述
    A18签名区块长度(此8字节不计算在内)
    A1 + 8n一组或多组ID-Value
    B1 - 248签名区块长度
    B1 -1616常量标识 “APK Sig Block42”

    A1: 签名区块起始偏移
    B2: 中心目录区起始位置

    案例展示

    1)首先获取目录区域偏移量;

    目录尾部标识:06 05 4B 50,中央目录区起始位置偏移量:00 04 4E 7D

    2)通过中央目录区偏移量,找到常量标识"APK Sig Block42"及签名区块长度:

    同时也能知道签名区块长度为:0F F8;

    3)查找 ID-Valuce 的起始位置,以魔术最后一位,前移动 0F F8 :

    所以图中的偏移量为 0X043E86 就是ID-Value的起始位。

    4)查看 ID-Value 结构:

    长度(bytes)
    ID-Value 字节总数8字节(不包含本身)
    ID4字节
    Value“ID-值”对的长度 - 4 个字节

    V2 的签名信息存放在 ID = 0x7109871a 的数据块中
    V3 的签名信息存放在 ID = 0xf05368c0 的数据块中

    • 第一项 ID-Value
      • 偏移量:0X043E8D
      • 长度:05 6E
      • ID:71 09 87 1A

    • 第二项 ID-Value
      • 偏移量:0x0443FB
      • 长度: 0A 62
      • ID: 72 42 65 77

    • 第二项最后一位刚好与第二位的区块长度衔接

    • 不难发现这里的Value字节全为0,这些 0 本身毫无含义,本以为能删除掉缩减一点包体,但是经查询,这里为了使签名区块为4096 的整数倍与内存页对齐(同时高版本 Android 会检测这个对齐规则,不可删除)。

    部分团队的多渠道APK方案,则是在签名区块中加入额外的 ID-Value ,通常是固定一个 ID,Value设置为渠道信息即可。

    小结

    至此完成部分了 ZIP 及 APK 文件的格式分析,另外 ZIP 相关的其他知识点也比较繁多,很难说一篇文章能盖全,或全面了解,例如各类压缩算法、ZIP 版本差异以及 7z (后续计划讲下 7z ) 等等,但是能掌握到文中介绍的这些,也足以解决日常遇到的绝大多 ZIP 相关问题了。

    Android 学习笔录

    Android 性能优化篇:https://qr18.cn/FVlo89
    Android Framework底层原理篇:https://qr18.cn/AQpN4J
    Android 车载篇:https://qr18.cn/F05ZCM
    Android 逆向安全学习笔记:https://qr18.cn/CQ5TcL
    Android 音视频篇:https://qr18.cn/Ei3VPD
    Jetpack全家桶篇(内含Compose):https://qr18.cn/A0gajp
    OkHttp 源码解析笔记:https://qr18.cn/Cw0pBD
    Kotlin 篇:https://qr18.cn/CdjtAF
    Gradle 篇:https://qr18.cn/DzrmMB
    Flutter 篇:https://qr18.cn/DIvKma
    Android 八大知识体:https://qr18.cn/CyxarU
    Android 核心笔记:https://qr21.cn/CaZQLo
    Android 往年面试题锦:https://qr18.cn/CKV8OZ
    2023年最新Android 面试题集:https://qr18.cn/CgxrRy
    Android 车载开发岗位面试习题:https://qr18.cn/FTlyCJ
    音视频面试题锦:https://qr18.cn/AcV6Ap

  • 相关阅读:
    Java数据结构与Java算法学习Day06---堆(简略笔记记录)
    css实现排行榜样式(vue组件)
    mySQL创建表的基础命令
    Linux开源IM GGTalk 8.0发布,支持在统信UOS、银河麒麟上运行!
    2022-2027年中国劳动防护眼镜行业市场供需现状与趋势预测研究报告
    使用MySQL,请善用 JSON 这张牌
    进制转换详解(二进制、八进制、十进制、十六进制)
    python最基础
    Linux I2C(二) - I2C软硬件架构
    寒假训练——第一周(STL)
  • 原文地址:https://blog.csdn.net/weixin_61845324/article/details/133676343