Android編譯系統簡要介紹和學習計劃

在Android源碼環境中,我們開發好一個模塊後,再寫一個Android.mk文件,就可通過m/mm/mmm/make等命令進行編譯。此外,通過make命令還可製作各種系統鏡像文件,例如system.img、boot.img和recovery.img等。這一切都得益於Android編譯系統,它爲我們處理了各種依賴關係,以及提供各種有用工具。本文對Android編譯系統進行簡單介紹以及制定學習計劃。

在研究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鏡像文件的製作過程;

發佈了64 篇原創文章 · 獲贊 177 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章