`
810364804
  • 浏览: 773531 次
文章分类
社区版块
存档分类
最新评论

Android编译系统简要介绍和学习计划

 
阅读更多

在Android源码环境中,我们开发好一个模块后,再写一个Android.mk文件,就可通过m/mm/mmm/make等命令进行编译。此外,通过make命令还可制作各种系统镜像文件,例如system.img、boot.img和recovery.img等。这一切都得益于Android编译系统,它为我们处理了各种依赖关系,以及提供各种有用工具。本文对Android编译系统进行简单介绍以及制定学习计划。

老罗的新浪微博:http://weibo.com/shengyangluo,欢迎关注!

在研究Android编译系统之前,我们首先需要了解Linux系统的make命令。在Linux系统中,我们可以通过make命令来编译代码。Make命令在执行的时候,默认会在当前目录找到一个Makefile文件,然后根据Makefile文件中的指令来对代码进行编译。也就是说,make命令执行的是Makefile文件中的指令。Makefile文件中的指令可以是编译命令,例如gcc,也可以是其它命令,例如Linux系统中的shell命令cp、rm等等。理解这一点非常重要,因为虽然通常我们说make命令是可以编译代码的,但是它实际上可以做任何事情。

看到这里,有的小伙伴可能会说,在Linux系统中,直接通过shell命令也可以做很多事情啊,它和make命令有什么区别呢?通过前面的介绍可以知道,make命令事实也是通过shell命令来完成任务的,但是它的神奇之处是可以帮我们处理好文件之间的依赖关系。我们通常都有会这样的一个需求,假设有一个文件T,它依赖于另外一个文件D,要求只有当文件D的内容发生变化,才重新生成文件T。这种需求在编译系统中表现得尤其典型,当一个*.c文件include的*.h文件发生变化时,需要重新编译该*.c文件,或者当一个模块A所引用的模块B发生变化时,重新编译模块B。正是由于编译系统中存在这种典型的文件依赖需求,而make命令又是专门用来解决这种文件依赖问题的,因此我们通常认为make命令是用来编译代码的。

Make命令是怎么知道两个文件之间存在依赖关系,以及当被依赖文件发生变化时如何处理目标文件的呢?答案就在前面提到的Makefile文件。Makefile文件实际上是一个脚本文件,就像普通的shell脚本文件一样,只不过它遵循的是Makefile语法。Makefile文件最基础的功能就是描述文件之间的依赖关系,以及怎么处理这些依赖关系。例如,假设有一个目录文件target,它依赖于文件dependency,并且当文件dependency发生变化时,需要通过command命令来重新生成文件T,这时候我们就可以在Makefile编写以下语句:

target: dependency
<tab>command -o target -i dependency
我们假设命令command的-o选项指定的是输出文件,而-i选项指定的是输入文件。此外,命令command必须是另起一行,并且以tab键开头。

这就是最基础也是最主要的Makefile文件语法。当然,Makefile文件还有很多其它的语法,这里不可能一一描述。老罗推荐一本书《GNU make中文手册》,里面非常详细地介绍了make以及Makefile文件语法。

通常我们在一个源代码工程中,包含有非常多模块,模块之间保持相对独立。我们说相对独立,是指模块之间只在接口上存在依赖,但是在实现上是相全独立的。例如,我们有一个SO模块A,它引用了另一个SO模块B的一个导出函数F。只要函数F的原型不变,那么模块B就可以独立于模块A来实现,而模块A只需要一个包含有函数F原型声明的头文件即可对模块B进行引用。在一个源代码工程中,使得模块之间保持上述的相对独立非常重要,否则的话,当模块多了之后,就会很容易造成混乱。

当一个源代码工程被划分成很多个相对独立的模块之后,我们就很直觉地想到,为每一个模块都编译写一个Makefile文件,使得它们可以独立编译,然后再在工程的根目录下编写一个Makefile文件来递归执行这些Makefile文件。事实上,这种做法是错误的。正确的做法无论工程有多少个模块,都应该只有一个Makefile文件。注意,即使整个工程只有一个Makefile文件,但是我们仍然是要求对工程进行模块划分,并且需要保持这些模块之间的相对独立性,否则的话,就会同样引发混乱。在整个工程只有一个Makefile文件的情况下,我们要求工程的各个模块拥有自己的Makefile片段。这些Makefile片段统统直接或者间接地通过Makefile语法中的include指令包含在工程的Makefile文件中,然后再进行编译。

为什么我们要求整个工程只有一个Makefile文件,而不是工程的每一个模块都拥有一个Makefile文件呢?直觉告诉我们,每一个模块都拥有一个Makefile文件就会显得更加独立。事实的确如此,每一个模块都拥有自己的Makefile文件可以使得它更加独立。但是这种方法在处理模块之间的依赖关系时会显得力不从心,导致make命令要么是做得太少(do too little),要么是做得太多(do too much)。Make命令做得太少意味着编译结果不正确,而做得太多意味着编译速度慢。注意,出现这种情况并不是make命令本身的设计有问题,而是因为我们给了它不好的输入,就是所谓的"垃圾进,垃圾出"(Garbage In, Garbage Out)。

早在1998年的时候,Peter Miller就发现了为工程的每一个模块都编写一个Makefile文件的种种坏处,并且发表有一篇论文Recursive Make Considered Harmful。有兴趣的小伙伴可以读一下,写得非常好。这里我们简单从这篇论文摘录一下为什么Recursive Make会导致“do too little”和“do too much”。

假设我们有一个工程,它的目录结构如下所示:

Project
  ----Makefile
  ----ant
       ----Makefile
       ----main.c
  ----bee
       ----Makefile
       ----parse.c
       ----parse.h
顶层目录的Makefile的内容如下所示:

MODULES = ant bee
all:
    for dir in $(MODULES); do \
        (cd $$dir; $(MAKE) all); \
    done
ant目录下的Makefile的内容如下所示:

all: main.o
main.o: main.c ../bee/parse.h
    $(CC) -I../bee -c main.c
通过图1的无回路有向图(DAG)可以清楚看到ant目录的Makefile所描述的文件依赖关系:

图1 ant模块的文件依赖关系图


bee目录下的Makefile的内容如下所示:

OBJ = ../ant/main.o parse.o
all: prog
prog: $(OBJ)
    $(CC) -o $@ $(OBJ)
parse.o: parser.c parse.h
    $(CC) -c parse.c
通过图2的无回路有向图(DAG)可以清楚看到bee目录的Makefile所描述的文件依赖关系:

图2 bee模块的文件依赖关系图


到目前为止,一切都正常,我们在工程根目录下执行make命令,就可以递归对ant和bee模块进行编译,并且最终生成可执行文件prog。

考虑一个情景,bee目录的parse.h和parse.c文件是通过yacc工具自动生成的,也就是我们在bee/Makefile文件增加以下内容来生成parse.h和parse.c文件:

parse.c parse.h: parse.y
    $(YACC) -d parse.y
    mv y.tab.c parse.c
    mv y.tab.h parse.h
这时候bee模块的文件依赖关系图如图3所示:

图3 修改后的bee模块文件依赖关系图

这时候如果我们修改了parse.y文件,那么在工程根目录执行make命令时,先会对ant模块进行编译,然后再对bee模块进行编译。由于对ant模块进行编译时,parse.h文件的内容还没有被修改,因此该文件就认为是最新的,于是就不会重新生成main.o文件。接下来对bee模块进行编译时,就会重新生成parse.h和parse.c文件,并且重新生成parse.o文件。很遗憾,在重新生成prog文件时,使用的是过旧的main.o文件,于是就会得到不正确的prog文件。

产生这个问题的原因就在于ant模块缺乏对parse.h文件的掌控,也就是不知道parse.h文件是如何产生的。如果我们坚持使用这种递归Makefile的方法来编译工程,并且希望解决上述问题,那么有三种方法:

1. 修改工程目录的Makefile,使得它先对bee模块进行编译,再对ant模块进行编译。然而,这就相当于是要求我们需要明确地指定工程的每一个模块的编译顺序。在一个包含有非常多模块的工程里面,要确定每一个模块的编译顺序是相当困难的。而且当我们新增、修改或者删除模块之后,又需要重新确定各个模块的编译顺序。理想的做法是让make自动地帮我们决定各个模块的编译顺序,这也是我们使用make命令来编译工程的初衷。

2. 对工程的各个模块进行重复编译,也就是将工程目录的Makefile修改为以下的内容:

MODULES = ant bee
all:
    for dir in $(MODULES); do \
        (cd $$dir; $(MAKE) all); \
    done 
    for dir in $(MODULES); do \
        (cd $$dir; $(MAKE) all); \
    done
这里我们只对每一个模块进行了一次重复编译。然而,在一个复杂的工程中,有可能需要多次进行重复编译才能得到正确的编译结果。与前一种方法(do too little)相比,这里明显就是“do too much”。这会使得我们浪费CPU资源,并且拖慢整个工程的编译速度。

3. 修改ant/Makefile文件,增加如下所示的内容:

.PHONY: ../bee/parse.h
../bee/parse.h:
    cd ../bee; \
    make clean; \
    make all
这种方法在每次编译ant模块之前,都先对bee模块进行重新编译,无论它是否需要。与第2种方法相比,这种方法不用对工程的每一个模块都进行重新编译,但是它仍然是“do too much”,因为不管bee模块是否需要重新编译,它都会被重新编译。

由此可见,上述三种方法,要么是“do too little”,要么是“do too much”,它们虽然都能解决问题,但都不是理想的解决方案。如果我们进一步分析问题的根源,就会发现工程的文件依赖关系是属于工程级别的,也就是它们属于一个整体,而当我们将它们划分成模块级别的时候,就会造成各个模块只看到一部分的文件依赖关系,因此就不能得到正确的编译结果。

为了保持工程文件依赖关系的整体性,我们就必须使得整个工程只存在一个Makefile。当整个工程只存在一个Makefile时,我们就可以很容易地构造出如图4所示的文件依赖图:


图4 完整的文件依赖关系图

整个工程只有一个Makefile,听起来似乎是一件很疯狂的事情,因为这个Makefile可能会变得无比庞大和复杂。其实不用担心,我们可以按照模块来将这个Makefile划分成一个个Makefile片段(fragement),然后通过Makefile的include指令来将这些Makefile片段组装在一个Makefile中。与递归Makefile相比,每一个模块现在拥有的是一个Makefile片段,而不是一个Makefile文件。这正是Android编译系统的设计思想和原则,也就是说,我们平时所编写的Android.mk编译脚本都只不过是整个Android编译系统的一个Makefile片段。

明白了Android编译系统的设计思想和原则之后,我们就可以通过图5来观察一下Android编译系统的整体架构了:


图5 Android编译系统架构

在使用Android编译系统之前,我们需要打开一个shell进入到Android源码根目录中,并且在该shell中将build/envsetup.sh脚本文件source进来。脚本文件build/envsetup.sh被source到当前shell的过程中,会在vendor和device两个目录将厂商指定的envsetup.sh也source到当前shell当中,这样就可以获得厂商提供的产品配置信息。此外,脚本文件build/envsetup.sh还提供了以下几个重要的命令来帮助我们编译Android源码:

1. lunch

用来初始化编译环境,例如设置环境变量和指定目标产品型号。Lunch命令在执行的时候,主要做两件事情。第一件事情是设置TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_BUILD_TYPE和TARGET_BUILD_APPS等环境变量,用来指定目标产品类型和编译类型。第二件事情是通过make命令执行build/core/config.mk脚本,并且通过加载另外一个脚本build/core/dumpvar.mk打印出当前的编译环境配置信息。注意,build/core/config.mk和build/core/dumpvar.mk均为Makefile脚本,因此它们可以通过make命令来执行。另外,build/core/config.mk脚本还会加载一个名称为BoradConfig.mk的脚本以及build/core/envsetup.mk脚本来配置目标产品型号的相关信息。

2. m

相当于是在执行make命令。对整个Android源码进行编译。

3. mm

如果是在Android源码根目录下执行,那么就相当于是执行make命令对整个源码进行编译。如果是在Android源码根目录下的某一个子目录执行,那么就在会在从该子目录开始,一直往上一个目录直至到根目录,寻找是否存在一个Android.mk文件。如果存在的话,那么就通过make命令对该Android.mk文件描述的模块进行编译。

4. mmm

后面可以跟一个或者若干个目录。如果指定了多个目录,那么目录之间以空格分隔,并且每一个目录下都必须存在一个Android,mk文件。如果没有在目录后面通过冒号指定模块名称,那么在Android.mk文件中描述的所有模块都会被编译,否则只有指定的模块会被编译。如果需要同时指定多个模块,那么这些模块名称必须以逗号分隔。它的语法如下所示:

mmm <dir-1> <dir-2> ... <dir-N>[:module-1,module-2,...,module-M]
该命令会通过make命令来执行Android源码根目录下的Makefile文件,该Makefile文件又会将build/core/main.mk加载进来。文件build/core/main.mk在加载的过程中,还会加载以下几个主要的文件:

(1). build/core/config.mk

该文件根据lunch命令所配置的产品信息在build/target/board、vendor或者device目录中找到对应的BoradConfig.mk文件,以及通过加载build/core/product_config.mk文件在build/target/product、vendor或者device目录中找到对应的AndroidProducts.mk文件,来进一步对编译环境进行配置,以便接下来编译指定模块时可以获得必要的信息。

(2). build/core/definitions.mk

该文件定义了在编译过程需要调用到的各种自定义函数。

(3). 指定的Android.mk

这些指定的Android.mk环境是由mmm命令通过环境变量ONE_SHOT_MAKEFILE传递给build/core/main.mk文件使用的。这些Android.mk文件一般还会通过环境变量BUILD_PACKAGE、BUILD_JAVA_LIBRARY、BUILD_STATIC_JAVA_LIBRARY、BUILD_SHARED_LIBRARY、BUILD_STATIC_LIBRARY、BUILD_EXECUTABLE和BUILD_PREBUILT将build/core/package.mk、build/core/java_library.mk、build/core/static_java_library.mk、build/core/shared_library.mk、build/core/static_library.mk、build/core/executable.mk和build/core/prebuilt.mk等编译片段模板文件加载进来,来表示要编译是APK、Java库、Linux动态库/静态库/可执行文件或者预先编译好的文件等等。

(4). build/core/Makefile

该文件包含了用来制作system.img、ramdisk.img、boot.img和recovery.img等镜像文件的脚本。

在接下来的一系列文章中,我们将按照以下情景来进一步分析Android的编译系统:

1. Android编译系统的环境初始化过程

2. 模块编译命令mmm的执行过程

3. Android镜像文件的制作过程

希望通过这三个情景的分析,使得我们对Android的编译系统有一个深刻的认识,以提高我们的Android系统开发效率,敬请关注!更多信息可以关注老罗的新浪微博:http://weibo.com/shengyangluo

分享到:
评论

相关推荐

    android系统原理及开发要点详解

     第2章“Android系统开发综述”,介绍Android系统开发的综述性内容,包括工具使用、获得代码、编译系统、仿真器运行、SDK使用等。  第3章“Android的Linux内核与驱动程序”,介绍Android内核的特点、Android中使用...

    如何在Android下编译C程序

    Android.mk 文件本身是比较简单的,不过它并不是我们熟悉的 Makefile,而是经过了 Android 自身编译系统的很多处理,因此要真正理清楚其中的联系还比较复杂,不过这种方式的好处在于,编写一个新的 Android.mk 来给 ...

    基于android,移植libxml2和libiconv

    最近一段时间,在android上想用c++处理xml数据,在网上找了半天资料,大多数教程都是在编译android系统的时候顺便把libxml2和libiconv源码放到系统指定路径下编译出来。可是没干过这样的事,也不想这么做。最后还是...

    android系统原理及开发要点详解_韩超_梁泉 4

     第2章“Android系统开发综述”,介绍Android系统开发的综述性内容,包括工具使用、获得代码、编译系统、仿真器运行、SDK使用等。  第3章“Android的Linux内核与驱动程序”,介绍Android内核的特点、Android中使用...

    图解Google Android内核编译教程

    图解Google Android内核编译教程 Android作为Google公司推出的一款手机开发平台,其本身...本文结合Android的开发文档以及本人的实践经验,简单介绍了Android内核的编译过程,希望有助于对内核移植感兴趣的开发人员。

    Android底层开发实战

    在简要介绍系统底层开发流程的基础上,首先分析了主流的Zynq和pcDuino平台上开发环境的搭建、Linux内核以及Android系统的编译、下载;然后结合前文学习过的知识点,从零开始设计LED显示系统的Linux内核驱动、Android...

    Android操作系统移植及应用研究

     其次,介绍了Devkit8000开发板的硬件资源,采用了先将Linux内核移植到Devkit8000开发板上,然后制作Android内核补丁打到Linux内核上的方式,把Android系统移植到了Devkit8000开发板上,具体工作包括制作交叉编译工具链...

    android系统原理及开发要点详解_韩超_梁泉 1

     第2章“Android系统开发综述”,介绍Android系统开发的综述性内容,包括工具使用、获得代码、编译系统、仿真器运行、SDK使用等。  第3章“Android的Linux内核与驱动程序”,介绍Android内核的特点、Android中使用...

    Google Android操作系统内核编译图文教程

    和标准的Linux开发流程一样,Android平台开发的一个很重要的基础工作就是对其内核的编译和...本文结合Android的开发文档以及本人的实践经验,简单介绍了Android内核的编译过程,希望有助于对内核移植感兴趣的开发人员

    Android内核编译

    Android系统的内核编译操作过程,简单的文档描述

    Android apk文件反编译工具

    Android系统 apk文件的反编译,超简单,有详细步骤!

    《Android应用开发实战》配套源码

     全面介绍了Android的系统架构、开发环境的搭建、Android应用程序的常用组件,以及一个简单的微博客户端的实现方法,为接下来动手实现本书中的完整案例(新浪微博客户端)奠定了基础;第二部分实例篇:介绍了微博...

    ARM Cortex-A8和Android 4.x联动报警系统

    第二部分内容,先简单讲解Android系统移植相关原理,然后一步步手把手教大家如何进行Linux内核移植、Android源码编译、以及Android到Cortex A8开发板的移植;第三部分内容,先教大家如何搭建裸机开发环境,然后带领...

    新版Android开发教程.rar

    Android 是一个专门针对移动设备的软件集,它包括一个操作系统,中间件和一些重要的应用程序。 Beta 版 的 Android SDK 提供了在 Android 平台上使用 JaVa 语言进行 Android 应用开发必须的工具和 API 接口。 特性 ...

    基于ARM Cortex-A8和Android 4.x的联动报警系统 (Android 、A8、Linux、驱动、NDK)

    第二部分内容,先简单讲解Android系统移植相关原理,然后一步步手把手教大家如何进行Linux内核移植、Android源码编译、以及Android到Cortex A8开发板的移植;第三部分内容,先教大家如何搭建裸机开发环境,然后带领...

    Qualcomm平台android开发总结

    1、 高通平台android开发总结. 7 1.1 搭建高通平台环境开发环境. 7 1.2 搭建高通平台环境开发环境. 7 1.2.1 高通android智能... 19 1.3 高通平台,android和 modem 编译流程分析. 21 1.3.1 android代码编译流程分析. 21

    利用计算机视觉库的Android平台系统道路识别

    利用计算机视觉库OpenCV和Android NDK编译技术在Android平台上实现道路识别的处理过程。首先简要介绍了开源计算机视觉库OpenCV及其移植到Android平台上的方法,该方法使Android平台的应用更加广泛,能够更好地实现...

    Android应用源码基于安卓的校园二手交易系统

    Android应用源码基于安卓的校园二手交易系统客户端+服务端+数据库源码免费下载。本项目是一个基于安卓和javaweb的校园二手交易系统, 包括整套安卓客户端、javaweb服务端、mysql数据库,可以进行基本的列表显示帖子...

    深入理解Android:卷I--详细书签版

     Android系统开发工程师常常需要深入理解系统的运转过程,而本书所涉及的内容可能正是他们在工作和学习中最想了解的。那些对具体模块(如Audio系统和Surface系统)感兴趣的读者 也可以直接阅读相关章节的内容。 ...

Global site tag (gtag.js) - Google Analytics