APK文件结构:签名块和文件对齐的底层逻辑

做APK优化的人,第一关就是要搞清楚APK这个zip包里到底装了啥。很多同学把签名当成一个黑盒,对着Android Studio点一下Build就完事了。但签名块的位置、对齐的方式,实实在在地影响着你的包体积——有时候甚至能差出几百KB。

这章我就带你把APK的肚子剖开看个明白。

APK的本质就是一个Zip包

说白了,APK就是一个标准Zip格式的压缩包,只是Android给它加了个签名块。Zip文件的结构其实很简单:从文件头到文件尾,大体分三块:

  • 本地文件头 + 文件数据 — 每个文件挨个存
  • 中央目录 — 记录所有文件的索引
  • End of Central Directory (EOCD) — 文件尾标记

但Android对APK有额外要求:签名信息不能放在Zip的数据区里,否则很容易被篡改。于是从Android 7.0开始,引入了APK Signature Scheme v2,在Zip的中央目录前面插了一个「签名块」。

我在项目里遇到过一个坑:某个第三方SDK的aar产物里,签名块后面有残留数据没清理干净,导致打出来的APK安装时会报"Invalid APK"的错误。排查了一下午才定位到问题。

搞懂签名块的位置

签名块不是一个独立文件,它是嵌入在Zip结构中一段连续的区域。准确的位置是:在中央目录之前,但在所有文件数据之后。看一下这个结构图就明白:

APK 文件结构(Zip 格式) 本地文件头 + 文件数据(N个Entry) APK Signing Block(签名块)v2 / v3 Central Directory(中央目录) End of Central Directory (EOCD) 文件对齐(Zip Alignment) • 每个文件数据起始地址需是4字节对齐 • 对齐填充在本地文件头与数据之间 • 对齐后体积可能增加,但mmap更快 签名块带来的体积变化 • v2签名块通常10~50KB • v3签名块包含证书链,更大 • 签名块内的padding也会增加体积

为什么签名块要插在数据区和中央目录之间?我个人理解,这样做的好处是:中央目录没必要跟着签名一起变,工具可以先扫描到EOCD,再找到中央目录,读签名,然后验证。如果签名块放在最后,Zip解析器很容易被搞乱。

签名块里都有些什么?

打开签名块,你会看到一组ID-Value对。每个条目由一个4字节的ID标识,后面跟着长度和数据。Android签名方案v2和v3都共用这个结构。

ID(十六进制)含义典型大小
0x7109871av2签名 (APK Signature Scheme v2)几KB~几十KB
0xf05368c0v3签名 (APK Signature Scheme v3)比v2稍大(含证书)
0x42726577v4签名(增量更新专用)视文件而定
其他第三方或未来扩展—

这里面有个小陷阱:签名块本身对齐也有要求。签名块的总长度必须是8字节对齐的。签名工具会在末尾补足padding。我曾经见过一个Case,因为签名工具的版本问题,padding被打成了0x00 0x00 0x00 0x00,结果多出4个无用字节,积少成多。

文件对齐是个"看不见的胖子"

Zip文件对齐这个概念,很多人要么忽视,要么以为就是Android独有的。其实Zip标准就有个「extra field」,可以在本地文件头后面补零,让文件的压缩数据从4字节对齐的地址开始。

为什么Android要求4字节对齐?因为Android系统在安装APK时,会用mmap把文件映射到内存。如果每个文件的开头不是对齐的,CPU读取时可能会出现跨页访问,降低性能。说白了,对齐是用体积换速度。

举个具体的例子吧。假设你的APK里有个DEX文件,压缩前是15MB,压缩后是8.2MB。如果不做对齐,它的数据起始地址可能是0x12345,不是4的倍数。对齐就需要在本地文件头后面加3个字节的0x00填充。这个填充量对单个文件来说微不足道,但一个APK里可能包含成百上千个文件——资源文件、图片、so库、DEX……加起来很容易多出几百KB。

关键结论:AGP默认会做4字节对齐,但如果你用到了非标准的资源压缩或自定义打包流程,对齐可能被破坏。我曾经接手过一个项目,用了自定义的ProGuard后处理脚本,不小心把对齐padding删掉了,导致APK安装时系统重新对齐又膨胀了300KB。嗯,这事让我难受了好几天。

签名块和文件对齐的叠加效应

你可能会想:签名块对体积的影响能有多大?我们算笔账。

  • v2签名块:一般12~30KB
  • v3签名块:包含完整的证书链,通常30~60KB
  • v4签名块:用于增量更新,如果开启了会增加约几十KB
  • 签名块内部的padding:最多8字节

单独看都不算大。但你要知道,签名块本身处于一个对齐的「关键位置」。签名块后面的中央目录必须紧跟其后,而中央目录的第一个文件记录也需要对齐。所以签名块的padding不当,会影响后面整个中央目录的对齐状态。

我习惯在构建完成后用 zipinfo -v your.apk 或者 Android Studio 的 APK Analyzer 来检查对齐情况。打开APK Analyzer,切换到「Raw File Size」视图,很容易看到哪些文件被填充了额外的字节。

小技巧:使用Android SDK自带的 zipalign 工具可以强制对齐:

zipalign -v -p 4 input.apk output.apk

-p 参数会把page align也加上(4096字节对齐,用于Android 11+)。谨慎使用,page align会让包体积变大更多。

签名方式的演进对体积的影响

从v1到v4,签名块的结构越来越复杂。v1签名是把签名信息放在META-INF目录里,本质上是个Jar签名,对APK结构影响最小。但它不安全——攻击者可以删除META-INF里的文件再重新签名。所以Google推荐用v2起步。

v2签名把整个APK(除了签名块本身)都纳入了签名保护范围,安全性高,但签名块占位在数据区之后,会导致中央目录的偏移量变化。这个变化本身不会增加体积,但签名块本身的size就是增量。

v3签名在v2基础上增加了证书轮转的支持,签名块里多了个证书列表,体积上涨15%~30%。我有个大型项目,模块化后有12个module,每个module用自己的签名,最终APK的签名块总大小达到了180KB——对,你没看错,签名块占了将近200KB。

避坑指南:我曾经给一个海外版App配置了v2+v3双签名(为了兼容低版本升级),结果签名块体积直接翻倍。后来发现低版本的用户占比已经不到1%,果断去掉了v2签名。每次构建节省了大概40KB。别盲目兼容,要看具体数据。

实操:怎么看你的APK签名块占了多少体积?

用Android Studio的Build > Analyze APK… 打开APK,左侧面板会列出所有条目。签名块在列表中通常显示为「APK Signing Block」或「Signing Block」。点它就能看到大小和内部区块。

你也可以用命令行:

apksigner verify --verbose my-app.apk

这个命令会列出签名方案的版本和签名块大小(虽然不会精确到字节,但够用了)。

更彻底的办法是写个Python脚本,直接从Zip文件中读出签名块的偏移量和长度。但这已经是工具链定制范畴了,这里就不展开。

好了,这章我们把APK的签名块和文件对齐这两个"隐形体积刺客"给捉了出来。下回你再看APK体积增大,心里至少有个底——到底是资源膨胀了,还是签名块在作怪。