1. 程式人生 > >當我們談Android編譯系統的時候,我們在幹嗎?

當我們談Android編譯系統的時候,我們在幹嗎?

本文的目的是用比較容易理解的方式,介紹一下整個Android專案的編譯。至少知道大概的編譯流程是怎麼樣的,專案裡面的Android.mk檔案包含些什麼內容。

makefile的作用

makefile檔案用來描述檔案之間的依賴關係,並描述檔案的編譯規則。我們知道從原始碼到可執行程式,中間要經歷編譯生成中間檔案(windows裡面的obj檔案,Linux裡面的.o檔案),連結這些中間檔案生成可執行檔案的過程。

一個最簡單的makefile檔案為:


main.o : main.c defs.h

    cc -c main.c

main.o依賴main.c和defs.h兩個檔案,使用命令cc來編譯main.c最終生成main.o這個中間檔案。

當然我們在Android中極少看到這種形式的makefile檔案。我想講的重點是,Android中的mk檔案會定義一些全域性變數,描述模組編譯的先後順序,宣告模組編譯依賴的其他模組(包括一些三方庫)。整個Android專案是由很多模組組成,專案的編譯涉及到譬如Java原始碼的編譯,C原始碼的編譯,python原始碼的編譯等等。在make檔案中同樣也定義了這些編譯工具的路徑,這樣我們就可以呼叫這些工具來編譯不同種類的原始碼。

Android專案編譯系統

我們一般編譯專案會執行如下命令:


source build/envsetup.sh

lunch 3 #或者lunch full_eng 取決於你當前的專案配置情況
make -j8

第一條命令引入了shell指令碼envsetup.sh,在這個檔案頭的註釋裡面就描述清楚了該檔案的作用。我簡單翻譯如下:


將下面這些命令引入到編譯環境中:

- lunch:   lunch <product_name>-<build_variant>,指定編譯哪個目標裝置。

- tapas:   tapas [<App1> <App2> ...] [arm|x86|mips|armv5|arm64|x86_64|mips64] [eng|userdebug|user]

- croot:   切換工作目錄到專案根目錄.
- m: 在原始碼根目錄執行make命令. - mm: 僅編譯當前目錄下面的模組,但是不包括他們的依賴(比如某個模組依賴的模組再另一個地址,所依賴的模組就不會被編譯). - mmm: 同mm,不過是編譯該命令後面制定的目錄下的所有模組,同樣不包含模組的依賴。 To limit the modules being built use the syntax: mmm dir/:target1,target2. - mma: 編譯當前目錄下所有的模組和它們的依賴。 - mmma: 編譯製定目錄下所有的模組和它們的依賴。 - cgrep: 在所有的C/C++檔案中搜索,用法同grep。 - ggrep: 在所有的gradle檔案中搜索。 - jgrep: 在所有的Java檔案中搜索。 - resgrep: 在所有的res/*.xml資原始檔中搜索。 - mangrep: 在所有的AndroidManifest.xml資原始檔中搜索。 - sepgrep: 在所有的sepolicy檔案(字尾為te的檔案)中搜索。 - sgrep: Greps on all local source files. - godir: Go to the directory containing a file. - carrier: for open cmcc cu and others

上面的命令在執行完第一條命令之後都是可以使用的,在平常使用中可以節省大量的時間,特別是m/mm/mmm/mma/mmma編譯命令和grep的變種命令。

lunch執行載入某個專案的編譯環境和設定,最後執行make命令即可開始編譯。-j8的意思是開啟8個工作執行緒(j代表job),8這個數值可以根據自己的cpu核心來決定,一般一個核心可以開雙執行緒,所以如果是4核cpu,那麼就可以用8。執行make命令後,Android編譯系統會根據當前的編譯環境設定,以根目錄下面的Makefile檔案內容作為編譯啟動入口,執行編譯指令。包括編譯系統核心原始碼,編譯廠商自己新增的模組原始碼,編譯應用模組的原始碼(Android.mk),再將編譯結果連結在一起輸出,就是我們最後燒機的img檔案了。

編譯結果輸出到原始碼根目錄下面的out/資料夾。

  1. /out/host/:在Android編譯過程中要用到的各種主機工具,包括各種SDK tools:emulator,adb,aapt等。所謂主機,是相對你的目標工程機來說的,也就是你用來編譯這套程式碼的電腦。

  2. /out/target/common/:針對目標裝置的編譯結果,主要是JAVA應用程式碼和庫檔案。

  3. /out/target/product//:特定產品的編譯結果,裡面包含具體平臺的程式碼,比如C/C++庫和二進位制檔案。其實就是庫檔案和映象檔案。

  4. /out/dist/。這個比較少看到就不說了。參考文獻裡面有詳細的說明。

三種不同的make型別

如果太深入細節去看,我們就很容易暈頭,所以要分模組和分種類。譬如說建房子,我們從一塊塊磚頭來複述建房子過程,那鐵定是痛苦和漫長的過程。但是如果我們先搭建鋼筋混凝土框架,再把磚頭往框架裡面塞。這樣理解起來是不是很快。

上面講到Android編譯系統中make的三種類型,核心程式碼make,廠商專案特定make,應用模組原始碼Android.mk。

核心程式碼make

前文講到make的入口是根目錄的Makefile檔案,內容如下:


### DO NOT EDIT THIS FILE ###

include build/core/main.mk

### DO NOT EDIT THIS FILE ###

其實就是跳轉到了build/core/目錄下面的main.mk,這個目錄就是整個系統核心原始碼編譯的所有make檔案都在這個目錄了。包括廠商專案特定make和應用模組的make都會在編譯過程中被覆蓋到。這個檔案是頂重要的,描述了編譯的整個流程,串聯起了所有的make檔案。


subdir_makefiles := \

    $(shell build/tools/findleaves.py $(FIND_LEAVES_EXCLUDES) $(subdirs) Android.mk)

上面的程式碼摘自main.mk,可以看到其實所有的Android.mk檔案是使用python指令碼來搜尋的。而且main.mk裡面include了很多make檔案,設定了一些編譯變數,在分析Android.mk的時候,我們會分析其中一部分變數。

注意:所有make檔案裡面定義的變數都是在同一個名稱空間裡面,所以要考慮到重新命名和變數在使用前清空。這個就是為什麼Android.mk裡都包含include $(CLEAR_VARS)這個語句。同樣CLEAR_VARS這個變數對應的是”build/core/”資料夾下面的clear_vars.mk檔案。很多我們在Android.mk裡面看到的變數都是在main.mk裡面引用的make檔案裡面定義了。

下面是從參考文獻裡面引入的一張main.mk呼叫圖
這裡寫圖片描述

像一棵樹一張,串聯起了整個專案的編譯過程。這些mk檔案的作用,請仔細閱讀參考文獻2.

Android原始碼包含的模組也是分類的,比如Java庫,APK應用,主機庫,目標機器共享庫,可執行檔案等等。對於同一種類型的模組,不適用的編譯方法也是一樣的,所以Android將不同模組的編譯方法抽取出來,定義了下面的變數:

  1. BUILD_HOST_STATIC_LIBRARY/BUILD_HOST_SHARED_LIBRARY:編譯主機靜態/共享庫

  2. BUILD_STATIC_LIBRARY/BUILD_SHARED_LIBRARY:編譯目標機器靜態共享庫

  3. BUILD_EXECUTABLE/BUILD_HOST_EXECUTABLE:編譯目標機器可執行檔案/編譯主機可執行檔案

  4. BUILD_PACKAGE:編譯APK檔案

  5. BUILD_PREBUILT/BUILD_MULTI_PREBUILT:如何處理一個或多個已經編譯好的檔案(例如一個jar包)

  6. BUILD_HOST_PREBUILT:同上,針對主機。

  7. BUILD_JAVA_LIBRARY/ BUILD_STATIC_JAVA_LIBRARY:如何編譯裝置上的Java庫/如何編譯裝置上的靜態Java庫

  8. BUILD_HOST_JAVA_LIBRARY:如何編譯主機上的Java庫

這些變數都是在同目錄下面的config.mk檔案裡面定義的,這個mk檔案同樣在main.mk裡面被include了。以BUILD_PACKAGE為例,定義如下:


#config.mk

BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk

其實引入的是package.mk模組。這個跟我們上面說的針對不同模組,Android抽取了不同的編譯方法。所以針對APK包的編譯,Android使用的是package.mk進行處理,這個也是一種複用,防止出現冗餘的編譯程式碼。

廠商專案特定make

一般在同一套程式碼下面,不同的廠商會定義自家的多個產品。通常這些產品差異是硬體配置,而不同的硬體對應不同的驅動。所以不同的產品都需要有自己的make檔案來告訴編譯系統應該編譯哪些產品本身的檔案。一般都會在”/device“目錄下面定義廠商的資料夾,而這個資料夾下面會定義具體產品相關的資料夾,這個資料夾下面就放置的是具體產品相關的make檔案。device目錄結構如下:
這裡寫圖片描述

在這些mk檔案裡面,最重要的是AndroidProducts.mk和BoardConfig.mk檔案。一般產品只是有部分配置改變,所以其實這些make檔案可以繼承某個make檔案,僅僅修改差異的部分。AndroidProducts.mk其實定義瞭如下語句:


$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)

所以可以看到這個make檔案其實是繼承了core_64_bit.mk檔案的,這個也簡化了我們建立新專案的步驟。

應用原始碼Android.mk

Android編譯系統會將原始碼分成不同的模組。大家經常看到Android.mk檔案,這個檔案一般放在模組的根目錄下面,描述的是該模組如何編譯,依賴哪些模組,最後以什麼形式輸出。我們以Settings模組為例,介紹Android.mk裡面的元素和用法:


LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

LOCAL_PREBUILT_STATIC_JAVA_LIBRARIES :=\

        javalib.jar:libs/javalib.jar

include $(BUILD_MULTI_PREBUILT)

include $(CLEAR_VARS)



LOCAL_JAVA_LIBRARIES := bouncycastle conscrypt telephony-common ims-common audiopostprocess

LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4 android-support-v13 common_zxing jsr305 letv-domain-common

LOCAL_MODULE_TAGS := optional

res_dirs := cus_res res

LOCAL_RESOURCE_DIR := $(addprefix $(LOCAL_PATH)/, $(res_dirs))



LOCAL_SRC_FILES := \

        $(call all-java-files-under, src) 



LOCAL_PACKAGE_NAME := Settings

LOCAL_CERTIFICATE := platform

LOCAL_PRIVILEGED_MODULE := true

LOCAL_PROGUARD_FLAG_FILES := proguard.flags

ifneq ($(INCREMENTAL_BUILDS),)

    LOCAL_PROGUARD_ENABLED := disabled

endif

include frameworks/base/packages/SettingsLib/common.mk

include $(BUILD_PACKAGE)

一般開頭都會有兩行程式碼:


LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)

作用是:

  1. 將當前模組的根目錄賦值為一個變數LOCAL_PATH,方便後面引用。

  2. 我們前面說過,所有的mk檔案都是被編譯系統整合在一起的,所以所有的變數都是在同一個名稱空間裡面。CLEAR_VARS用於清楚一些變數的值,防止影響到本模組的編譯。從include語法看,這個變數應該指向的是某個mk檔案。

剩下的大部分都是變數定義,我們看看這些變數含義:

  1. LOCAL_SRC_FILES:當前模組包含的所有原始碼檔案。

  2. LOCAL_MODULE:當前模組的名稱,這個名稱應當是唯一的,模組間的依賴關係就是通過這個名稱來引用的。

  3. LOCAL_C_INCLUDES:C 或 C++ 語言需要的標頭檔案的路徑。

  4. LOCAL_STATIC_LIBRARIES:當前模組在靜態連結時需要的庫的名稱。

  5. LOCAL_SHARED_LIBRARIES:當前模組在執行時依賴的動態庫的名稱。

  6. LOCAL_CFLAGS:提供給 C/C++ 編譯器的額外編譯引數。

  7. LOCAL_JAVA_LIBRARIES:當前模組依賴的 Java 共享庫。

  8. LOCAL_STATIC_JAVA_LIBRARIES:當前模組依賴的 Java 靜態庫。

  9. LOCAL_PACKAGE_NAME:當前 APK 應用的名稱。

  10. LOCAL_CERTIFICATE:簽署當前應用的證書名稱。

  11. LOCAL_MODULE_TAGS:當前模組所包含的標籤,一個模組可以包含多個標籤。標籤的值可能是 debug, eng, user,development 或者 optional。其中,optional 是預設標籤。標籤是提供給編譯型別使用的。

這些都不是最關鍵的,僅僅只是變數賦值罷了,點睛之筆是最後的


include $(BUILD_PACKAGE)

這個變數我們上面提到過


BUILD_PACKAGE:編譯APK檔案

#config.mk

BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk

所以這個地方是引入package.mk檔案,而這個檔案會使用在這個Android.mk檔案裡面賦值的各種變數,編譯生成制定的目標。

參考文獻

  1. Makefile經典教程(掌握這些足夠)

  2. 特別推薦: 理解 Android Build 系統

  3. Android build system & Android.mk 規範

  4. IT SPA Club(注:需科學上網