说在前面
从最开始接触Android系统开始,每次进行代码编译都需要网上搜索编译指令。后来大致熟悉了Android的编译体系,加深了对Android编译的理解。
编译流程
编译 android 系统的流程,首先执行 source build/envsetup.sh,然后执行 lunch 选择板级配置,最后执行 make 编译
- source build/envsetup.sh 流程
脚本中的第一个函数是hmm,介绍了脚本的一些功能,cat <<EOF 表示把 EOF 中的内容当做文件打印到标准输出,包含了脚本的使用说明和功能介绍。
function hmm() {
cat <<EOF
Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch: lunch <product_name>-<build_variant>
- tapas: tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- croot: Changes directory to the top of the tree.
- m: Makes from the top of the tree.
- mm: Builds all of the modules in the current directory, but not their dependencies.
- mmm: Builds all of the modules in the supplied directories, but not their dependencies.
To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma: Builds all of the modules in the current directory, and their dependencies.
- mmma: Builds all of the modules in the supplied directories, and their dependencies.
- provision: Flash device with all required partitions. Options will be passed on to fastboot.
- cgrep: Greps on all local C/C++ files.
- ggrep: Greps on all local Gradle files.
- jgrep: Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- mangrep: Greps on all local AndroidManifest.xml files.
- mgrep: Greps on all local Makefiles files.
- sepgrep: Greps on all local sepolicy files.
- sgrep: Greps on all local source files.
- godir: Go to the directory containing a file.
Environment options:
- SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
ASAN_OPTIONS=detect_leaks=0 will be set by default until the
build is leak-check clean.
Look at the source to view more functions. The complete list is:
EOF
local T=$(gettop)
local A=""
local i
for i in `cat $T/build/envsetup.sh | sed -n "/^[[:blank:]]*function /s/function ([a-z_]*).*/1/p" | sort | uniq`; do
A="$A $i"
done
echo $A
}
函数只有在调用时才会执行,在执行 source build/envsetup.sh 时该函数并不会被调用,而只有在终端运行 hmm 时才会输出,输出使用说明和脚本中所有函数名称,但显然不够准确。
[android_8.1]$ source build/envsetup.sh
including device/rockchip/rk3399/vendorsetup.sh
including sdk/bash_completion/adb.bash
[android_8.1]$ hmm
Invoke ". build/envsetup.sh" from your shell to add the following functions to your environment:
- lunch: lunch <product_name>-<build_variant>
- tapas: tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]
- croot: Changes directory to the top of the tree.
- m: Makes from the top of the tree.
- mm: Builds all of the modules in the current directory, but not their dependencies.
- mmm: Builds all of the modules in the supplied directories, but not their dependencies.
To limit the modules being built use the syntax: mmm dir/:target1,target2.
- mma: Builds all of the modules in the current directory, and their dependencies.
- mmma: Builds all of the modules in the supplied directories, and their dependencies.
- provision: Flash device with all required partitions. Options will be passed on to fastboot.
- cgrep: Greps on all local C/C++ files.
- ggrep: Greps on all local Gradle files.
- jgrep: Greps on all local Java files.
- resgrep: Greps on all local res/*.xml files.
- mangrep: Greps on all local AndroidManifest.xml files.
- mgrep: Greps on all local Makefiles files.
- sepgrep: Greps on all local sepolicy files.
- sgrep: Greps on all local source files.
- godir: Go to the directory containing a file.
Environment options:
- SANITIZE_HOST: Set to 'true' to use ASAN for all host modules. Note that
ASAN_OPTIONS=detect_leaks=0 will be set by default until the
build is leak-check clean.
Look at the source to view more functions. The complete list is:
addcompletions add_lunch_combo build_build_var_cache cgrep check_product check_variant choosecombo chooseproduct choosetype choosevariant core coredump_enable coredump_setup cproj croot destroy_build_var_cache findmakefile get_abs_build_var getbugreports get_build_var getdriver getlastscreenshot get_make_command getprebuilt getscreenshotpath getsdcardpath gettargetarch gettop ggrep godir hmm is isviewserverstarted jgrep key_back key_home key_menu lunch _lunch m make mangrep mgrep mm mma mmm mmma pez pid printconfig print_lunch_menu provision qpid rcgrep resgrep runhat runtest sepgrep set_java_home setpaths set_sequence_number set_stuff_for_environment settitle sgrep smoketest stacks startviewserver stopviewserver systemstack tapas tracedmdump treegrep _wrap_build
下面先挑出只在函数外的内容进行分析
(1)
VARIANT_CHOICES=(user userdebug eng)
(2)
unset LUNCH_MENU_CHOICES
(3)
# add the default one here
add_lunch_combo aosp_arm-eng
add_lunch_combo aosp_arm64-eng
add_lunch_combo aosp_mips-eng
add_lunch_combo aosp_mips64-eng
add_lunch_combo aosp_x86-eng
add_lunch_combo aosp_x86_64-eng
(4)
# Execute the contents of any vendorsetup.sh files we can find.
for f in `test -d device && find -L device -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
`test -d vendor && find -L vendor -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
`test -d product && find -L product -maxdepth 4 -name 'vendorsetup.sh' 2> /dev/null | sort`
do
echo "including $f"
. $f
done
unset f
(5) addcompletions
(1) VARIANT_CHOICES 定义为一个数组,包含3种模式可选,在某些脚本函数中会使用到。
(2) unset LUNCH_MENU_CHOICES 删除给定的环境变量内容,重新进行设置
(3) 调用 add_lunch_combo 函数, 并传入参数,看下函数的具体实现
function add_lunch_combo()
{
local new_combo=$1
local c
for c in ${LUNCH_MENU_CHOICES[@]} ; do
if [ "$new_combo" = "$c" ] ; then
return
fi
done
LUNCH_MENU_CHOICES=(${LUNCH_MENU_CHOICES[@]} $new_combo)
}
这里对 LUNCH_MENU_CHOICES 数组的内容进行循环,与函数后传入的参数进行对比,如果数组中已经包含传入的参数项,直接返回;否则就把传入的参数加入到数组项中。
(4)检查是否存在 device、vendor 和 product 目录,如果存在的话,就在目录下查找脚本 vendorsetup.sh,并设定了查找深度为4层目录,2> /dev/null 这里将标准错误重定向到/dev/null,也就是不输出,然后把找到的脚本文件进行排序,最后调用找到的脚本文件。当前的脚本文件内容如下:
including device/rockchip/rk3399/vendorsetup.sh
add_lunch_combo rk3399-userdebug
add_lunch_combo rk3399-user
add_lunch_combo rk3399_mid-userdebug
add_lunch_combo rk3399_mid-user
add_lunch_combo rk3399pro-userdebug
add_lunch_combo rk3399pro-user
add_lunch_combo rk3399_box-userdebug
add_lunch_combo rk3399_box-user
(5) 查找 sdk/bash_completion 目录下所有以 .bash 结尾的文件,当前仅有一个
including sdk/bash_completion/adb.bash
前面已经分析过这个函数了,实际上还是把这些板级配置添加到数组中,而通过运行 lunch 命令时进行选择。
- lunch 流程
通过执行 lunch 选择合适的板级配置
function lunch()
{
local answer
if [ "$1" ] ; then
answer=$1
else
print_lunch_menu
echo -n "Which would you like? [aosp_arm-eng] "
read answer
fi
local selection=
if [ -z "$answer" ]
then
selection=aosp_arm-eng
elif (echo -n $answer | grep -q -e "^[0-9][0-9]*$")
then
if [ $answer -le ${#LUNCH_MENU_CHOICES[@]} ]
then
selection=${LUNCH_MENU_CHOICES[$(($answer-1))]}
fi
else
selection=$answer
fi
export TARGET_BUILD_APPS=
local product variant_and_version variant version
product=${selection%%-*} # Trim everything after first dash
variant_and_version=${selection#*-} # Trim everything up to first dash
if [ "$variant_and_version" != "$selection" ]; then
variant=${variant_and_version%%-*}
if [ "$variant" != "$variant_and_version" ]; then
version=${variant_and_version#*-}
fi
fi
if [ -z "$product" ]
then
echo
echo "Invalid lunch combo: $selection"
return 1
fi
TARGET_PRODUCT=$product
TARGET_BUILD_VARIANT=$variant
TARGET_PLATFORM_VERSION=$version
build_build_var_cache
if [ $? -ne 0 ]
then
return 1
fi
export TARGET_PRODUCT=$(get_build_var TARGET_PRODUCT)
export TARGET_BUILD_VARIANT=$(get_build_var TARGET_BUILD_VARIANT)
if [ -n "$version" ]; then
export TARGET_PLATFORM_VERSION=$(get_build_var TARGET_PLATFORM_VERSION)
else
unset TARGET_PLATFORM_VERSION
fi
export TARGET_BUILD_TYPE=release
echo
set_stuff_for_environment
printconfig
destroy_build_var_cache
}
首先判断有没有传入参数,如果有参数就把值赋给answer变量,否则调用 print_lunch_menu 打印板级配置菜单项,并接受用户终端输入。
function print_lunch_menu()
{
local uname=$(uname)
echo
echo "You're building on" $uname
echo
echo "Lunch menu... pick a combo:"
local i=1
local choice
for choice in ${LUNCH_MENU_CHOICES[@]}
do
echo " $i. $choice"
i=$(($i+1))
done
echo
}
该函数遍历 LUNCH_MENU_CHOICES 数组,输出每一项内容,而这些数组项则是通过调用 add_lunch_combo 函数添加进来的。
如果传入的参数为零,selection 变量就会赋值为 aosp_arm-eng,否则会输出answer的值,并从该值中搜索以2位数开始的字符串,如果找到的话,再进一步对这个数字进行数组越界的检查。如果这个数字小于 LUNCH_MENU_CHOICES 的数组项,就会把 LUNCH_MENU_CHOICES 的这一项赋给 selection 变量。
如果 selection 为空,则直接报错退出。
然后根据获取的数组项,进行字符串截取,从 selection 中取出 - 前面的字符串保存到 product,后面的字符串保存到 variant_and_version,然后再对 variant_and_version 进行字符串截取,保存到 variant。
然后根据以上的数据赋值给 TARGET_PRODUCT、TARGET_BUILD_VARIANT 和 TARGET_PLATFORM_VERSION等,然后调用 build_build_var_cache 对编译时必需的环境变量进行赋值和处理。
最后
调用 set_stuff_for_environment 函数设置编译环境的变量,包括 ANDROID_BUILD_PATHS 和 JAVA_HOME等。
调用 printconfig 打印最终准备好的环境变量。参考如下:
============================================
PLATFORM_VERSION_CODENAME=REL
PLATFORM_VERSION=8.1.0
TARGET_PRODUCT=rk3399
TARGET_BUILD_VARIANT=userdebug
TARGET_BUILD_TYPE=release
TARGET_PLATFORM_VERSION=OPM1
TARGET_BUILD_APPS=
TARGET_ARCH=arm64
TARGET_ARCH_VARIANT=armv8-a
TARGET_CPU_VARIANT=cortex-a53
TARGET_2ND_ARCH=arm
TARGET_2ND_ARCH_VARIANT=armv7-a-neon
TARGET_2ND_CPU_VARIANT=cortex-a15
HOST_ARCH=x86_64
HOST_2ND_ARCH=x86
HOST_OS=linux
HOST_OS_EXTRA=Linux-3.10.0-862.el7.x86_64-x86_64-with-centos-7.5.1804-Core
HOST_CROSS_OS=windows
HOST_CROSS_ARCH=x86
HOST_CROSS_2ND_ARCH=x86_64
HOST_BUILD_TYPE=release
BUILD_ID=OPM8.181205.001
OUT_DIR=out
AUX_OS_VARIANT_LIST=
============================================
调用 destroy_build_var_cache 清除不需要的中间环节产生的变量值。
- make 流程
谷歌在 Android 7.0 开始引入了 ninja 进行系统编译,相对于 Makefile 组织编译系统来说,ninja 在大的项目管理的速度和并行方面有突出的优势。但是现有的 Android 还是由 Makefile 组织,所以谷歌引入了 kati soong,将 Android.mk 文件转化成 ninja 文件,使用 ninja 文件对编译系统进行管理。
但从 Android 8.0 开始,引入了 Android.bp 文件来替代之前的 Android.mk 文件。与 Android.mk 文件不同,Android.bp 只是纯粹的配置文件,不包括分支、循环等流程控制,它使用 blueprint 框架来解析,最终转换成 ninja 文件。
Android 9.0 则强制使用 Android.bp 来构建编译。
blueprint 是生成、解析 Android.bp 的工具,是 soong 的一部分。 soong 则是专为 Android 编译而设计的工具,blueprint 只是解析文件的形式,而 soong 则解释内容的含义。
Android.mk 可以通过 soong 提供的 androidmk 转换成 Android.bp,但仅限简单配置。目前 Android 8.0 的编译流程中,仍然是使用 kati 来做的转换。
Android.bp --> blueprint --> soong --> ninja
Makefile && Android.mk --> kati --> ninja
Android.mk --> soong --> blueprint --> Android.bp
现存的 Android.mk 和 Android.bp 都会被转换为 ninja,Android.mk 和其他的 Makefile,会生成out/build-<product_name>.ninja
文件,而Android.bp则会生成out/soong/build.ninja
。此外,还会生成一个较小的out/combined-<product_name>.ninja
文件,负责把二者结合起来,作为执行入口。
当执行 make 的时候,会查找当前目录价的 Makefile 文件并执行,内容如下:
### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###
所以,真正的 Makefile 入口是 build/core/main.mk。
而我们在执行编译的时候并没有传入目标,那么就一定会有一个默认的目标,在 build/core/main.mk 能够找到如下内容:
ifndef KATI
host_prebuilts := linux-x86
ifeq ($(shell uname),Darwin)
host_prebuilts := darwin-x86
endif
.PHONY: run_soong_ui
run_soong_ui:
+@prebuilts/build-tools/$(host_prebuilts)/bin/makeparallel --ninja build/soong/soong_ui.bash --make-mode $(MAKECMDGOALS)
.PHONY: $(MAKECMDGOALS)
$(sort $(MAKECMDGOALS)) : run_soong_ui
@#empty
else # KATI
其中 MAKECMDGOALS 这个命名是 make 执行时后面的参数赋值,也就是我们执行任何 make 的时候都是执行 run_soong_ui 这个目标,这样 android 从 Makefile 切换为 soong 进行了编译流程,后面跟 make 就没有关系了。
- mm 流程
当我们单独编译某个模块时,可以在这个模块目录下输入 mm 命令进行编译,流程和make差不多,只不过目标是单独模块组成,会生成独立的ninja文件。
function mm()
{
local T=$(gettop)
local DRV=$(getdriver $T)
# If we're sitting in the root of the build tree, just do a
# normal build.
if [ -f build/soong/soong_ui.bash ]; then
_wrap_build $DRV $T/build/soong/soong_ui.bash --make-mode $@
else
# Find the closest Android.mk file.
local M=$(findmakefile)
local MODULES=
local GET_INSTALL_PATH=
local ARGS=
# Remove the path to top as the makefilepath needs to be relative
local M=`echo $M|sed 's:'$T'/::'`
if [ ! "$T" ]; then
echo "Couldn't locate the top of the tree. Try setting TOP."
return 1
elif [ ! "$M" ]; then
echo "Couldn't locate a makefile from the current directory."
return 1
else
local ARG
for ARG in $@; do
case $ARG in
GET-INSTALL-PATH) GET_INSTALL_PATH=$ARG;;
esac
done
if [ -n "$GET_INSTALL_PATH" ]; then
MODULES=
ARGS=GET-INSTALL-PATH-IN-$(dirname ${M})
ARGS=${ARGS////-}
else
MODULES=MODULES-IN-$(dirname ${M})
# Convert "/" to "-".
MODULES=${MODULES////-}
ARGS=$@
fi
if [ "1" = "${WITH_TIDY_ONLY}" -o "true" = "${WITH_TIDY_ONLY}" ]; then
MODULES=tidy_only
fi
ONE_SHOT_MAKEFILE=$M _wrap_build $DRV $T/build/soong/soong_ui.bash --make-mode $MODULES $ARGS
fi
fi
}
整个编译体系的Makefile大致可区分为:系统核心的Makefile,产品级的Makefile,具体模块的Makefile.
- Android.mk语法
在源码树中每一个模块的根目录下都有一个Android.mk文件,编译系统正是以模块为单位进行编译的,每个模块有唯一的模块名,一个模块可以依赖多个其他模块,模块之间的依赖关系是通过模块名来引用的(熟悉OpenWRT或者Buildroot编译体系的同学对此应该不会陌生)。
以 frameworks/native/cmds/servicemanager/Android.mk
为例,解析下具体的语法格式:
LOCAL_PATH:= $(call my-dir) //设置为当前文件夹所在路径
svc_c_flags =
-Wall -Wextra -Werror //设置svc_c_flags编译选项
ifneq ($(TARGET_USES_64_BIT_BINDER),true)
ifneq ($(TARGET_IS_64_BIT),true)
svc_c_flags += -DBINDER_IPC_32BIT=1
endif
endif
include $(CLEAR_VARS) //清空编译环境的变量,可能由其他模块设置过的变量
LOCAL_SHARED_LIBRARIES := liblog //当前模块在运行时依赖的动态库
LOCAL_SRC_FILES := bctest.c binder.c //当前模块包含的所有源码
LOCAL_CFLAGS += $(svc_c_flags) //设置CFLAGS编译选项
LOCAL_MODULE := bctest //当前模块的名称,具有唯一性
LOCAL_MODULE_TAGS := optional //当前模块的标签,设置为默认值(可能为debug,eng,user等)
include $(BUILD_EXECUTABLE) //编译成可执行程序
include $(CLEAR_VARS)
LOCAL_SHARED_LIBRARIES := liblog libcutils libselinux
LOCAL_SRC_FILES := service_manager.c binder.c
LOCAL_CFLAGS += $(svc_c_flags)
LOCAL_MODULE := servicemanager
LOCAL_INIT_RC := servicemanager.rc
include $(BUILD_EXECUTABLE)
除了上面的环境变量,编译系统还设置了很多其他的环境变量,如下:
环境变量 | 说明 |
---|---|
LOCAL_PACKAGE_NAME | 当前APK应用的名称(具有唯一性) |
LOCAL_C_INCLUDES | C/C++所需的头文件路径 |
LOCAL_STATIC_LIBRARIES | 当前模块静态连接需要的库 |
LOCAL_STATIC_JAVA_LIBRARIES | 当前模块依赖的Java静态库 |
LOCAL_JAVA_LIBRARIES | 当前模块依赖的Java动态库 |
LOCAL_CERTIFICATE | 签署当前应用的证书名称 |
BUILD_EXECUTABLE | 编译成可执行程序 |
BUILD_STATIC_JAVA_LIBRARY | 编译成Java静态库 |
BUILD_SHARED_LIBRARY | 编译成C/C++动态库 |
此外,编译系统还定义了一些函数,如下:
$(call all-java-files-under, ):获取指定目录下的所有Java文件;
$(call all-c-files-under, ):获取指定目录下的所有C文件;
$(call all-Iaidl-files-under, ) :获取指定目录下的所有AIDL文件;
$(call all-makefiles-under, ):获取指定目录下的所有Make文件;
- Android.bp语法
Android.bp 是一种配置文件,语法类似于JSON,文件中仅记录当前模块的信息,没有条件判断或者控制流语句,控制逻辑需要Go来处理。bp文件的语法类似于Bazel,由Go语言进行解析,转换为Ninja文件。
以 frameworks/base/Android.bp
文件为例,具体如下:
// ==== c++ proto device library ==============================
cc_library {
name: "libplatformprotos",
host_supported: true,
proto: {
export_proto_headers: true,
include_dirs: ["external/protobuf/src"],
},
target: {
host: {
proto: {
type: "full",
},
srcs: [
"core/proto/**/*.proto",
"libs/incident/**/*.proto",
],
},
android: {
proto: {
type: "lite",
},
// We only build the protos that are optimized for the lite
// runtime, as well as the only protos that are actually
// needed by the device.
srcs: [
"core/proto/android/service/graphicsstats.proto",
],
shared: {
enabled: false,
},
},
},
}
subdirs = [
"core/jni",
"libs/*",
"media/*",
"tools/*",
"native/android",
"native/graphics/jni",
]
optional_subdirs = [
"core/tests/utiltests/jni",
]
定义一个模块从模块的类型开始,如例子中的"cc_library",模块会包含一些属性,格式为"{name: value}".
编译指令
代码编译
编译指令 | 说明 |
---|---|
m | 在源码树的根目录执行编译 |
mm | 编译当前路径下所有模块,不包含依赖 |
mmm [module_path] | 编译指定路径下所有模块,不包含依赖 |
mma | 编译当前路径下所有模块,包含依赖 |
mmma [module_path] | 编译指定路径下所有模块,包含依赖 |
make [module_name] | 无参数,则表示编译整个Android代码 |
部分模块的编译指令:
# make help
Common make targets:
--------------------------------------------------------------------------------
droid Default target
clean (aka clobber) equivalent to rm -rf out/
snod Quickly rebuild the system image from built packages
vnod Quickly rebuild the vendor image from built packages
offline-sdk-docs Generate the HTML for the developer SDK docs
doc-comment-check-docs Check HTML doc links & validity, without generating HTML
libandroid_runtime All the JNI framework stuff
framework All the java framework stuff
services The system server (Java) and friends
help You're reading it right now
#### build completed successfully ####
模块 | make | mmm |
---|---|---|
init | make init | mmm system/core/init |
zygote | make app_process | mmm frameworks/base/cmds/app_process |
system_services | make services | mmm frameworks/base/services |
java framework | make framework | mmm framworks/base |
framework-res | make framework-res | mmm frameworks/base/core/res |
jni framework | make libandroid_runtime | mmm frameworks/base/core/jni |
binder | make libbinder | mmm frameworks/native/libs/binder |
上述mmm命令同样适用于mm/mma/mmma,编译系统采用的是增量编译,只会编译发生变化的目标文件。
代码检索
Android源码非常庞大,直接使用grep/ag来搜索代码,可能会耗费大量时间。根据具体需求,可以选择合适的检索指令,有效节省代码搜索时间,提高准确度。
指令 | 说明 |
---|---|
cgrep | Greps on all local C/C++ files. |
ggrep | Greps on all local Gradle files. |
jgrep | Greps on all local Java files. |
resgrep | Greps on all local res/*.xml files. |
mangrep | Greps on all local AndroidManifest.xml files. |
mgrep | Greps on all local Makefiles files. |
sepgrep | Greps on all local sepolicy files. |
sgrep | Greps on all local source files. |
其他指令
上面介绍了常用的编译和检索指令,还有一些其他指令,可以执行 hmm 查询指令的帮助信息。
指令 | 说明 |
---|---|
hmm | 查询所有指令的帮助信息 |
gettop | 查询Android源码的根目录 |
printconfig | 打印设置的编译参数配置 |
print_lunch_menu | 打印lunch可选的板级配置 |
godir | 跳转到包含某个文件的目录 |
findmakefile | 查询当前目录所在工程的Android.mk文件路径 |