原文:《Android内核剖析》读书笔记 第18章 Android编译系统
添加一个项目,这个项目就是子项目,也就是Android源码编译后,编译目标中能够包含该项目上。比如,可增加一个C/C++工具程序,这样在目标系统的shell命令行下就可使用该工具;增加一个APK程序,在设备出厂时就预装了该程序。
要增加项目就要告诉编译中枢一些必要的信息:
1> 项目的target名称
2> 项目输出的类型是?APK 、Jar包?
3> 项目对应的variant?
1. 添加一个C项目
首先,需要选择一个子目录,可选择external目录。
接着,定义一个target名称,必须是全局唯一的,这里定义 为helloMake.
然后,可以写一个简单的"hello world"的main.c程序。
再编写Anroid.mk文件:
2. 添加一个APK项目
首先,在Eclipse下创建一个HelloApk的Android应用项目,接下来要做的就是将这个项目移植到Android源码中。
一般可以将HelloApk的源码放在/vendor/xx(厂商)/apps目录下。删除掉HellpApk工程下的bin和gen目录(它们是由Eclipse编译出来的)。
然后新建Android.mk文件:
- Android编译系统的核心仍然是第1章末尾介绍的Make,基于此建立了一个适合于Android自身的编译框架,该编译框架由各种.mk文件、shell脚本、python脚本共同组成;通过该框架各个系统模块可以单独编译或打包、也可以根据一定的规则进行整合打包输出;
框架的核心设计理论有如下几点:- 建立在Make之上,定义好各类target;并适当引入python、shell脚本;
- 基于COC(规则胜于配置)思想强制每个子项目使用Android.mk文件描述,为了降低该文件的编写难度,编译系统默认定义好了相关变量以及各种通用函数库;
- 利用python脚本自动扫描所有包括Android.mk文件的子目录,并将文件中的设置内容转换为Make命令;
- 整个编译系统的源码在./build/目录下,绝大部分都是.mk文件,理解该编译系统的本质实际上就是分析这些脚本文件之间的关系;
整个编译系统由编译中枢、子项目、输出路径三部分组成,参见下图:
- 编译中枢:包括各种.mk文件,这些文件将遍历系统所有的子项目,并生成相对应的target,从而当执行make时能够知道应该执行什么具体的编译或打包命令;
- 子项目:就是整个Android源码系统中的各个功能模块,每个功能模块的目录中必须包含一个Android.mk文件,这是基于COC的设计,不允许自定义文件名。该文件中描述的就是包括哪些源文件,该项目的输出目标是什么类型,比如是Jar包、apk包、so包等等;在实际编写该文件的过程中,编译中枢已经定义好很多常用的变量,对这些变量进行赋值即可;
- 输出路径:编译过程中产生的各种临时文件、以及最终目标文件;系统默认定义为out目录,下面又分2个子目录host/target;
- out/host/:对应的是在PC上进行编译过程中所需要的各种工具和中间产出,利用这些产出才能编译出最终所需的目标程序;
- out/target/:对应的是可用于手机上部署的最终输出文件,比如各类img文件;
- 各编译脚本之间的关系
- 编译时执行make命令会查找android源码顶级目录中的MakeFile文件,该文件直接跳转至./build/core/main.mk;
- main.mk就是整个编译中枢的开始,使用.PHONY定义了默认的target名称为droid,这样当执行make命令时是可以不用任何参数的;
- config.mk:核心是定义各类常用的变量、以及编译命令宏;里面具体会依赖于pathmap.mk/executable.mk/buildspec.mk/envsetup.mk/product_config.mk等;
- definitions.mk:定义编译过程中需要用到的各种脚本函数,比如all-java-files-under就是返回指定目录下所有的java文件;
- ./build/tools/findleaves.py:用来扫描指定目录下所有的Android.mk文件,可以用参数排除掉某些不必要的目录;
扫描出来的结果其实就是上面提到的子项目列表;
- product/variant/tag这三者之间的关系
- product:产品的名称,可简单理解为我们的编译目标代号,体现在源码编译目标值-分割后的第一个参数,比如lunch full-eng中的full;
他的值可以是任意的,但必须在编译打包脚本中定义,本质上他就是一组信息组合,不同的产品可以包括不同的子项目,比如我们可以自己新建一个product,只包括我们自己需要的几个子项目;但也可以不强制指定,而完全通过variant子类型来自动判断;
一个product中具体包括哪些子项目可以查看对应mk文件设置的PRODUCT_PACKAGES/PRODUCT_COPY_FILES两个变量值;
相关的脚本可参见./build/target/product/目录下的mk文件;比如常见的full.mk中未指定PRODUCT_PACKAGES变量值,而sdk.mk中就指定了该变量值,明确指出包括哪些子项目; - variant:产品子类型,与product搭配使用,是一组系统定义好的默认值,包括eng/user/userdebug/tests,体现在源码编译目标值-分割后的第二个参数,比如lunch full-eng中的eng,eng代表的意思是engineer;
设置variant参数有何作用呢,他与接下来要介绍的tag有着密切的mapping关系; - tag:每个子项目为自己设置的标签,可以在子项目下的Android.mk文件中的LOCAL_MODULE_TAGS变量设置,包括debug/eng/tests/optional/samples/shell_ash/shell_mksh,不能设置为其他值,系统编译时会在base_rules.mk中进行校验,校验不通过的话会出现警告,并不会将该子项目添加到任何product中;
从上可以看出variant和tag在很多可选值上面都是相同的,的确他们之间是有mapping关系的,也就是说当编译命令中指定variant后,具体执行过程中脚本会根据variant的值与每个子项目中设置的tag值进行比对,若匹配则编译该子项目并加入到product的输出路径中,否则不进行编译;
注意:tag和variant不是完全一一对应的,比如:- optional代表的是可选,即他不是android系统运行所必须的,但只要在product中PRODUCT_PACKAGES变量包括了该子项目,那该子项目就会被编译并添加到product中;
- samples代表的是实例性项目,多为独立子项目,一般无需添加到product中;
./development/samples/目录下所有的子项目都是该类型的; - shell_ash只有在system/core/sh/子项目中用到;
- shell_mksh只有在external/mksh/子项目中用到;
- product:产品的名称,可简单理解为我们的编译目标代号,体现在源码编译目标值-分割后的第一个参数,比如lunch full-eng中的full;
- Android标准应用apk包编译全过程,参见下图,理解这些原理对自己写批量打包脚本时非常重要:
- Framework/android.jar的编译(书中这段的描述个人觉得条理不太清晰,部分描述也不准确)
- 首先我们直接看sdk中的android.jar包中核心包括如下几个顶级package:
- java/javax:主要是Java语言的基础类库,比如lang/io/sql/util/xml/net等等;
- dalvik:里面类不多,主要包括dalvik虚拟机特有的DexClassLoader以及相关ByteCode类;
- android:大家都超级熟悉了吧,View/Activity/Handler/...都在这里了;
- asset/res:一些系统功能中会用到的各种资源文件;
- org/com:主要是一些通用性很强的第三方包,比如httpclient、json等;
- 以上的内容可以分成3类,分别来源于不同的jar包,包括:
- core.jar:Dalvik虚拟机运行所需要的基础类包,其功能主要是完成dex文件到标准class文件的转换与加载,在其之上可以正常运行标准java应用;对应于android.jar中java/javax/dalvik/这3个顶级package;
源码位于./libcore/;编译目标在./libcore/Android.mk中定义;编译完成后的输出位于./out/target/common/obj/JAVA_LIBRARIES/core_intermediates - framework.jar:基于Dalvik虚拟机之上用于开发真正android手机应用的框架包,比如视图的绘制、管理,页面的跳转与管理等;对应于android.jar中android/assets/res/这3个顶级package;
源码位于./frameworks/base;编译目标在./frameworks/base/Android.mk中定义;编译完成后的输出位于./out/target/common/obj/JAVA_LIBRARIES/framework_intermediates - ext.jar:扩展类库,集成一些通用性较高、已经被业界广泛接受并使用的工具包;对应于android.jar中org/com/这2个顶级package;
源码位于./external/;编译目标在./frameworks/base/Android.mk中定义;编译完成后的输出位于./out/target/common/obj/JAVA_LIBRARIES/ext_intermediates
- core.jar:Dalvik虚拟机运行所需要的基础类包,其功能主要是完成dex文件到标准class文件的转换与加载,在其之上可以正常运行标准java应用;对应于android.jar中java/javax/dalvik/这3个顶级package;
- 但是否core.jar/framework.jar/ext.jar中的所有内容都放在android.jar中呢?答案是否定的;原因是为了保持应用开发api的稳定性和向后兼容,google把源文件分为了“公有”和“私有”两类,明显私有的一些源文件代表稳定性不够未来很可能会变更,所以这部分不稳定源码是不会放进android.jar中的,经常看源码的同学会经常看到@hide这个标签,这也是一种私有表现;
android.jar编译完成后的输出位于./out/target/common/obj/JAVA_LIBRARIES/sdk_v${version}_intermediates
其中${version}代表的是SDK的具体版本号;
- 首先我们直接看sdk中的android.jar包中核心包括如下几个顶级package:
添加一个项目,这个项目就是子项目,也就是Android源码编译后,编译目标中能够包含该项目上。比如,可增加一个C/C++工具程序,这样在目标系统的shell命令行下就可使用该工具;增加一个APK程序,在设备出厂时就预装了该程序。
要增加项目就要告诉编译中枢一些必要的信息:
1> 项目的target名称
2> 项目输出的类型是?APK 、Jar包?
3> 项目对应的variant?
1. 添加一个C项目
首先,需要选择一个子目录,可选择external目录。
接着,定义一个target名称,必须是全局唯一的,这里定义 为helloMake.
然后,可以写一个简单的"hello world"的main.c程序。
再编写Anroid.mk文件:
LOCAL_PATH:=$(call my-dir)
inclue $(CLEAR_VARS)
LOCAL_SRC_FILES:=\
main.c
LOCAL_C_INCLUDES+=$(LOCAL_PATH)
LOCAL_MODULE:=helloMake
LOCAL_MODULE_TAGS:=eng
include $(BUILD_EXECUTABLE)
然后调用 make PRODUCT-full_erespo-eng来编译整个android工程,编译完后,将新的system.img烧入设备,启动 ./adb shell,在命令行下,直接调用helloMake,程序就会输出"hello world";
2. 添加一个APK项目
首先,在Eclipse下创建一个HelloApk的Android应用项目,接下来要做的就是将这个项目移植到Android源码中。
一般可以将HelloApk的源码放在/vendor/xx(厂商)/apps目录下。删除掉HellpApk工程下的bin和gen目录(它们是由Eclipse编译出来的)。
然后新建Android.mk文件:
LOCAL_PATH:=$(call my-dir)
inclue $(CLEAR_VARS)
LOCAL_SRC_FILES:=$(call alll-java-files-under, src)
LOCAL_C_INCLUDES+=$(LOCAL_PATH)
LOCAL_PACKAGE_NAME:=HelloApk //描述apk项目的名称
LOCAL_MODULE_TAGS:=optional //指定项目标签值,可指定为optional,tests,eng
LOCAL_SDK_VERSION:=current //指定APK项目所需的sdk版本
LOCAL_CERTIFICATE:=platform /*指定该APK项目将使用何种签名文件最后生成的apk文件。有4种:
platform,Framework源码最后将生成一个jar包
shared,一些系统应用程序使用该类签名
user,一些私有项目签名
tests,调试过程中使用的签名
*/
include $(BUILD_PACKAGE) //告诉编译中枢目标是一个APK程序