當我們談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/資料夾。
/out/host/:在Android編譯過程中要用到的各種主機工具,包括各種SDK tools:emulator,adb,aapt等。所謂主機,是相對你的目標工程機來說的,也就是你用來編譯這套程式碼的電腦。
/out/target/common/:針對目標裝置的編譯結果,主要是JAVA應用程式碼和庫檔案。
/out/target/product//:特定產品的編譯結果,裡面包含具體平臺的程式碼,比如C/C++庫和二進位制檔案。其實就是庫檔案和映象檔案。
/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將不同模組的編譯方法抽取出來,定義了下面的變數:
BUILD_HOST_STATIC_LIBRARY/BUILD_HOST_SHARED_LIBRARY:編譯主機靜態/共享庫
BUILD_STATIC_LIBRARY/BUILD_SHARED_LIBRARY:編譯目標機器靜態共享庫
BUILD_EXECUTABLE/BUILD_HOST_EXECUTABLE:編譯目標機器可執行檔案/編譯主機可執行檔案
BUILD_PACKAGE:編譯APK檔案
BUILD_PREBUILT/BUILD_MULTI_PREBUILT:如何處理一個或多個已經編譯好的檔案(例如一個jar包)
BUILD_HOST_PREBUILT:同上,針對主機。
BUILD_JAVA_LIBRARY/ BUILD_STATIC_JAVA_LIBRARY:如何編譯裝置上的Java庫/如何編譯裝置上的靜態Java庫
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)
作用是:
將當前模組的根目錄賦值為一個變數LOCAL_PATH,方便後面引用。
我們前面說過,所有的mk檔案都是被編譯系統整合在一起的,所以所有的變數都是在同一個名稱空間裡面。CLEAR_VARS用於清楚一些變數的值,防止影響到本模組的編譯。從include語法看,這個變數應該指向的是某個mk檔案。
剩下的大部分都是變數定義,我們看看這些變數含義:
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 是預設標籤。標籤是提供給編譯型別使用的。
這些都不是最關鍵的,僅僅只是變數賦值罷了,點睛之筆是最後的
include $(BUILD_PACKAGE)
這個變數我們上面提到過
BUILD_PACKAGE:編譯APK檔案
#config.mk
BUILD_PACKAGE:= $(BUILD_SYSTEM)/package.mk
所以這個地方是引入package.mk檔案,而這個檔案會使用在這個Android.mk檔案裡面賦值的各種變數,編譯生成制定的目標。