第二章:Android构建过程与APK生成
你有没有想过,我们写的 Java/Kotlin 代码、资源文件、AndroidManifest.xml 最后是怎么变成那个能安装的 APK 文件的?说实话,我刚入行时对着 Gradle 点一下 Build,APK 就出来了,但里面发生了什么完全是个黑盒。后来有一次线上包出问题,我才被迫把整个流程摸了一遍。
今天我就带大家亲手拆开这个黑盒。从源码到 APK,中间经历了 aapt、dx、apkbuilder 这些老家伙,还有现代的 d8、bundletool。掌握它们,你才能从容应对构建时的各种诡异问题。
2.1 构建流程概览
Android 构建可以简单分成四个阶段:资源编译 → 代码编译 → 打包 → 签名对齐。每个阶段都有专门的工具在干活。
我习惯用一张图把整体流程画出来,这样看后续细节时心里有数。给你看看我画的这张流程简图:
这张图我故意把 资源编译 和 代码编译 分成了两棵树,最后在 APKBuiler 那里汇合。说白了,APK 就是个 Zip 包,但里面的内容必须按 Android 系统的要求组织。
2.2 AAPT:资源处理工具
AAPT(Android Asset Packaging Tool)是构建的第一道关卡。它负责编译资源文件,生成 R.java 和 compiled resources(.flat 格式)。现代 Gradle 用的都是 AAPT2,比老版本快不少。
AAPT2 的主要工作包括:
- 解析 AndroidManifest.xml,检查权限、四大组件声明
- 编译 drawable、layout 等 XML 资源为二进制格式(压缩体积)
- 生成 R.java 常量类,方便代码里引用资源 ID
- 生成资源表和字符串池,写入 resources.arsc
你可以在 .gradle 的中间产物目录(build/intermediates/res/)里看到这些编译后的 .flat 文件。嗯,平时 debug 时可以翻翻那里,挺有意思的。
2.3 Java 编译器与 D8/DX
代码这边经过 javac 或 kotlinc 编译成 .class 字节码。但这些 .class 是标准 JVM 格式,Android 的 Dalvik/ART 虚拟机不认识。所以必须通过 dx(老工具)或 D8(新工具)转成 .dex 格式。
为什么要转成 DEX? Android 的指令集和寄存器架构跟标准 JVM 不一样。D8 会把多个 .class 文件合并成一个 classes.dex,并且做字节码优化(方法内联、常量折叠等)。
我个人的习惯是关注 minSdkVersion。低于 21 的版本,D8 会自动启用多 dex(生成多个 classes.dex),因为单个 dex 的方法数上限是 65535。我曾经在一个老项目里把方法数撑爆了,编译时 D8 提示需要 multidex,但我忘了在 AndroidManifest 里声明 Application 继承 MultiDexApplication,结果低版本手机直接崩溃……那教训够深刻。
D8 和 DX 的核心命令:
# 老版本 DX
dx --dex --output=classes.dex MyApp.jar
# 新版本 D8(AGP 3.4+ 默认启用)
d8 --lib android.jar --output . MyApp.jar
现在 AGP 自动接管了这些,你只要在 build.gradle 里配置 compileSdk 和 minSdk 就行。
2.4 APK 打包与签名
有了编译好的资源(resources.arsc、.flat 文件)和 DEX 文件,下一步就是 APKBuiler 把它们塞进一个 Zip 包里。APKBuiler 是 Android SDK 里的一个工具,它负责:
- 按固定目录结构组织文件(META-INF、res、lib 等)
- 处理 AIDL 生成的 .java 文件(如果有)
- 合并多种资源配置(如国际化文件夹)
打包完成后得到的是一个未签名的 APK。接下来两步很重要:Zipalign 和 签名。
| 步骤 | 工具 | 作用 |
|---|---|---|
| 字节对齐 | Zipalign | 将 APK 内未压缩的二进制文件按 4 字节对齐,减少运行时内存访问开销 |
| 数字签名 | apksigner (v1/v2/v3) | 验证 APK 的完整性,防止篡改;v2 签名在 apk 文件末尾添加签名块 |
2.5 实战经验与注意事项
最后再分享几个我在项目中踩过的坑,希望能帮你少走弯路:
- 资源文件冲突: 多个 library 如果都定义了同名的 drawable 或 layout,AAPT2 默认以后声明的库为准。我建议在每个 module 的 build.gradle 里加上
resourcePrefix "mylib_"来避免。 - multiDex 配置: 如果方法数超过 65535,除了在 build.gradle 里开启
multiDexEnabled true,还要注意 Application 类必须继承MultiDexApplication或手动调用MultiDex.install()。否则在 Android 5.0 以下会 crash。 - 构建缓存: 重新构建时,AAPT2 和 D8 会利用缓存加速。但如果改了某个依赖包版本,记得
Build > Clean Project,否则可能遇到缓存导致的老代码残留。我记得有次升级了 support 库后忘记 Clean,运行时一直闪退,最后发现是 classes.dex 里还有旧库的类。 - Signing Report: 想知道 APK 用了哪种签名方案?可以运行
apksigner verify --print-certs app.apk。这个命令能看出 v1/v2/v3 分别是否生效。
好了,这一章我们跑通了从源码到 APK 的完整流水线。你看到了 AAPT 如何把资源变成 R.java,D8 如何把 .class 变成 .dex,最后 APKBuiler 和签名工具如何拼出可安装的 APK。下一章(其实不是下一章,你自己往后学吧)会重点剖析 APK 包体结构的内部细节,比如 META-INF 文件夹里的签名文件、resources.arsc 的结构等。到时候你会更清楚地理解为什么这些步骤是必须的。
建议你自己打开一个 debug APK,用 Android Studio 的 Build Analyzer 或者命令行工具 zipinfo 看看里面的文件和顺序,配合这章的内容,能加深理解。