架构¶
如图 4-1 所示,理解 Android 编译系统的入口是 build/core/ 目录下的 main.mk 文件——我们在上一章已经看到,它通过顶层 makefile 被调用。build/core/ 目录实际上包含了编译系统的主体内容,后文将覆盖其中的关键文件。需要再次提醒的是:Android 的编译系统将所有内容聚合到一个 makefile 中,它并不递归。因此,你看到的每个 .mk 文件最终都会成为包含整个系统构建规则的单一巨大 makefile 的一部分。

为什么 make 会"挂起"?¶
每次你输入 make 时,都会目睹一个看似恼人的构建现象:编译系统输出构建配置信息后,会在屏幕静止很长一段时间后才继续。在这段漫长的沉默之后,它才真正开始编译 AOSP 的各个部分,并像普通构建系统那样向屏幕输出常规信息。任何构建过 AOSP 的人都曾好奇:在这段时间里编译系统到底在做什么?答案是:它正在将 AOSP 中的所有 Android.mk 文件聚合到一起来。
如果你想亲眼看看这个过程,可以编辑 build/core/main.mk,将:
include $(subdir_makefiles)
替换为:
$(foreach subdir_makefile, $(subdir_makefiles), \
$(info Including $(subdir_makefile)) \
$(eval include $(subdir_makefile)) \
)
subdir_makefile :=
下次执行 make -j16 时,你就会看到实际发生了什么:
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=2.3.4
TARGET_PRODUCT=generic
...
============================================
Including ./bionic/Android.mk
Including ./development/samples/Snake/Android.mk
Including ./libcore/Android.mk
...
配置¶
编译系统做的第一件事,是通过包含 config.mk 来引入编译配置。编译配置可以通过两种方式完成:使用 envsetup.sh 和 lunch 命令,或者在顶层目录提供 buildspec.mk 文件。两种方式都需要设置以下部分变量。
envsetup.sh¶
envsetup.sh 脚本提供了一系列命令,用于配置和构建 AOSP。其核心是 lunch 命令,用于选择要构建的 Android 变体(flavor)和目标设备。
envsetup.sh 中包含的关键命令:
- lunch:选择要构建的产品。执行
lunch <product_name>可以设置TARGET_PRODUCT、TARGET_BUILD_VARIANT等环境变量。 - m:从源码树根目录运行 make,等价于完整编译。
- croot:快速回到源码树根目录。
- pid:查看正在运行的模拟器或设备的 ADB PID。
- grep:在所有 Makefile 中搜索指定的 makefile 变量或目标。
- get_build_var:获取指定变量的值。
- set_stuff_for_environment:设置编译所需的环境变量。
- set_sequence_number:设置序列号(用于并行构建)。
- add_lunch_combo:向 lunch 菜单添加新的构建变体组合。
通过 lunch 设置的主要变量包括:
| 变量 | 说明 |
|---|---|
TARGET_PRODUCT |
要构建的 Android 产品/设备型号 |
TARGET_BUILD_VARIANT |
构建变体(如 user、userdebug、eng) |
TARGET_ARCH |
CPU 架构(如 arm、x86) |
TARGET_OS |
操作系统(通常为 linux) |
TARGET_BUILD_TYPE |
构建类型(如 release、debug) |
BUILD_SYSTEM |
编译系统所在路径(通常为 build/core) |
执行 lunch <product> 后,这些变量被设置,后续的 make 命令会使用这些配置来决定构建哪些模块以及如何构建。
buildspec.mk的优先级高于envsetup.sh的设置。如果buildspec.mk存在于顶层目录,编译系统会首先读取其中的变量,覆盖通过lunch设置的值。
函数定义¶
由于 Android 的编译系统相当庞大——仅 build/core/ 下就有超过 40 个 .mk 文件——为了尽可能复用代码,系统中定义了大量的函数和宏。这些函数定义分散在各个 .mk 文件中,其中最重要的定义位于 definitions.mk。
definitions.mk 中定义的常用函数包括:
my_dir:返回当前 Android.mk 所在目录的路径。all-java-files-under:返回指定目录下所有的 Java 文件。all-c-files-under:返回指定目录下所有的 C 文件。all-cpp-files-under:返回指定目录下所有的 C++ 文件。intermediates:返回中间文件(.o、.dex 等)的输出目录。gen:生成文件的目录。java_library:构建 Java 库的标准模板。shared_library:构建共享库的标准模板。static_library:构建静态库的标准模板。cc_binary:构建 C/C++ 二进制可执行文件。cc_library:构建 C/C++ 库(可配置为静态或共享)。
这些函数通过 include 语句引入 Makefile,大幅简化了各个模块 Android.mk 的编写。
主要 Make 配方¶
你可能想知道各种产物(如 RAM disk 镜像、SDK 等)究竟是如何生成的。这些内容由 main.mk 中定义的主要构建目标控制。
核心构建目标:
- droid:默认目标,构建完整的 Android 系统镜像(相当于
make droid)。 - **sdk`:构建 Android SDK。
- **cts`:构建兼容性测试套件(CTS)。
- **ndk`:构建 Android 原生开发套件(NDK)。
- **clean
:清理构建输出(删除out/` 目录)。 clobber:完全清理,包括删除所有构建产物。modules:仅构建所有模块,不生成系统镜像。showcommands:在构建时显示每个命令的详细输出。
清理¶
如前所述,make clean 等价于删除 out/ 目录。clean 目标本身在 main.mk 中定义,但此外还有其他清理目标:
make clean:删除out/target/product/<product>/下的所有内容。make clobber:删除整个out/目录,包括所有产品的构建产物。make clean-<module>:清理指定模块的构建产物。
模块构建模板¶
以上描述的是编译系统的架构及其核心组件的运行机制。读完这些,你应该对 Android 的构建过程有了更清晰的认识——接下来看看各个模块的 Android.mk 是如何组织的。
每个 Android.mk 文件通常包含以下部分:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := <模块名>
LOCAL_SRC_FILES := <源文件列表>
include $(BUILD_<TYPE>)
其中 BUILD_<TYPE> 可以是 BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY、BUILD_EXECUTABLE 等,对应编译系统提供的各种模块构建模板。
输出¶
了解了编译系统的工作原理和模块构建模板的使用方式后,来看看它在 out/ 目录下创建了哪些产物。在较高层面,out/ 目录的结构如下:
out/
├── target/
│ └── product/
│ └── <product>/
│ ├── system/ # 系统镜像内容
│ │ ├── app/ # 系统应用
│ │ ├── bin/ # 系统可执行文件
│ │ ├── lib/ # 系统库
│ │ └── etc/ # 系统配置文件
│ ├── root/ # 根文件系统
│ ├── ramdisk.img # RAM disk 镜像
│ ├── system.img # system 分区镜像
│ ├── userdata.img # userdata 分区镜像
│ └── boot.img # 启动镜像
├── host/
│ └── linux-x86/ # 为主机(Linux x86)构建的工具
│ └── bin/ # 主机端工具(如 aapt, dx, adb)
└── gen/ # 生成的源代码(如 R.java, aidl)
构建产物按目标平台(target)和主机平台(host)分别组织。