签名块结构:藏在 APK 尾巴里的秘密

APK 签名块,说白了就是 Android 给 APK 文件加的一道「防伪封条」。但它不是随便贴在文件头或文件中间,而是藏在文件末尾的特定位置。这位置选得挺讲究——既不破坏 ZIP 结构,又能让系统在安装时快速找到它。

我个人习惯把这部分叫「APK 的尾巴三部曲」:ZIP 条目→中央目录→签名块→EOCD。今天我们就把它拆开来看。

APK 文件整体布局:签名块在哪?

先看一张经典的 APK 文件结构图:

APK 文件物理布局(从文件头到文件尾) ZIP 条目区(所有原始文件:DEX、资源、Manifest…) 中央目录(Central Directory) APK 签名块(APK Signing Block) ID 0x7109871a(v2/v3) ID 0x7109871b(v4) 其他扩展 ID EOCD(End of Central Directory) EOCD 中 comment 字段 → 指向签名块大小 关键点:签名块不改变 ZIP 结构,只占用尾部 comment 区域 (EOCD 的 comment 长度字段被重写为签名块大小)

你仔细看这张图,签名块正好卡在中央目录和 EOCD 之间。为什么放在这?我简单解释一下:ZIP 文件格式规定,EOCD 必须位于文件末尾,中央目录之前的内容(包括签名块)只要不破坏偏移量,系统就能忽略它。Android 安装器正是通过 EOCD 的 comment 字段大小反向计算出签名块的位置。

核心结论:签名块不是「附加」在文件末尾,而是「插入」在中央目录和 EOCD 之间。EOCD 的 comment 长度字段不再表示真正的注释,而是用来存储签名块的大小。

签名块对包体积的直接影响:不拆不知道

签名块不是固定大小的。我在项目中曾对比过按不同签名的 APK 体积差异——同一个 Release APK,用 RSA 签名和用 ECDSA 签名,最终 APK 大小差了将近 4KB。别小看这 4KB,对于经常凑在 100MB 边缘的包,一点冗余都可能决定你能不能发 Google Play。

签名块主要由以下几部分贡献体积:

  • 签名者证书链:每张证书大约 1~2KB,签名方证书链可能包含根证书、中间证书、签名证书。如果用了自签名证书可能只有一张,但如果是公开 CA 签发的,中间证书可能有 2~3 张。
  • 签名算法输出:RSA 2048 位签名 ≈ 256 字节,RSA 4096 位 ≈ 512 字节;ECDSA P-256 签名 ≈ 64~72 字节;DSA 签名 ≈ 56 字节。这个差异挺明显的。
  • 签名属性:v2/v3 的签名块中包含整个 APK 的摘要集合(每个文件一个摘要,DEX/资源等会单独计算),一串摘要加起来可能占几百字节到一两 KB。
  • v4 签名方案:新增的增量更新专用签名,会额外携带一个「签名半块」,体积约 1~2KB。
  • 对齐填充:签名块要求按 4 字节对齐,很多场景下会补 0~3 字节的填充。
签名方案 算法组合 签名块典型大小 备注
v1 + v2 RSA 2048 ~5 KB 同时携带 MANIFEST.MF/SF/RSA 文件 + 签名块
v3 + v4 RSA 2048 ~6 KB v4 半块 + 旋转证书信息
v2 + v3 ECDSA P-256 ~2.5 KB 签名体积小,证书链通常短
v2 + v3 + v4 ECDSA P-384 ~3.8 KB 兼容性与安全性的折中

我的建议:如果目标 SDK 已经 >= 28(Android 9),完全可以只使用 v2 或 v3 签名,不要 v1。v1 需要额外存储 MANIFEST 等文件,签名块体积会多 1~2 KB,而且 v1 的签名校验方式也更慢。我用 AGP 7.x 默认配置经常去掉 v1,效果不错。

签名信息大小分析:精确到字节

我们拆开看看签名块里每条信息到底多大。以最常见的 v2 签名块举例,里面是一串 ID-value 的序列化结构:

// 签名块结构(伪代码)
block = {
  block_size (8 字节)           // 整个块的长度
  sequence_of_id_value:
    {
      id_length (4 字节)
      id        (4 字节)
      value_length (4 字节)
      value     (variable)
    } ...
  block_size_again (8 字节)     // 与开头的 block_size 相同,用于校验
  magic "APK Sig Block 42" (16 字节)
}

每个 ID-value 对的开销是 12 字节(4+4+4)。如果签名信息里只放一个 v2 摘要 + 一个签名者,大概需要:

  • 摘要数据:DEX + 资源 + 原生库 + Manifest + 其他 ≈ 每个文件 32 字节(SHA-256),假设 100 个文件 → 3200 字节
  • 签名者信息:证书链(假设 1 张证书 ≈ 1000 字节)+ 签名值(RSA 2048 的 256 字节)+ signed data 的一些元数据 → 约 1300 字节
  • ID 列表开销:2 × 12 = 24 字节
  • padding: 最多 3 字节

粗略算下来,一个典型的 v2 签名块大概在 4.5~5.5 KB 之间。如果签名方案改成 v3,还会多一个「签名者旋转历史」字段,大概多 200~400 字节。v4 半块差不多 1.5KB。

⚠️ 我曾经遇到过一个问题:为了支持 Google Play 的 apk 签名方案 v4(增量更新),在 AGP 里同时开启了 v2+v3+v4,结果 APK 体积比只用 v2 多了 2.5 KB。这 2.5 KB 对于大型游戏包不算什么,但对于小工具类 App(比如 1~2 MB 的工具),可能就是 0.2%~0.5% 的体积增幅了。如果增量更新不是刚需,建议关闭 v4。

说到优化,你可能会问:「我能不能直接去掉签名块减小体积?」——不能。签名块是 Android 安装验证的命根子,去掉后系统会认为 APK 未签名直接拒绝安装。唯一的优化路径是:选择更紧凑的签名算法(ECDSA > RSA)、精简证书链、按需开启签名方案(别开 v1 和 v4 除非必须)。

还有个小细节:签名块的 padding 是受对齐控制的。之前有同事为了省几字节试图通过修改 EOCD 的 comment 偏移量手工截断签名块,结果包直接崩了。嗯,这坑我踩过,别试。