Android系统开发进阶-Android编译系统介绍

1. 编译系统变化

Android 最初是用 Android.mk 来定义模块的, Android.mk 本质上就是 Makefile。随着 Android 工程越来越大,包含的模块越来越多,以 Makefile 组织的项目编译花费的时间越来越多。google 在 Android 7.0 开始引入了 ninja 编译系统。相对于 make 来说 ninja 在大的项目管理中速度和并行方面有突出的优势,因此 google 采用了 ninja 来取代之前使用的 make。关于 ninja 的介绍,请参考 ninja 构建系统ninja 官方说明文档。由于 Android.mk 的数量巨大且复杂,不可能把所有的 Android.mk 改写成 ninja 的构建规则,因此 google 搞了个 kati 工具,用于将 Androd.mk 转换成 ninja 的构建规则文件 build.ninja,再使用 ninja 来进行构建工作。

Android 8.0 开始,google 引入了 Android.bp 文件来替代之前的 Android.mk 文件,Android.bp 只是纯粹的配置文件,不包括分支、循环等流程控制, 本质上就是一个 json 配置文件。同时还引入 Soong 这个工具,用于将 Android.bp 转换为 ninja 的构建规则文件 build.ninja,再使用 ninja 来进行构建工作。但之前的模块全部是用 Android.mk 来定义的,google 不可能一下子把所有模块都修改成 Android.bp,只能逐步替换,目前 Android 10 上还有少部分 Android.mk 的模块,按这进度,估计要到 Android 12 才能完全替换成 Android.bp 了。无论是 Android.mk 还是 Android.bp 最后都是转化成 ninja 的构建规则,再进行编译的。 各种文件跟工具的关系如下:
build-system-androidmk-to-ninja

2. 编译参数配置

Android编译系统的配置,可以分为三个层级,从下到上依次是:

2.1 平台级(Board)参数配置

平台级配置主要在 BoardConfig.mk 文件中参数主要有以下几个类别的配置:

  • CPU 体系结构: TARGET_ARCH, TARGET_CPU_ABI, TARGET_ARCH_VARIANT等。配置 CPU 是 x86 架构还是 arm 架构, arm 架构下又分好几个版本。编译系统会根据这个配置去加载对应 CPU 构架的 mk:build/core/combo/arch/$(TARGET_ARCH)/$(TARGET_ARCH_VARIANT).mk
  • 内核参数配置: BOARD_KERNEL_BASE, BOARD_KERNEL_PAGESIZE, BOARD_KERNEL_CMDLINE 等, 这些参数最终会被打包到 boot 分区的镜像文件中(boot.img),作为内核的启动参数。
  • 分区镜像: TARGET_USERIMAGES_USE_EXT4, BOARD_BOOTIMAGE_PARTITION_SIZE, BOARD_SYSTEMIMAGE_PARTITION_SIZE 等与分区格式、分区大小相关的参数。system, vendor 等分区的大小就是在这里面配置的。
  • 外设参数配置: 蓝牙, wifi 等外接设备的参数配置。

2.2 产品级(Product)的参数配置

同一块板子可以做不同的产品,各产品之间的差异主要是软件方面的差异,比如内置的应用不一样, 默认语言不一样等。AndroidProducts.mk 文件中的 PRODUCT_MAKEFILES 列出了所有产品的配置文件。
每一个产品都会有一个对应的 $(PRODUCT_NAME).mk 文件, 这个产品的所有配置都在里面列出,主要的配置项目如下:

  • PRODUCT_NAME : 产品名, 我们通过 lunch 选择的产品, 就要与这个配置保持一致, 同时产品的 mk 文件也要命名为 $(PRODUCT_NAME).mk。
  • PRODUCT_DEVICE : 板子的名称,编译系统会使用该名称查找 BoardConfig.mk。产品的输出目录也是根据该名称创建 out/target/product/$PRODUCT_DEVICE。
  • PRODUCT_BRAND : 对软件进行自定义所针对的品牌(如果有),例如运营商。
  • PRODUCT_MANUFACTURER : 制造商的名称, 会赋值给 ro.product.manufacturer 属性。
  • PRODUCT_MODEL : 最终产品的最终用户可见名称, 或者叫机型名。
  • PRODUCT_LOCALES : 以空格分隔的列表,用于列出由双字母语言代码和双字母国家/地区代码组成的代码对,以便说明针对用户的一些设置,例如界面语言和时间、日期以及货币格式。PRODUCT_LOCALES 中列出的第一个语言区域会被用作产品的默认语言区域。
  • PRODUCT_PACKAGES : 列出要安装的 APK 和模块。
  • PRODUCT_COPY_FILES : 预置文件列表,例如 source_path:destination_path。在编译相应产品时,应将源路径下的文件复制到目标路径。
  • PRODUCT_PROPERTY_OVERRIDES : 系统属性列表(采用“key=value”格式)列表。
  • PRODUCT_PACKAGE_OVERLAYS : 资源 overlay 目录。

以上是一个产品的常见配置, 基本上每一个产品都会用到这些配置, 作为一个系统 rom 开发者, 我们要非常熟悉这些配置的作用。
对于产品的配置还有一个重要的配置:编译类型(TARGET_BUILD_TYPE), 这个配置是我们在 lunch 选择要编译的产品的时候选择的。
我们在添加产品的时候有以下配置,把产品添加到 lunch 的选择列表中,其中的形式就是 $(PRODUCT_NAME)-$(TARGET_BUILD_TYPE)

1
2
3
4
COMMON_LUNCH_CHOICES := \
pure-eng \
pure-userdebug \
pure-user

编译类型的取值范围为:eng, user, userdebug。这三者的区别如下:

    1. eng 工程师版本, 主要用于开发调试阶段:
      • 安装带有 eng 或 debug 标记的模块。
      • 除了带有标记的模块之外,还会根据产品定义文件安装相应模块。
      • ro.secure=0
      • ro.debuggable=1
      • ro.kernel.android.checkjni=1
      • adb 默认处于启用状态。
      • WITH_DEXPREOPT 可以设置为 false, 即在编译时不对系统映像上安装的 DEX 代码调用 dex2oat。 具体作用下面单独讲。
    1. user 用户发布版本:
      • 安装带有 user 标记的模块。
      • 除了带有标记的模块之外,还会根据产品定义文件安装相应模块。
      • ro.secure=1
      • ro.debuggable=0
      • adb 默认处于停用状态。
      • WITH_DEXPREOPT 只能为 true, 不能设置为 false。
    1. userdebug 用户调试版本, 除了以下几点之外,其余均与 user 相同:
      • 还会安装带有 debug 标记的模块。
      • ro.debuggable=1
      • adb 默认处于启用状态。

WITH_DEXPREOPT 意思为预优化, 也就是把 Android 在启动或 APP 在运行时所需要做的一些事情,把这些事情转移到编译 APK 时完成,来达到更快的 Android 系统启动速度和更快的APP运行速度。Android 在首次启动和首次安装应用时,需要将字节码翻译成机器码,这样 Android 系统的启动速度将会大大减慢,如果没有预优化,APP 的运行速度也会加上翻译所需要的时间。所以,这个翻译的工作需要转移到编译上面来,也就是所,在编译 APK 文件时,将会预先对 APK 进行翻译的优化,然后再打包到系统里面去,这样 Android 系统在首次启动时,就不再需要花费大量的时间去翻译 APK 的字节码。

WITH_DEXPREOPT 虽然能够提高系统的首次开机速度和APP的首次运行速度, 但是会大大增加镜像的大小,从经验上来看,会增加1.5倍左右。而且 jar 经过预优化之后, 就没法简单的直接替换生效了。我们在开发过程经常需要修改 framework.jar, services.jar 等, 如果系统不是 eng 版本,替换之后是不生效的, 需要重新编译升级。所以为了提高开发验证速度, 我们需要替换 jar 包的话,就要使用 eng 版本的系统。

2.3 模块级(Module)参数配置

目前模块配置有两种形式,Android.mk 和 Android.bp。这两种模块定义系统是独立的,也就是 Android.mk 中定义的模块,不能被 Android.bp 中的模块依赖。 我们分别讨论一下这两种模块配置形式。

2.3.1 Android.mk

一个模块最基本的四个要素为:模块名, 源文件列表, 依赖关系, 模块类型。对于 Android.mk 模块来说,这几个基本要素的配置方法如下:

1
2
3
4
5
6
7
LOCAL_PATH := $(call my-dir)  
include $(CLEAR_VARS)
LOCAL_MODULE := hello # 模块名
LOCAL_SRC_FILES := hello.cpp    # 源文件列表
LOCAL_SHARED_LIBRARIES := liblog # 依赖关系
LOCAL_xxx := xxx
include $(BUILD_EXECUTABLE) # 模块类型

一个模块通常以以下两行配置作为开头:

1
2
LOCAL_PATH := $(call my-dir) 
include $(CLEAR_VARS)

这两行配置的作用是:

  • 设置当前模块的编译路径为当前文件夹路径。
  • 清理(可能由其他模块设置过的)编译环境中用到的变量。具体来说是重置除了 LOCAL_PATH 之外的所有以 LOCAL_ 开头的编译环境变量。

Android 源码是一个非常庞大的系统,包含了成千上万个模块,各模块之间的依赖关系也很复杂。为了方便模块的编译, Android 开发团队最初在 Makefile 的基础上,开发了一套编译系统,在这套编译系统下,我们要定义一个模块变得非常简单,只需要定义一系列的编译环境变量就行了,根本不用关心具体的编译细节。下面举例解析一下部分重要的编译环境变量:

  • LOCAL_MODULE:当前模块的名称,这个名称在系统中应当是唯一的,模块间的依赖关系就是通过这个名称来引用的。编译系统会自动添加适当的后缀。例如,libfoo,要产生动态库,则生成libfoo.so。
  • LOCAL_SRC_FILES:当前模块包含的所有源代码文件。
  • LOCAL_C_INCLUDES:C 或 C++ 语言需要的头文件的路径。
  • LOCAL_STATIC_LIBRARIES:当前模块依赖的静态库。
  • LOCAL_SHARED_LIBRARIES:当前模块依赖的动态库。
  • LOCAL_CFLAGS:提供给 C/C++ 编译器的额外编译参数。
  • LOCAL_JAVA_LIBRARIES:当前模块依赖的 Java 共享库。
  • LOCAL_STATIC_JAVA_LIBRARIES:当前模块依赖的 Java 静态库。
  • LOCAL_CERTIFICATE: APK 签名类型。
  • LOCAL_DEX_PREOPT: 禁止对当前模块进行预优化。
  • LOCAL_JACK_ENABLED: 禁止使用Jack编译工具链编译该模块。
  • LOCAL_MODULE_TAGS:当前模块所包含的标签,一个模块可以包含多个标签。标签的值可能是 user,eng, debug, optional。其中,optional 是默认标签。标签是提供给编译类型使用的。具体的规则见上面的 2.2 小节(产品级(Product)的参数配置 )中对编译类型的解析。 在 Android 10 中取消了 eng, debug 这两种类型,具体说明见路径 Android/build/make/Changes.md 中的说明。

除此以外,Build 系统中还定义了一些便捷的函数以便在 Android.mk 中使用,包括:

  • $(call all-java-files-under, ):获取指定目录下的所有 Java 文件。
  • $(call all-c-files-under, ):获取指定目录下的所有 C 语言文件。
  • $(call all-Iaidl-files-under, ) :获取指定目录下的所有 AIDL 文件。
  • $(call all-makefiles-under, ):获取指定目录下的所有 Make 文件。

定义完 LOCAL_XXX 变量之后, 最后一步是 include $(BUILD_XXX)
其中 BUILD_XXX 的可选项在 build/make/core/config.mk 文件中有定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CLEAR_VARS:= $(BUILD_SYSTEM)/clear_vars.mk
BUILD_HOST_STATIC_LIBRARY:= $(BUILD_SYSTEM)/host_static_library.mk
BUILD_HOST_SHARED_LIBRARY:= $(BUILD_SYSTEM)/host_shared_library.mk
BUILD_STATIC_LIBRARY:= $(BUILD_SYSTEM)/static_library.mk
BUILD_HEADER_LIBRARY:= $(BUILD_SYSTEM)/header_library.mk
BUILD_AUX_STATIC_LIBRARY:= $(BUILD_SYSTEM)/aux_static_library.mk
BUILD_AUX_EXECUTABLE:= $(BUILD_SYSTEM)/aux_executable.mk
BUILD_SHARED_LIBRARY:= $(BUILD_SYSTEM)/shared_library.mk
BUILD_EXECUTABLE:= $(BUILD_SYSTEM)/executable.mk
BUILD_HOST_EXECUTABLE:= $(BUILD_SYSTEM)/host_executable.mk
BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk
BUILD_PHONY_PACKAGE:= $(BUILD_SYSTEM)/phony_package.mk
BUILD_RRO_PACKAGE:= $(BUILD_SYSTEM)/build_rro_package.mk
BUILD_HOST_PREBUILT:= $(BUILD_SYSTEM)/host_prebuilt.mk
BUILD_PREBUILT:= $(BUILD_SYSTEM)/prebuilt.mk
BUILD_MULTI_PREBUILT:= $(BUILD_SYSTEM)/multi_prebuilt.mk
BUILD_JAVA_LIBRARY:= $(BUILD_SYSTEM)/java_library.mk
BUILD_STATIC_JAVA_LIBRARY:= $(BUILD_SYSTEM)/static_java_library.mk
BUILD_HOST_JAVA_LIBRARY:= $(BUILD_SYSTEM)/host_java_library.mk
...

可见 BUILD_XXX 变量都是一个 mk 文件路径,每一个 BUILD_XXX 定义了一种编译规则。
当我们 include $(BUILD_XXX) ,编译系统就根据前面所定义的 LOCAL_XXX 变量,来定义模块的目标, 依赖关系,及编译命令,编译参数等。
需要注意的是,这个时候只是定义了模块的目标而已,并没有开始编译。

2.3.3 Android.bp

对于 Android.bp 模块来说,四个基本要素的配置方法如下:

1
2
3
4
5
6
7
cc_binary {              // 模块类型
name: "hello", // 模块名
srcs: ["hello.cpp"], // 源文件列表
shared_libs: [ // 依赖关系
"liblog",
],
}

Android.bp 支持的模块类型基本上跟 Android.mk 是一样的。它们的对应关系如下(Android/build/soong/androidmk/cmd/androidmk/android.go):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
var moduleTypes = map[string]string{
"BUILD_SHARED_LIBRARY": "cc_library_shared",
"BUILD_STATIC_LIBRARY": "cc_library_static",
"BUILD_HOST_SHARED_LIBRARY": "cc_library_host_shared",
"BUILD_HOST_STATIC_LIBRARY": "cc_library_host_static",
"BUILD_HEADER_LIBRARY": "cc_library_headers",
"BUILD_EXECUTABLE": "cc_binary",
"BUILD_HOST_EXECUTABLE": "cc_binary_host",
"BUILD_NATIVE_TEST": "cc_test",
"BUILD_HOST_NATIVE_TEST": "cc_test_host",
"BUILD_NATIVE_BENCHMARK": "cc_benchmark",
"BUILD_HOST_NATIVE_BENCHMARK": "cc_benchmark_host",

"BUILD_JAVA_LIBRARY": "java_library_installable", // will be rewritten to java_library by bpfix
"BUILD_STATIC_JAVA_LIBRARY": "java_library",
"BUILD_HOST_JAVA_LIBRARY": "java_library_host",
"BUILD_HOST_DALVIK_JAVA_LIBRARY": "java_library_host_dalvik",
"BUILD_PACKAGE": "android_app",

"BUILD_CTS_EXECUTABLE": "cc_binary", // will be further massaged by bpfix depending on the output path
"BUILD_CTS_SUPPORT_PACKAGE": "cts_support_package", // will be rewritten to android_test by bpfix
"BUILD_CTS_PACKAGE": "cts_package", // will be rewritten to android_test by bpfix
"BUILD_CTS_TARGET_JAVA_LIBRARY": "cts_target_java_library", // will be rewritten to java_library by bpfix
"BUILD_CTS_HOST_JAVA_LIBRARY": "cts_host_java_library", // will be rewritten to java_library_host by bpfix
}

var prebuiltTypes = map[string]string{
"SHARED_LIBRARIES": "cc_prebuilt_library_shared",
"STATIC_LIBRARIES": "cc_prebuilt_library_static",
"EXECUTABLES": "cc_prebuilt_binary",
"JAVA_LIBRARIES": "java_import",
"ETC": "prebuilt_etc",
}

每一种模块类型都有很多配置选项,这里就不一一列举了,请参考 soong官方文档

需要注意的是目前 Android.bp 还不支持预置APK模块。所以我们还是需要 Android.mk 来定义预置 APK。

3. 编译流程

我们编译 Android 系统一般有三个步骤:

    1. source build/envsetup.sh : 配置编译环境
    1. lunch : 选择要编译的产品
    1. make -j : 开始编译

我们接下来就从这三步骤入手, 分析一下编译系统的源码, 梳理一下系统的编译流程, 看看编译系统是怎么根据我们上面的模块,产品,平台配置等信息最终编译成各个系统镜像的。