做過Android平台開發的朋友對make,mm或make clean命令應該很熟悉,但也許大家只是熟知這些命令的作用卻不知道這些命令底下有些什么原理?那么今天我就帶着大家推開Android編譯系統的大門,探索一下這片未知的恐怖之森(問啥要用恐怖之森后面大家就知道了)。
Makefile入門
在講解Android編譯系統之前首先來了解一下什么是Makefile:
簡單的說,Makefile提供了一種機制,讓使用者可以有效的組織工作。
注意這里用的是“工作”而非“編譯”,這是因為Makefile並不是用來完成編譯工作的,它只是一種規則的執行者,而使用者用它來執行什么規則沒有任何限制。如既可以用它來編譯系統,也能用它備份文檔或只是打印log。
既然它是用來執行規則的,那么我們就來看看Makefile的規則。
TARGET:PREREQUISITES
COMMANDS
注意:COMMANDS前面必須有一個TAB制表符
在Makefile的規則中TARGET是需要生成的目標文件,PREREQUISITES是目標的先決條件,也是另一個規則中的target。COMMANDS是生成目標文件的命令,當prerequisites中的任何一個文件比target文件要新的時候就會觸發commands。commands的具體內容取決於使用者的需求,如調用gcc編譯器。
下面我們用一個簡單的例子說明一下Makefile規則的用法。
涉及到的文件如下:
| 文件名 | 描述 |
|---|---|
| mian.c | 主函數所在文件 |
| test.h | 提供一個測試函數getNumber的聲明 |
| test.c | 提供getNumber的函數實現,用於返回一個值 |
| Makefile | 我們的主角,用於編譯整個例子 |
文件內容如下:
(1)test.h對getNumber進行聲明。
int getNumber();
(2)test.c實現getNumber函數
#include "test.h"
int getNumber()
{
return 2333;
}
(3)main.c打印getNumber的返回值
#include <stdio.h>
#include "test.h"
int main()
{
printf("Hello,getNumber=%d\n",getNumber());
return 0;
}
(4)Makefile文件,用於組織這個小例子的編譯工作
MakefileTest : main.o test.o
gcc -o MakefileTest main.o test.o
main.o : main.c
gcc -c main.c
test.o : test.c
gcc -c test.c
對上面這段代碼簡單解釋一下,MakefileTest為TAEGET即目標產物,main.o和test.o是MakefileTest的先決條件。gcc -o MakefileTest main.o test.o則是MakefileTest對應的COMMANDS,這條命令使用gcc命令將main.o和test.o編譯成MakefileTest可執行文件。
最后通過使用make命令就會在當前目錄生成一個MakefileTest可執行文件,運行結果如下:

上面這個是一個非常簡單的Makefile的示例,但“麻雀雖小,五臟俱全”,它清晰的展示了一個Makefile的編寫過程。另外Make工具本身是非常強大的,它有很多隱含的規則來幫助開發者快速搭建復雜的編譯體系。如利用它的自動推導功能可以簡化對象間的依賴關系,還可以加入變量來減少重復輸入。
上面的Makefile還可以寫成這樣:
OBJECT = main.o test.o
MakefileTest : $(OBJECT)
gcc -o MakefileTest $(OBJECT)
關於Makefile的更多用法可以查看how to write makefile。希望大家多了解一下makefile的用法,這有助於理解Android的編譯系統。
Android編譯系統入門
像我這樣從事過Android平台開發的開發人員對make,mm等命令應該很熟悉,同樣也知道Android.mk文件,用它結合mm命令來編譯單個的Android模塊。如果這就是你對Android編譯系統的全部了解,那只能說你是一個普通的Android平台開發人員。想要在Android平台開發界混的話,學習和掌握Android編譯系統的原理將是必不可少的。前面說道Android編譯系統是一個恐怖之森,應為它真好符合恐怖之森的兩個特點,讓人望而卻步和容易迷失方向。這么說一點也不過分,在學習Android編譯系統的時候,如果缺少很明確的指引很容易迷失方向,或者讓你根本就不敢去試着闖一闖。
makefile依賴樹的概念
不難發現在makefile中的target的依賴關系實際上可以組成一棵樹,我將其稱為makefile依賴樹,仍然以上一個MakefileTest為例,給出它的makefile依賴樹:

只要照個這個樹形結構順藤摸瓜,那么分析Android編譯系統也就變得有目的性了。
恐怖之森里的樹
有了上面的知識做鋪墊,我們也就可以大膽的往恐怖之森闖了。

根節點
既然是樹,肯定有它的根節點,我們就來找一找Android編譯系統的根節點。
如果make命令沒有指定文件的話默認會在當前目錄尋找Makefile這個文件,所以先看看Android源碼根目錄的那個Makefile文件:
### DO NOT EDIT THIS FILE ###
include build/core/main.mk
### DO NOT EDIT THIS FILE ###
原來這里只是指路牌,真正的文件是build/core/main.mk。
在 Makefile 使用 include 關鍵字可以把別的 Makefile 包含進來,這很像 C 語言的
#include,被包含的文件會原模原樣的放在當前文件的包含位置。
打開main.mk發現它有上千行代碼,並且在其中又包含了很多其他makefile腳本,所以整個文件的內部結構讓人感覺雜亂無章,無法入手。這時候我們就不能一行行的去讀這個文件,這樣很可能會事倍功半。我們需要找出它其中依賴樹的根節點,以此為突破口。但往往大型的工程中不止一棵依賴樹,這時候如果沒有指定依賴樹的話make命令會以從上至下第一個target作為默認依賴樹的根節點。其實大家非常熟悉的make clean中的clean就是這些依賴樹中的一棵。
.PHONY: clean
clean:
@rm -rf $(OUT_DIR)/*
@echo "Entire build directory removed."
clean是一個偽目標,偽目標一般沒有目標文件。且使用
.PHONY顯示聲明。
從上面clean的COMMANDS知道它的作用是刪除$(OUT_DIR)下的所有目錄和文件,而$(OUT_DIR)就是out目錄。
OK,那么我們就跟着上面的思路找尋那棵默認依賴樹,對照main.mk代碼很快就發現了它的默認依賴樹根節點:
# This is the default target. It must be the first declared target.
.PHONY: droid
DEFAULT_GOAL := droid
$(DEFAULT_GOAL):
從注釋中可以看出來droid就是我們找的依賴樹的根節點,不過這里只是定義了一下,沒有給出真正的規則。根據關鍵字搜索的話會發現main.mk中有幾處對droid規則的定義:
ifneq ($(TARGET_BUILD_APPS),)
# If this build is just for apps, only build apps and not the full system by default.
...
.PHONY: apps_only
apps_only: $(unbundled_build_modules)
droid: apps_only
...
else # TARGET_BUILD_APPS
...
# Building a full system-- the default is to build droidcore
droid: droidcore dist_files
上面會根據TARGET_BUILD_APPS變量的值是否為空來走不同的分支,如果不為空,則droid的先決條件是apps_only,如果為空droid的先決條件是droidcore和dist_files,我們使用默認的make命令編譯android系統的話這里的TARGET_BUILD_APPS的值為空,所以走下面這個分支。TARGET_BUILD_APPS何時不為空,感興趣的朋友可以自行分析一下。
main.mk總覽
在分析droidcore和dist_files兩個先決條件之前先來看一下main.mk的一個文件結構,它除了構建droid等依賴樹外,有一大半內容是在做一下這些事情。
- 對編譯環境的檢測
比如java環境是否符合要求,當前是linux系統還是mac系統。如果這些檢測中有任何一項不符合要求,則會終止編譯。 - 進行一些必要的前期處理
比如整個項目工程是否要進行清理操作,部分工具的安裝等。 - 引用其他Makefile文件
比如引用config.mk,cleanbuild.mk等。 - 設置全局變量
- 各種函數的實現
Android編譯系統中定義了很多實用的函數,它們提供了整個編譯系統的統一解決方案。比如my-dir這個在Android.mk中出鏡率最高的函數,就是用來獲得當前的路徑。
下表是對Android編譯系統中涉及的主要Makefile文件的解釋,可供參考。
| Name | Description |
|---|---|
| main.mk | 整個編譯系統的主導文件 |
| config.mk | 產品配置的主導文件 |
| base_rules.mk | 編譯系統需要遵循的基礎規則定義 |
| build_id.mk | 版本id號的定義 |
| cleanbuild.mk | clean操作的定義 |
| clear_vars.mk | LOCAL開頭的相關系統變量 |
| definitions.mk | 提供了大量實用的函數定義 |
| envsetup.mk | 配置編譯時的環境變量 |
| executable.mk | 負責BUILD_EXECUTABLE的具體實現 |
| host_executable.mk | 負責BUILD_HOST_EXECUTABLE的具體實現 |
| host_static_library.mk | 負責BUILD_HOST_STATIC_LIBRARY的具體實現,其他類型的BUILD_XX這里不再贅述 |
| product_config.mk | 產品級別的配置,屬於config的一部分 |
| version_defaults.mk | 負責生成版本信息,如版本號BUILD_NUMBER := eng.\((USER).\)(shell date +%Y%m%d.%H%M%S) |
droidcore節點
droid規則的定義如下:
# Build files and then package it into the rom formats
.PHONY: droidcore
droidcore: files \
systemimage \
$(INSTALLED_BOOTIMAGE_TARGET) \
$(INSTALLED_RECOVERYIMAGE_TARGET) \
$(INSTALLED_USERDATAIMAGE_TARGET) \
$(INSTALLED_CACHEIMAGE_TARGET) \
$(INSTALLED_VENDORIMAGE_TARGET) \
$(INSTALLED_FILES_FILE)
可以看出來droidcore有如下幾個先決條件,
| Prerequisite | Description |
|---|---|
| files | 代表其所依賴的先決條件的集合,沒有實際意義 |
| systemimage | 將生成system.img |
| INSTALLED_BOOTIMAGE_TARGET | 將生成boot.img |
| INSTALLED_RECOVERYIMAGE_TARGET | 將生成recovery.img |
| INSTALLED_USERDATAIMAGE_TARGET | 將生成userdata.img |
| INSTALLED_CACHEIMAGE_TARGET | 將生成cache.img |
| INSTALLED_VENDORIMAGE_TARGET | 將生成vendor.img |
| INSTALLED_FILES_FILE | 將生成install-files.txt,用於記錄當前系統中預裝的程序、庫等模塊 |
這幾個先決條件的生成原理類似,這里挑幾個做重點分析。
1.files
定義如下:
# All the droid stuff, in directories
.PHONY: files
files: prebuilt \
$(modules_to_install) \
$(INSTALLED_ANDROID_INFO_TXT_TARGET)
-
prebuilt
"prebuilt"也是一個偽目標,它依賴於 $(ALL_PREBUILT),不過ALL_PREBUILT只用於早起的版本,目前已經廢棄,建議使用它的替代品PRODUCT_COPY_FILES. -
modules_to_install
描述了系統所需要安裝的模塊。
# TODO: Remove the 3 places in the tree that use ALL_DEFAULT_INSTALLED_MODULES
# and get rid of it from this list.
modules_to_install := $(sort \
$(ALL_DEFAULT_INSTALLED_MODULES) \
$(product_FILES) \
$(foreach tag,$(tags_to_install),$($(tag)_MODULES)) \
$(CUSTOM_MODULES) \
)
其中product_FILES用於編譯該產品涉及的相關文件,與product_FILES有直接聯系的是product_MODULES,而product_MODULES是基於PRODUCT_PACKAGES的處理結果。簡單說product_FILES是各個modules需要安裝的文件列表。
tags_to_install是指那些被指定編譯標志的modules。在選擇了不同的編譯標志,如user,debug,eng等后只有與之相關的模塊會被編譯進系統。
2.systemimage
systemimage的定義規則在Makefile中。
$(INSTALLED_SYSTEMIMAGE): $(BUILT_SYSTEMIMAGE) $(RECOVERY_FROM_BOOT_PATCH) | $(ACP)
@echo "Install system fs image: $@"
$(copy-file-to-target)
$(hide) $(call assert-max-image-size,$@ $(RECOVERY_FROM_BOOT_PATCH),$(BOARD_SYSTEMIMAGE_PARTITION_SIZE))
systemimage: $(INSTALLED_SYSTEMIMAGE)
可以看出來systemimage的先決條件為INSTALLED_SYSTEMIMAGE指代的內容,而后者的先決條件中BUILT_SYSTEMIMAGE是關鍵。
它的定義是
BUILT_SYSTEMIMAGE := $(systemimage_intermediates)/system.img
它的規則是
$(BUILT_SYSTEMIMAGE): $(FULL_SYSTEMIMAGE_DEPS) $(INSTALLED_FILES_FILE)
$(call build-systemimage-target,$@)
最后就是通過build-systemimage-target這個函數生成system.img
dist_files節點
根節點droid還有一個依賴是dist_files,它在整個編譯過程中就出現了一次,如下
# dist_files only for putting your library into the dist directory with a full build.
.PHONY: dist_files
可以看出來它是提供給我們用於存儲多種分發包的,通常情況下它不做任何工作,我們可已跳過它。
Android編譯系統入門的第一部分就寫到這里,以上內容參考《深入理解Android內核設計思想》。下一部分將分析Android.mk的編寫規則。
