理解 Android Build 系统

摘自: https://www.ibm.com/developerworks/cn/opensource/os-cn-android-build/index.htmlhtml

 

前言

Android Build 系统是 Android 源码的一部分。关于如何获取 Android 源码,请参照 Android Source 官方网站:java

http://source.android.com/source/downloading.htmlnode

Android Build 系统用来编译 Android 系统,Android SDK 以及相关文档。该系统主要由 Make 文件,Shell 脚本以及 Python 脚本组成,其中最主要的是 Make 文件。linux

众所周知,Android 是一个开源的操做系统。Android 的源码中包含了大量的开源项目以及许多的模块。不一样产商的不一样设备对于 Android 系统的定制都是不同的。android

如何将这些项目和模块的编译统一管理起来,如何可以在不一样的操做系统上进行编译,如何在编译时可以支持面向不一样的硬件设备,不一样的编译类型,且还要提供面向各个产商的定制扩展,是很是有难度的。api

但 Android Build 系统很好的解决了这些问题,这里面有不少值得咱们开发人员学习的地方。网络

对于 Android 平台开发人员来讲,本文能够帮助你熟悉你天天接触到的构建环境。并发

对于其余开发人员来讲,本文能够做为一个 GNU Make 的使用案例,学习这些成功案例,能够提高咱们的开发经验。app

概述

Build 系统中最主要的处理逻辑都在 Make 文件中,而其余的脚本文件只是起到一些辅助做用,因为篇幅所限,本文只探讨 Make 文件中的内容。框架

整个 Build 系统中的 Make 文件能够分为三类:

第一类是 Build 系统核心文件,此类文件定义了整个 Build 系统的框架,而其余全部 Make 文件都是在这个框架的基础上编写出来的。

图 1 是 Android 源码树的目录结构,Build 系统核心文件所有位于 /build/core(本文所提到的全部路径都是以 Android 源码树做为背景的,“/”指的是源码树的根目录,与文件系统无关)目录下。

图 1. Android 源码树的目录结构

图 1. Android 源码树的目录结构

第二类是针对某个产品(一个产品多是某个型号的手机或者平板电脑)的 Make 文件,这些文件一般位于 device 目录下,该目录下又以公司名以及产品名分为两级目录,图 2 是 device 目录下子目录的结构。对于一个产品的定义一般须要一组文件,这些文件共同构成了对于这个产品的定义。例如,/device/sony/it26 目录下的文件共同构成了对于 Sony LT26 型号手机的定义。

图 2. device 目录下子目录的结构

图 2. device 目录下子目录的结构

第三类是针对某个模块(关于模块后文会详细讨论)的 Make 文件。整个系统中,包含了大量的模块,每一个模块都有一个专门的 Make 文件,这类文件的名称统一为“Android.mk”,该文件中定义了如何编译当前模块。Build 系统会在整个源码树中扫描名称为“Android.mk”的文件并根据其中的内容执行模块的编译。

编译 Android 系统

执行编译

Android 系统的编译环境目前只支持 Ubuntu 以及 Mac OS 两种操做系统。关于编译环境的构建方法请参见如下路径:http://source.android.com/source/initializing.html

在完成编译环境的准备工做以及获取到完整的 Android 源码以后,想要编译出整个 Android 系统很是的容易:

打开控制台以后转到 Android 源码的根目录,而后执行如清单 1 所示的三条命令便可("$"是命令提示符,不是命令的一部分。):

完整的编译时间依赖于编译主机的配置,在笔者的 Macbook Pro(OS X 10.8.2, i7 2G CPU,8G RAM, 120G SSD)上使用 8 个 Job 同时编译共须要一个半小时左右的时间。

清单 1. 编译 Android 系统
1
2
3
$ source build/envsetup.sh
$ lunch full-eng
$ make -j8

这三行命令的说明以下:

第一行命令“source build/envsetup.sh”引入了 build/envsetup.sh脚本。该脚本的做用是初始化编译环境,并引入一些辅助的 Shell 函数,这其中就包括第二步使用 lunch 函数。

除此以外,该文件中还定义了其余一些经常使用的函数,它们如表 1 所示:

表 1. build/envsetup.sh 中定义的经常使用函数

第二行命令“lunch full-eng”是调用 lunch 函数,并指定参数为“full-eng”。lunch 函数的参数用来指定这次编译的目标设备以及编译类型。在这里,这两个值分别是“full”和“eng”。“full”是 Android 源码中已经定义好的一种产品,是为模拟器而设置的。而编译类型会影响最终系统中包含的模块,关于编译类型将在表 7 中详细讲解。

若是调用 lunch 函数的时候没有指定参数,那么该函数将输出列表以供选择,该列表相似图 3 中的内容(列表的内容会根据当前 Build 系统中包含的产品配置而不一样,具体参见后文“添加新的产品”),此时能够经过输入编号或者名称进行选择。

图 3. lunch 函数的输出

图 3. lunch 函数的输出

第三行命令“make -j8”才真正开始执行编译。make 的参数“-j”指定了同时编译的 Job 数量,这是个整数,该值一般是编译主机 CPU 支持的并发线程总数的 1 倍或 2 倍(例如:在一个 4 核,每一个核支持两个线程的 CPU 上,可使用 make -j8 或 make -j16)。在调用 make 命令时,若是没有指定任何目标,则将使用默认的名称为“droid”目标,该目标会编译出完整的 Android 系统镜像。

Build 结果的目录结构

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

  • /out/host/:该目录下包含了针对主机的 Android 开发工具的产物。即 SDK 中的各类工具,例如:emulator,adb,aapt 等。
  • /out/target/common/:该目录下包含了针对设备的共通的编译产物,主要是 Java 应用代码和 Java 库。
  • /out/target/product/<product_name>/:包含了针对特定设备的编译结果以及平台相关的 C/C++ 库和二进制文件。其中,<product_name>是具体目标设备的名称。
  • /out/dist/:包含了为多种分发而准备的包,经过“make disttarget”将文件拷贝到该目录,默认的编译目标不会产生该目录。

Build 生成的镜像文件

Build 的产物中最重要的是三个镜像文件,它们都位于 /out/target/product/<product_name>/ 目录下。

这三个文件是:

  • system.img:包含了 Android OS 的系统文件,库,可执行文件以及预置的应用程序,将被挂载为根分区。
  • ramdisk.img:在启动时将被 Linux 内核挂载为只读分区,它包含了 /init 文件和一些配置文件。它用来挂载其余系统镜像并启动 init 进程。
  • userdata.img:将被挂载为 /data,包含了应用程序相关的数据以及和用户相关的数据。

Make 文件说明

整个 Build 系统的入口文件是源码树根目录下名称为“Makefile”的文件,当在源代码根目录上调用 make 命令时,make 命令首先将读取该文件。

Makefile 文件的内容只有一行:“include build/core/main.mk”。该行代码的做用很明显:包含 build/core/main.mk 文件。在 main.mk 文件中又会包含其余的文件,其余文件中又会包含更多的文件,这样就引入了整个 Build 系统。

这些 Make 文件间的包含关系是至关复杂的,图 3 描述了这种关系,该图中黄色标记的文件(且除了 $开头的文件)都位于 build/core/ 目录下。

图 4. 主要的 Make 文件及其包含关系

图 4. 主要的 Make 文件及其包含关系

表 2 总结了图 4 中提到的这些文件的做用:

表 2. 主要的 Make 文件的说明

Android 源码中包含了许多的模块,模块的类型有不少种,例如:Java 库,C/C++ 库,APK 应用,以及可执行文件等 。而且,Java 或者 C/C++ 库还能够分为静态的或者动态的,库或可执行文件既多是针对设备(本文的“设备”指的是 Android 系统将被安装的设备,例如某个型号的手机或平板)的也多是针对主机(本文的“主机”指的是开发 Android 系统的机器,例如装有 Ubuntu 操做系统的 PC 机或装有 MacOS 的 iMac 或 Macbook)的。不一样类型的模块的编译步骤和方法是不同,为了可以一致且方便的执行各类类型模块的编译,在 config.mk 中定义了许多的常量,这其中的每一个常量描述了一种类型模块的编译方式,这些常量有:

  • BUILD_HOST_STATIC_LIBRARY
  • BUILD_HOST_SHARED_LIBRARY
  • BUILD_STATIC_LIBRARY
  • BUILD_SHARED_LIBRARY
  • BUILD_EXECUTABLE
  • BUILD_HOST_EXECUTABLE
  • BUILD_PACKAGE
  • BUILD_PREBUILT
  • BUILD_MULTI_PREBUILT
  • BUILD_HOST_PREBUILT
  • BUILD_JAVA_LIBRARY
  • BUILD_STATIC_JAVA_LIBRARY
  • BUILD_HOST_JAVA_LIBRARY

经过名称大概就能够猜出每一个变量所对应的模块类型。(在模块的 Android.mk 文件中,只要包含进这里对应的常量即可以执行相应类型模块的编译。对于 Android.mk 文件的编写请参见后文:“添加新的模块”。)

这些常量的值都是另一个 Make 文件的路径,详细的编译方式都是在对应的 Make 文件中定义的。这些常量和 Make 文件的是一一对应的,对应规则也很简单:常量的名称是 Make 文件的文件名除去后缀所有改成大写而后加上“BUILD_”做为前缀。例如常量 BUILD_HOST_PREBUILT 的值对应的文件就是 host_prebuilt.mk。

这些 Make 文件的说明如表 3 所示:

表 3. 各类模块的编译方式的定义文件

不一样类型的模块的编译过程会有一些相同的步骤,例如:编译一个 Java 库和编译一个 APK 文件都须要定义如何编译 Java 文件。所以,表 3 中的这些 Make 文件的定义中会包含一些共同的代码逻辑。为了减小代码冗余,须要将共同的代码复用起来,复用的方式是将共同代码放到专门的文件中,而后在其余文件中包含这些文件的方式来实现的。这些包含关系如图 5 所示。因为篇幅关系,这里就再也不对其余文件作详细描述(其实这些文件从文件名称中就能够大体猜出其做用)。

图 5. 模块的编译方式定义文件的包含关系

图 5. 模块的编译方式定义文件的包含关系

Make 目标说明

make /make droid

若是在源码树的根目录直接调用“make”命令而不指定任何目标,则会选择默认目标:“droid”(在 main.mk 中定义)。所以,这和执行“make droid”效果是同样的。

droid 目标将编译出整个系统的镜像。从源代码到编译出系统镜像,整个编译过程很是复杂。这个过程并非在 droid 一个目标中定义的,而是 droid 目标会依赖许多其余的目标,这些目标的互相配合致使了整个系统的编译。

图 6 描述了 droid 目标所依赖的其余目标:

图 6. droid 目标所依赖的其余 Make 目标

图 6. droid 目标所依赖的其余 Make 目标

图 6 中这些目标的说明如表 4 所示:

表 4. droid 所依赖的其余 Make 目标的说明

其余目标

Build 系统中包含的其余一些 Make 目标说明如表 5 所示:

表 5. 其余主要 Make 目标

在 Build 系统中添加新的内容

添加新的产品

当咱们要开发一款新的 Android 产品的时候,咱们首先就须要在 Build 系统中添加对于该产品的定义。

在 Android Build 系统中对产品定义的文件一般位于 device 目录下(另外还有一个能够定义产品的目录是 vender 目录,这是个历史遗留目录,Google 已经建议不要在该目录中进行定义,而应当选择 device 目录)。device 目录下根据公司名以及产品名分为二级目录,这一点咱们在概述中已经提到过。

一般,对于一个产品的定义一般至少会包括四个文件:AndroidProducts.mk,产品版本定义文件,BoardConfig.mk 以及 verndorsetup.sh。下面咱们来详细说明这几个文件。

  • AndroidProducts.mk:该文文件中的内容很简单,其中只须要定义一个变量,名称为“PRODUCT_MAKEFILES”,该变量的值为产品版本定义文件名的列表,例如:
1
2
3
4
PRODUCT_MAKEFILES := \
$(LOCAL_DIR)/full_stingray.mk \
$(LOCAL_DIR)/stingray_emu.mk \
$(LOCAL_DIR)/generic_stingray.mk
  • 产品版本定义文件:顾名思义,该文件中包含了对于特定产品版本的定义。该文件可能不仅一个,由于同一个产品可能会有多种版本(例如,面向中国地区一个版本,面向美国地区一个版本)。该文件中能够定义的变量以及含义说明如表 6 所示:
表 6. 产品版本定义文件中的变量及其说明

一般状况下,咱们并不须要定义全部这些变量。Build 系统的已经预先定义好了一些组合,它们都位于 /build/target/product 下,每一个文件定义了一个组合,咱们只要继承这些预置的定义,而后再覆盖本身想要的变量定义便可。例如:

1
2
3
4
5
6
7
# 继承 full_base.mk 文件中的定义
$(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk)
# 覆盖其中已经定义的一些变量
PRODUCT_NAME := full_lt26
PRODUCT_DEVICE := lt26
PRODUCT_BRAND := Android
PRODUCT_MODEL := Full Android on LT26
  • BoardConfig.mk:该文件用来配置硬件主板,它其中定义的都是设备底层的硬件特性。例如:该设备的主板相关信息,Wifi 相关信息,还有 bootloader,内核,radioimage 等信息。对于该文件的示例,请参看 Android 源码树已经有的文件。
  • vendorsetup.sh:该文件中做用是经过 add_lunch_combo 函数在 lunch 函数中添加一个菜单选项。该函数的参数是产品名称加上编译类型,中间以“-”链接,例如:add_lunch_combo full_lt26-userdebug。/build/envsetup.sh 会扫描全部 device 和 vender 二 级目 录下的名称 为"vendorsetup.sh"文件,并根据其中的内容来肯定 lunch 函数的 菜单选项。

在配置了以上的文件以后,即可以编译出咱们新添加的设备的系统镜像了。

首先,调用“source build/envsetup.sh”该命令的输出中会看到 Build 系统已经引入了刚刚添加的 vendorsetup.sh 文件。

而后再调用“lunch”函数,该函数输出的列表中将包含新添加的 vendorsetup.sh 中添加的条目。而后经过编号或名称选择便可。

最后,调用“make -j8”来执行编译便可。

添加新的模块

关于“模块”的说明在上文中已经提到过,这里再也不赘述。

在源码树中,一个模块的全部文件一般都位于同一个文件夹中。为了将当前模块添加到整个 Build 系统中,每一个模块都须要一个专门的 Make 文件,该文件的名称为“Android.mk”。Build 系统会扫描名称为“Android.mk”的文件,并根据该文件中内容编译出相应的产物。

须要注意的是:在 Android Build 系统中,编译是以模块(而不是文件)做为单位的,每一个模块都有一个惟一的名称,一个模块的依赖对象只能是另一个模块,而不能是其余类型的对象。对于已经编译好的二进制库,若是要用来被看成是依赖对象,那么应当将这些已经编译好的库做为单独的模块。对于这些已经编译好的库使用 BUILD_PREBUILT 或 BUILD_MULTI_PREBUILT。例如:当编译某个 Java 库须要依赖一些 Jar 包时,并不能直接指定 Jar 包的路径做为依赖,而必须首先将这些 Jar 包定义为一个模块,而后在编译 Java 库的时候经过模块的名称来依赖这些 Jar 包。

下面,咱们就来说解 Android.mk 文件的编写:

Android.mk 文件一般以如下两行代码做为开头:

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

这两行代码的做用是:

  1. 设置当前模块的编译路径为当前文件夹路径。
  2. 清理(可能由其余模块设置过的)编译环境中用到的变量。

为了方便模块的编译,Build 系统设置了不少的编译环境变量。要编译一个模块,只要在编译以前根据须要设置这些变量而后执行编译便可。它们包括:

  • LOCAL_SRC_FILES:当前模块包含的全部源代码文件。
  • LOCAL_MODULE:当前模块的名称,这个名称应当是惟一的,模块间的依赖关系就是经过这个名称来引用的。
  • 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_PACKAGE_NAME:当前 APK 应用的名称。
  • LOCAL_CERTIFICATE:签署当前应用的证书名称。
  • LOCAL_MODULE_TAGS:当前模块所包含的标签,一个模块能够包含多个标签。标签的值多是 debug, eng, user,development 或者 optional。其中,optional 是默认标签。标签是提供给编译类型使用的。不一样的编译类型会安装包含不一样标签的模块,关于编译类型的说明如表 7 所示:
表 7. 编译类型的说明

表 3 中的文件已经定义好了各类类型模块的编译方式。因此要执行编译,只须要引入表 3 中对应的 Make 文件便可(经过常量的方式)。例如,要编译一个 APK 文件,只须要在 Android.mk 文件中,加入“include $(BUILD_PACKAGE)

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

  • $(call my-dir):获取当前文件夹路径。
  • $(call all-java-files-under, <src>):获取指定目录下的全部 Java 文件。
  • $(call all-c-files-under, <src>):获取指定目录下的全部 C 语言文件。
  • $(call all-Iaidl-files-under, <src>) :获取指定目录下的全部 AIDL 文件。
  • $(call all-makefiles-under, <folder>):获取指定目录下的全部 Make 文件。
  • $(call intermediates-dir-for, <class>, <app_name>, <host or target>, <common?> ):获取 Build 输出的目标文件夹路径。

清单 2 和清单 3 分别是编译 APK 文件和编译 Java 静态库的 Make 文件示例:

清单 2. 编译一个 APK 文件
1
2
3
4
5
6
7
8
9
10
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 获取全部子目录中的 Java 文件
LOCAL_SRC_FILES := $(call all-subdir-java-files)         
# 当前模块依赖的静态 Java 库,若是有多个以空格分隔
LOCAL_STATIC_JAVA_LIBRARIES := static-library
# 当前模块的名称
LOCAL_PACKAGE_NAME := LocalPackage
# 编译 APK 文件
include $(BUILD_PACKAGE)
清单 3. 编译一个 Java 的静态库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
  
# 获取全部子目录中的 Java 文件
LOCAL_SRC_FILES := $(call all-subdir-java-files)
  
# 当前模块依赖的动态 Java 库名称
LOCAL_JAVA_LIBRARIES := android.test.runner
  
# 当前模块的名称
LOCAL_MODULE := sample
  
# 将当前模块编译成一个静态的 Java 库
include $(BUILD_STATIC_JAVA_LIBRARY)

结束语

整个 Build 系统包含了很是多的内容,因为篇幅所限,本文只能介绍其中最主要内容。

因为 Build 系统自己也是在随着 Android 平台不断的开发过程当中,因此不一样的版本其中的内容和定义可能会发生变化。网络上关于该部分的资料很零碎,而且不少资料中的一些内容已通过时再也不适用,再加上缺乏官方文档,因此该部分的学习存在必定的难度。

这就要求咱们要有很强的代码阅读能力,毕竟代码是不会说谎的。 要知道,对于咱们这些开发人员来讲,源代码就是咱们最忠实的朋友。 Use the Source,Luke!

 

相关主题

相关文章
相关标签/搜索