0%

Android编译过程详解之二

  Android编译过程详解之一
  Android编译过程详解之二
  Android编译过程详解之三
  Android.mk解析
 
  配置好环境变量后,接下来要做的就是build了。为了提高编译速度,我们自定义了一个go.sh的脚本文件,详细下面解释。另,编译的时候一般会在后面加一个-j8来实现多线程编译,如:

1
   ./go.sh -j8 or make -j8/--jobs  

参数“-j“ 和”–jobs ”指定了同时编译的线程数量,通常是编译主机 CPU 支持的并发线程总数的 1 倍或 2 倍(如:在一个 4 核,每个核支持两个线程的 CPU 上,可以使用 make -j8 或 make -j16)。在调用 make 命令时,如果没有指定任何目标,则将编译默认目标“droid”(下面详解),然后将会编译出完整的 Android 系统镜像。

build系统分类

  整个build系统的Make文件可以分为三类:

  1. build系统核心文件:定义整个build系统的框架,其他所有make文件都是在这个框架的基础上写出来的。
  2. 产品的make文件:位于device目录,通常以公司名和产品名分为两级目录,如:\device\qcom\msm8916_32。
  3. 模块的make文件:每个模块专用的make文件,kernel中统一名字为Makefile,其他为Android.mk。 

build结果

所有的编译产物都将位于 /out 目录下,该目录下主要有以下几个子目录:

  • /out/host/:该目录下包含了针对主机的 Android 开发工具的产物。即 SDK 中的各种工具,例如:emulator,adb,aapt 等。
  • /out/target/common/:该目录下包含了针对设备的共通的编译产物,主要是 Java 应用代码和 Java 库。
  • /out/target/product//:包含了针对特定设备的编译结果以及平台相关的 C/C++ 库和二进制文件。其中,是具体目标设备的名称。
  • /out/dist/:包含了为多种分发而准备的包,通过“make disttarget”将文件拷贝到该目录,默认的编译目标不会产生该目录。 Build 的产物中最重要的是几个镜像文件,它们都位于 /out/target/product// 目录下。
  • system.img:包含了 Android OS 的系统文件,库,可执行文件以及预置的应用程序,将被挂载到根分区,描述的是设备上的system分区,即/system目录。
  • ramdisk.img:在启动时将被 Linux 内核挂载为只读分区,它包含了 /init 文件和一些配置文件。它用来挂载其他系统镜像、文件系统,并启动 init 进程。
  • userdata.img:将被挂载为 /data,包含了应用程序相关的数据以及和用户相关的数据。
  • boot.img:包含有Kernel及其启动参数、Ramdisk,以及可选的BootLoader第二阶段。
  • recovery.img:设备进入recovery模式时所加载的镜像,相当于除正常启动系统的第二系统,此系统用来更新正常系统,即刷机。

go.sh

先来分析一下go.sh文件,其代码如下:

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
echo "OEM_PROJECT_NAME=$OEM_PROJECT_NAME"
start_time=`date +%s`
source customer/oem_define.sh # 导入OEM厂商配置

setup_ccache() { #设置ccache,提高编译速度
export CCACHE_DIR=../.ccache
export USE_CCACHE=1
if [ ! -d $CCACHE_DIR ];then
prebuilts/misc/linux-x86/ccache/ccache -M 10G  #设置编译文件缓存大小为10G
fi
}

delete_ccache() { # 删除缓存
prebuilts/misc/linux-x86/ccache/ccache -C
rm -rf $CCACHE_DIR

}

setup_ccache # 调用setup_ccache()
if [ "$1" = "clean" ];then #如果第一个参数为clean,清除ccache缓存
delete_ccache
fi

oemtools/copy_mp.sh # copy modem bin文件
make $1 $2 $3 $4 $5 # 调用Makefile编译,下面详解


end_time=`date +%s`
let min=($end_time-$start_time)/60
let sec=($end_time-$start_time)%60
echo "build_time $min mins $sec seconds" #打印编译时间

make

  执行make命令的时候会执行当前路径的Makefile文件,内容如下:

1
2
3
### DO NOT EDIT THIS FILE ###
include build/core/main.mk   
### DO NOT EDIT THIS FILE ###

  相信上面的文件大家都能看懂,接下来就解析一下main.mk,此文件中定义了整个Android的编译关系,主要文件(黄色背景除$开头的文件皆位于build\core\目录下)关系如下:
make

main.mk

  此文件首先对编译环境进行检查,然后引入其他相关mk文件,另,还将定义几个主要的make目标,如droid,sdk等,关键代码如下:

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
# 主要引入了如下mk文件
include $(BUILD_SYSTEM)/help.mk # Targets that provide quick help on the build system.
-include $(OEM_RESOURCE_DIR)/CustomerSpec.mk # 自定义,引入项目配置文件
-include $(TOP)/customer/oem_define.mk # 导入OEM厂商配置
-include $(BUILD_SYSTEM)/config.mk #整个build系统的配置文件
-include $(BUILD_SYSTEM)/cleanbuild.mk # 允许强制清除编译文件
-include vendor/google/build/config.mk #Google特性配置
-include $(BUILD_SYSTEM)/definitions.mk # 引入Build 系统其他文件将用到的函数和全局变量定义。例如:
my-dir : 当前路径
all-subdir-makefiles :当前目录及子目录Makefile文件
all-subdir-java-files:当前目录及子目录java文件
all-java-files-under: 指定目录及子目录java文件
sign-package : 对package签名
include $(BUILD_SYSTEM)/distdir.mk # 定义dist目标,dist目标用来拷贝文件到指定路径
-include $(BUILD_SYSTEM)/dex_preopt.mk # 针对启动jar包的预优化
-include build/core/pdk_config.mk # pdk(Platform Development Kit)的配置包
-include $(BUILD_SYSTEM)/post_clean.mk # 在前一次 Build 的基础上检查当前 Build 的配置,并执行必要清理工作
-include $(BUILD_SYSTEM)/legacy_prebuilts.mk # 定义GRANDFATHERED_ALL_PREBUILT
-include $(BUILD_SYSTEM)/Makefile # main.mk的辅助文件
-include $(BUILD_SYSTEM)/help.mk # 列出主要的make目标及其说明
-include $(ONE_SHOT_MAKEFILE) # 一个变量,当使用“mm”编译时,值为当前指定路径下的mk文件路径
...
DEFAULT_GOAL := droid # 定义默认make目标
# 依据不同编译条件,包含不同目录
subdirs += build/tools/acp
subdirs := $(TOP)
# 导入所有Android.mk,顶层Android.mk下详解
subdir_makefiles := \
$(shell build/tools/findleaves.py --prune=$(OUT_DIR) --prune=.repo --prune=.git --prune=kernel --prune=prj_out --prune=links --prune=.svn $(subdirs) Android.mk)

编译器平台相关的mk文件在build\core\combo路径下,如:javac.mk、select.mk等 

另,因定义了DEFAULT_GOAL := droid,当在根目录输入“Make”时,实际上就等同于我们执行“make droid”。加载include的所有文件,完成对所有mk文件的解析以后就会寻找生成droid的规则,依次生成它的依赖,直到所有满足的模块被编译好,然后使用相应的工具打包成相应的img。 

 droid目标依赖于很多其他目标,这些目标相互配合实现了整个系统的编译,droid依赖的其他make目标关系如下图:
  droid依赖目标关系图 

 build系统中的一些make目标如下:

droid的依赖目标

目标 说明
$(modules_to_install) modules_to_install 变量包含了当前配置下所有会被安装的模块(一个模块是否会被安装依赖于该产品的配置文件,模块的标签等信息),因此该目标将导致所有会被安装的模块的编译
$(modules_to_check) 该目标用来确保我们定义的构建模块是没有冗余的
$(INSTALLED_ANDROID_INFO_TXT_TARGET) 该目标会生成一个关于当前 Build 配置的设备信息的文件,该文件的生成路径是:out/target/product//android-info.txt
systemimage 生成 system.img
$(INSTALLED_BOOTIMAGE_TARGET) 生成 boot.img
$(INSTALLED_RECOVERYIMAGE_TARGET) 生成 recovery.img
$(INSTALLED_USERDATAIMAGE_TARGET) 生成 userdata.img
$(INSTALLED_CACHEIMAGE_TARGET) 生成 cache.img
$(INSTALLED_FILES_FILE) 该目标会生成 out/target/product// installed-files.txt 文件,该文件中内容是当前系统镜像中已经安装的文件列表

其他主要目标

目标 说明
make clean 执行清理,等同于:rm -rf out/
make sdk 编译出 Android 的 SDK
make clean-sdk 清理 SDK 的编译产物
make update-api 更新 API。在 framework API 改动之后,需要首先执行该命令来更新 API,公开的 API 记录在 frameworks/base/api 目录下
make dist 执行 Build,并将 MAKECMDGOALS 变量定义的输出文件拷贝到 /out/dist 目录
make all 编译所有内容,不管当前产品的定义中是否会包含
make help 帮助信息,显示主要的 make 目标
make snod 从已经编译出的包快速重建系统镜像
make libandroid_runtime 编译所有 JNI framework 内容
make framework 编译所有 Java framework 内容
make services 编译系统服务和相关内容
make 编译一个指定的模块,local_target 为模块的名称
make clean- 清理一个指定模块的编译结果
make dump-products 显示所有产品的编译配置信息,例如:产品名,产品支持的地区语言,产品中会包含的模块等信息
make PRODUCT-xxx-yyy 编译某个指定的产品
make bootimage 生成 boot.img
make recoveryimage 生成 recovery.img
make userdataimage 生成 userdata.img
make cacheimage 生成 cache.img

build\core\config.mk

  此文件是最重要的mk文件之一,主要定义不同模块的编译常量、编译参数及常见文件后缀(如.zip,.jar,.apk);根据BoardConfig.mk配置产品相关参数;设置常用工具的路径。其主要源码如下:

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 定义标准源文件路径等的环境变量
SRC_HEADERS := \
$(TOPDIR)system/core/include \
$(TOPDIR)hardware/libhardware/include \
$(TOPDIR)hardware/libhardware_legacy/include \
$(TOPDIR)hardware/ril/include \
$(TOPDIR)libnativehelper/include \
$(TOPDIR)frameworks/native/include \
$(TOPDIR)frameworks/native/opengl/include \
$(TOPDIR)frameworks/av/include \
$(TOPDIR)frameworks/base/include \
$(TOPDIR)external/skia/include
SRC_HOST_HEADERS:=$(TOPDIR)tools/include
SRC_LIBRARIES:= $(TOPDIR)libs
SRC_SERVERS:= $(TOPDIR)servers
SRC_TARGET_DIR := $(TOPDIR)build/target
SRC_API_DIR := $(TOPDIR)prebuilts/sdk/api
...
include $(BUILD_SYSTEM)/dumpvar.mk # build之前显示此次build的配置信息
include $(BUILD_SYSTEM)/pathmap.mk # 将许多头文件的路径通过名值对的方式定义为映射表,并提供 include-path-for 函数来获取。如,通过$(call include-path-for, frameworks-native)便可以获取到 framework 本地代码需要的头文件路径

# 定义编译命令常量,对应相关mk文件,每个常量描述一种类型模块的编译方式
BUILD_COMBOS:= $(BUILD_SYSTEM)/combo # 编译器平台相关mk文件
CLEAR_VARS:= $(BUILD_SYSTEM)/clear_vars.mk # 清除之前定义环境变量
BUILD_HOST_STATIC_LIBRARY:= $(BUILD_SYSTEM)/host_static_library.mk # 编译主机静态库,指编译此系统的主机,即PC
BUILD_HOST_SHARED_LIBRARY:= $(BUILD_SYSTEM)/host_shared_library.mk # 编译主机动态库
BUILD_STATIC_LIBRARY:= $(BUILD_SYSTEM)/static_library.mk # 编译设备静态库
BUILD_RAW_STATIC_LIBRARY := $(BUILD_SYSTEM)/raw_static_library.mk # 编译原生静态库
BUILD_SHARED_LIBRARY:= $(BUILD_SYSTEM)/shared_library.mk # 编译设备动态库
BUILD_EXECUTABLE:= $(BUILD_SYSTEM)/executable.mk # 编译设备可执行文件
BUILD_RAW_EXECUTABLE:= $(BUILD_SYSTEM)/raw_executable.mk # 编译原生可执行文件
BUILD_HOST_EXECUTABLE:= $(BUILD_SYSTEM)/host_executable.mk # 编译主机可执行文件
BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk # 编译apk文件
BUILD_PHONY_PACKAGE:= $(BUILD_SYSTEM)/phony_package.mk #
BUILD_HOST_PREBUILT:= $(BUILD_SYSTEM)/host_prebuilt.mk # 处理一个或多个主机使用的已编译文件,依赖multi_prebuilt.mk
BUILD_PREBUILT:= $(BUILD_SYSTEM)/prebuilt.mk # 处理一个已经编译好的文件,如:jar包
BUILD_MULTI_PREBUILT:= $(BUILD_SYSTEM)/multi_prebuilt.mk # 处理一个或多个已编译文件,依赖prebuilt.mk
BUILD_JAVA_LIBRARY:= $(BUILD_SYSTEM)/java_library.mk # 编译设备动态java库
BUILD_STATIC_JAVA_LIBRARY:= $(BUILD_SYSTEM)/static_java_library.mk # 编译设备静态java库
BUILD_HOST_JAVA_LIBRARY:= $(BUILD_SYSTEM)/host_java_library.mk # 编译主机动态java库
BUILD_DROIDDOC:= $(BUILD_SYSTEM)/droiddoc.mk
BUILD_COPY_HEADERS := $(BUILD_SYSTEM)/copy_headers.mk
BUILD_NATIVE_TEST := $(BUILD_SYSTEM)/native_test.mk
BUILD_HOST_NATIVE_TEST := $(BUILD_SYSTEM)/host_native_test.mk
BUILD_NOTICE_FILE := $(BUILD_SYSTEM)/notice_files.mk
...

include $(BUILD_SYSTEM)/envsetup.mk #定义全局变量、用户特殊编译配置等 ,下详解
# Boards may be defined under $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)
# or under vendor/*/$(TARGET_DEVICE). Search in both places, but
# make sure only one exists.
# Real boards should always be associated with an OEM vendor.
#在$(SRC_TARGET_DIR)/board/$(TARGET_DEVICE) 或vendor/*/$(TARGET_DEVICE)中搜索BoardConfig.mk文件,但两个路径中只能存在一个mk文件
board_config_mk := \
$(strip $(wildcard \
$(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)/BoardConfig.mk \
$(shell test -d device && find device -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
$(shell test -d vendor && find vendor -maxdepth 4 -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
))
ifeq ($(board_config_mk),)
$(error No config file found for TARGET_DEVICE $(TARGET_DEVICE))
endif
ifneq ($(words $(board_config_mk)),1)
$(error Multiple board config files for TARGET_DEVICE $(TARGET_DEVICE): $(board_config_mk))
endif
include $(board_config_mk)
ifeq ($(TARGET_ARCH),)
$(error TARGET_ARCH not defined by board config: $(board_config_mk))
endif
TARGET_DEVICE_DIR := $(patsubst %/,%,$(dir $(board_config_mk)))
board_config_mk :=

  不同类型的模块的编译过程会有一些相同的步骤,例如:编译一个 Java 库和编译一个 APK 文件都需要定义如何编译 Java 文件。因此,config.mk中导入的mk 文件的定义中会包含一些共同的代码逻辑。为了减少代码冗余,将共同的代码复用起来,将共同代码放到专门的文件中,然后在其他文件中包含这些文件的方式来实现的。这些包含关系如下图所示:
   模块mk文件的关系

build\core\ envsetup.mk

此文件主要包括product_config.mk文件,配置build系统需要的环境变量,确定当前编译的主机平台信息,然后指定编译时输出文件的OUT目录。部分关键代码如下:

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
# The product defaults to generic on hardware
# NOTE: This will be overridden in product_config.mk if make
# was invoked with a PRODUCT-xxx-yyy goal.
ifeq ($(TARGET_PRODUCT),)
TARGET_PRODUCT := full
endif

# the variant -- the set of files that are included for a build
ifeq ($(strip $(TARGET_BUILD_VARIANT)),)
TARGET_BUILD_VARIANT := eng
endif
...
# Read the product specs so we an get TARGET_DEVICE and other
# variables that we need in order to locate the output files.
include $(BUILD_SYSTEM)/product_config.mk
...
# 指定编译文件输出路径
ifeq (,$(strip $(OUT_DIR)))
ifeq (,$(strip $(OUT_DIR_COMMON_BASE)))
OUT_DIR := $(TOPDIR)out
else
OUT_DIR := $(OUT_DIR_COMMON_BASE)/$(notdir $(PWD))
endif
endif
DEBUG_OUT_DIR := $(OUT_DIR)/debug
PRODUCT_OUT := $(TARGET_PRODUCT_OUT_ROOT)/$(TARGET_DEVICE)
TARGET_OUT_JAVA_LIBRARIES:= $(TARGET_OUT)/framework
TARGET_OUT_INTERMEDIATES := $(PRODUCT_OUT)/obj
TARGET_OUT := $(PRODUCT_OUT)/$(TARGET_COPY_OUT_SYSTEM)
TARGET_OUT_CACHE := $(PRODUCT_OUT)/cache

build\core\ product_config.mk

此文件主要根据lunch选择的编译项读取device目录或vendor目录下不同厂商自己定义的AndroidProducts.mk文件,内包含目标产品配置文件,如:msm8916_32.mk;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 
include $(BUILD_SYSTEM)/node_fns.mk
include $(BUILD_SYSTEM)/product.mk
include $(BUILD_SYSTEM)/device.mk
...
# Import all product makefiles.
$(call import-products, $(all_product_makefiles)) # 找到并导入所有Makefile
# Import all or just the current product makefile
$(call import-products, $(current_product_makefile)) # 找到并导入当前产品的Makefile
$(check-all-products) # 完整性检查
...
# 调用resolve-short-product-name函数,返回TARGET_PRODUCT的配置文件目录
INTERNAL_PRODUCT := $(call resolve-short-product-name, $(TARGET_PRODUCT))
...
# Find the device that this product maps to.
TARGET_DEVICE := $(PRODUCTS.$(INTERNAL_PRODUCT).PRODUCT_DEVICE) # 获得目标设备名
#如:INTERNAL_PRODUCT = device\qcom\msm8916_32\msm8916_32.mk
TARGET_DEVICE = msm8916_32

build\core\ product.mk

1
2
3
4
5
6
7
8
9
10
11
# 读取所有AndroidProducts.mk文件
define _find-android-products-files
$(shell test -d device && find device -maxdepth 6 -name AndroidProducts.mk) \
$(shell test -d vendor && find vendor -maxdepth 6 -name AndroidProducts.mk) \
$(SRC_TARGET_DIR)/product/AndroidProducts.mk
endef

# 读取AndroidProducts.mk文件中设置的所有PRODUCT_MAKEFILES变量序列(其实为产品配置文件路径)
define get-all-product-makefiles
$(call get-product-makefiles,$(_find-android-products-files))
endef

build\core\ device.mk

这个文件没怎么看懂, 大致就是定义变量、根据device名字获得mk文件路径、检查device的必须变量是否被定义之类的,以后有闲功夫再研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
_device_var_list := \
DEVICE_NAME \
DEVICE_BOARD \
DEVICE_REGION

define dump-device
$(info ==== $(1) ====)\
$(foreach v,$(_device_var_list),\
$(info DEVICES.$(1).$(v) := $(DEVICES.$(1).$(v))))\
$(info --------)

endefdefine import-devices
$(call import-nodes,DEVICES,$(1),$(_device_var_list))

define _resolve-short-device-name

build\core\ node_fns.mk

1
2
3
4
5
6
7
8
9
10
11
12
define clear-var-list $(foreach v,$(1),$(eval $(v):=)) # 清除用“:=”的变量名
define copy-var-list $(foreach v,$(2),$(eval $(strip $(1)).$(v):=$($(v)))) # 拷贝变量名
define move-var-list

define import-nodes $(1) $(2) $(3) # 导入变量,此定义需要三个入口参数:
$(1)是一个字串,是输出变量的主干名。例如”PRODUCTS"和”DEVICES“
$(2)是一个makefile文件列表,这些文件中应该含有对$(3)中变量的定义
$(3)是一个变量列表
import-nodes会创建这样形式的变量,例如:$(1)="PRODUCTS",$(2)中含有"build/target/product/core.mk", $(3)中含有"PRODUCT_NAME",而core.mk中定义了PRODUCT_NAME:=core,则变量为:PRODUCT.build/target/product/core.mk.PRODUCT_NAME:=core。
import-nodes中还考虑了inherit(即继承)的问题,如果某个PRODUCT.xxx.xxx变量的值中有‘@inherit:<mk文件>’标识,则会把那个mk文件中相应的变量的属性添加到PRODUCT.xxx.xxx中。'@inherit:<mk文件>'是通过inherit-product命令添加的。这个函数定义在product.mk里面。
...
# 定义其他对变量等的处理,没去细看,也不一一列出了

device\qcom\msm8916_32\Android.mk

1
2
3
4
5
6
7
8
9
#include $(CLEAR_VARS)
#LOCAL_MODULE := wpa_supplicant.conf
#LOCAL_MODULE_TAGS := optional # 默认在所有版本下都编译
#LOCAL_MODULE_CLASS := ETC
#LOCAL_SRC_FILES := $(LOCAL_MODULE)
#LOCAL_MODULE_PATH := $(TARGET_OUT_ETC)/wifi
#include $(BUILD_PREBUILT)

#include $(call all-makefiles-under,$(LOCAL_PATH)) # 导入所有mk文件

模块mk文件详见Android.mk解析:Android.mk解析