1. 程式人生 > >深入理解Android Build系統

深入理解Android Build系統

概述

Android Build 系統是用來編譯 Android 系統、Android SDK 以及相關文件的一套框架。在Android系統中,Android 的原始碼中包含了許許多多的模組。 不同產商的不同裝置對於 Android 系統的定製都是不一樣的。如何將這些模組統一管理起來,如何能夠在不同的作業系統上進行編譯,如何在編譯時能夠支援面向不同的硬體裝置,不同的編譯型別,且還要提供面向各個產商的定製擴充套件,Android系統如何解決這些問題呢?這就是我們不得不談的Android Build 系統。

Android原始碼目錄結構:
這裡寫圖片描述

Linux系統的make命令

在講解Android編譯系統之前,我們首先需要了解Linux系統的make命令。在Linux系統中,我們可以通過make命令來編譯程式碼。Make命令在執行的時候,預設會在當前目錄找到一個Makefile檔案,然後根據Makefile檔案中的指令來對程式碼進行編譯。如gcc,Linux系統中的shell命令cp、rm等等。

看到這裡,有的小夥伴可能會說,在Linux系統中,shell和make命令有什麼區別呢?
make命令事實也是通過shell命令來完成任務的,但是它的神奇之處是可以幫我們處理好檔案之間的依賴關係。例如有一個檔案T,它依賴於另外一個檔案D,要求只有當檔案D的內容發生變化,才重新生成檔案T。

Make命令是怎麼知道兩個檔案之間存在依賴關係,以及當被依賴檔案發生變化時如何處理目標檔案的呢?答案就在前面提到的Makefile檔案。Makefile檔案實際上是一個指令碼檔案,就像普通的shell指令碼檔案一樣,只不過它遵循的是Makefile語法。Makefile檔案最基礎的功能就是描述檔案之間的依賴關係,以及怎麼處理這些依賴關係。

Android Build簡介

Android Build 系統是 Android 系統的一部分,主要用來編譯 Android 系統,Android SDK 以及相關文件。該系統主要由 Make 檔案,Shell 指令碼以及 Python 指令碼組成。
Android build分類:

  • build/core 目錄下的檔案,這是Android Build的系統框架核心;
  • device目錄下的檔案,存放的是具體的產品配置檔案;
  • 各個模組的編譯檔案:Android.mk,位於模組的原檔案目錄下。

Android Build系統核心

Android Build系統核心在目錄build/core,這個目錄中有mk檔案、shell指令碼和per指令碼,他們構成Android Build系統的基礎和架構。

在核心的buil/core裡,系統主要乾了三件事情:
這裡寫圖片描述

常用命令:

source build/envsetup.sh
lunch
make

envsetup.sh

而在build/envsetup.sh中主要完成了三件事:
這裡寫圖片描述

執行Android系統的編譯,必須先執行envsetup.sh指令碼,這個指令碼會建立Android的編譯環境。其具體執行的是建立shell命令以及呼叫add_lunch_combo命令,這個命令的將呼叫該命令的所傳遞的引數存放到一個全域性的陣列變數LUNCH_MENU_CHOICES中。

envsetup.sh指令碼中定義的常用shell命令:

命令 說明
contact-button 指定當前編譯的產品
croot 快速切換到原始碼的根目錄,方便開始編譯
m 編譯整個原始碼,但不用將當前的目錄切換到原始碼的根目錄
mm 編譯當前目錄下的所有模組,但是不編譯他們的依賴項
mm 編譯當前目錄下的所有模組,但是不編譯他們的依賴項
cgrep 對系統中所有的C/C++檔案執行grep命令
sgrep 對系統中所有的原始檔執行grep命令

編譯 Android 系統

Android 系統的編譯環境目前只支援 Ubuntu 以及 Mac OS 兩種作業系統。在編譯Android系統之前我們需要先獲取完整的 Android 原始碼。開啟控制檯之後轉到 Android 原始碼的根目錄,然後執行如下命名:

source build/envsetup.sh 
lunch full-eng 
make -j8

關於這幾條命令的意思,我們上面提過。
第一步命令“source build/envsetup.sh”引入了 build/envsetup.sh指令碼,該指令碼的作用是初始化編譯環境,並引入一些輔助的 Shell 函式;

第二步命令“lunch full-eng”是呼叫 lunch 函式,並指定引數為“full-eng”。lunch 函式的引數用來指定此次編譯的目標裝置以及編譯型別。

第三部命令“make -j8”才真正開始執行編譯。make 的引數“-j”指定了同時編譯的 Job 數量,這是個整數,該值通常是編譯主機 CPU 支援的併發執行緒總數的 1 倍或 2 倍(例如:在一個 4 核,每個核支援兩個執行緒的 CPU 上,可以使用 make -j8 或 make -j16)。
完整的編譯時間依賴於編譯主機的配置。

Build 結果

所有的編譯產物都將位於 /out 目錄下,該目錄下主要包含:

  • /out/host/:該目錄下包含了針對主機的 Android 開發工具的產物。即 SDK 中的各種工具,例如:emulator,adb,aapt 等。
  • /out/target/common/:該目錄下包含了針對裝置的共通的編譯產物,主要是 Java 應用程式碼和 Java 庫。
  • /out/target/product//:包含了針對特定裝置的編譯結果以及平臺相關的 C/C++ 庫和二進位制檔案。其中,是具體目標裝置的名稱。
  • /out/dist/:包含了為多種分發而準備的包,通過“make disttarget”將檔案拷貝到該目錄,預設的編譯目標不會產生該目錄。

Build 生成的映象檔案

Build 的產物中最重要的是三個映象檔案,它們都位於 /out/target/product// 目錄下:

  • system.img:包含了 Android OS 的系統檔案,庫,可執行檔案以及預置的應用程式,將被掛載為根分割槽。
  • ramdisk.img:在啟動時將被 Linux 核心掛載為只讀分割槽,它包含了 /init檔案和一些配置檔案。它用來掛載其他系統映象並啟動 init 程序。
  • userdata.img:將被掛載為 /data,包含了應用程式相關的資料以及和使用者相關的資料。

Make 檔案

整個 Build 系統的入口檔案是原始碼樹根目錄下名稱為“Makefile”的檔案,當在原始碼根目錄上呼叫 make 命令時,make 命令首先將讀取該檔案。
Makefile 檔案的內容只有一行:“include build/core/main.mk”。該行程式碼的作用很明顯:包含 build/core/main.mk 檔案。在 main.mk 檔案中又會包含其他的檔案,其他檔案中又會包含更多的檔案,這樣就引入了整個 Build 系統。

在整個Build系統中,Make 檔案間的關係是相當複雜的。看一張make檔案主要的關係圖:
這裡寫圖片描述

Make 常用檔案:

檔名 說明
main.mk 主要的 Make 檔案,該檔案中首先將對編譯環境進行檢查,同時引入其他的 Make 檔案。另外,該檔案中還定義了幾個最主要的 Make 目標,例如 droid,sdk,等(參見後文“Make 目標說明”)。
help.mk 含了名稱為 help 的 Make 目標的定義,該目標將列出主要的 Make 目標及其說明。
envsetup.mk 配置 Build 系統需要的環境變數,例如:TARGET_PRODUCT,TARGET_BUILD_VARIANT,HOST_OS,HOST_ARCH 等。 當前編譯的主機平臺資訊(例如作業系統,CPU 型別等資訊)就是在這個檔案中確定的。 另外,該檔案中還指定了各種編譯結果的輸出路徑。
pathmap.mk 將許多標頭檔案的路徑通過名值對的方式定義為對映表,並提供 include-path-for 函式來獲取。例如,通過 $(call include-path-for, frameworks-native)便可以獲取到 framework 原生代碼需要的標頭檔案路徑。
combo/select.mk 根據當前編譯器的平臺選擇平臺相關的 Make 檔案。
dumpvar.mk 在 Build 開始之前,顯示此次 Build 的配置資訊。
config.mk 整個 Build 系統的配置檔案,最重要的 Make 檔案之一。該檔案中主要包含以下內容: 定義了許多的常量來負責不同型別模組的編譯。 定義編譯器引數以及常見檔案字尾,例如 .zip,.jar.apk。 根據 BoardConfig.mk 檔案,配置產品相關的引數。 設定一些常用工具的路徑,例如 flex,e2fsck,dx。
definitions.mk 最重要的 Make 檔案之一,在其中定義了大量的函式。這些函式都是 Build 系統的其他檔案將用到的。例如:my-dir,all-subdir-makefiles,find-subdir-files,sign-package 等,關於這些函式的說明請參見每個函式的程式碼註釋。
distdir.mk 針對 dist 目標的定義。dist 目標用來拷貝檔案到指定路徑
dex_preopt.mk 針對啟動 jar 包的預先優化。
pdk_config.mk 顧名思義,針對 pdk(Platform Developement Kit)的配置檔案。
post_clean.mk 在前一次 Build 的基礎上檢查當前 Build 的配置,並執行必要清理工作。
legacy_prebuilts.mk 該檔案中只定義了 GRANDFATHERED_ALL_PREBUILT 變數。
Makefile 被 main.mk 包含,該檔案中的內容是輔助 main.mk 的一些額外內容。
Android 原始碼中包含了許多的模組,模組的型別有很多種,例如:Java 庫,C/C++ 庫,APK 應用,以及可執行檔案等 。並且,Java 或者 C/C++ 庫還可以分為靜態的或者動態的,庫或可執行檔案既可能是針對裝置(本文的“裝置”指的是 Android 系統將被安裝的裝置,例如某個型號的手機或平板)的也可能是針對主機(本文的“主機”指的是開發 Android 系統的機器,例如裝有 Ubuntu 作業系統的 PC 機或裝有 MacOS 的 iMac 或 Macbook)的。不同型別的模組的編譯步驟和方法是不一樣,為了能夠一致且方便的執行各種型別模組的編譯,在 config.mk 中定義了許多的常量,這其中的每個常量描述了一種型別模組的編譯方式。常見的有: BUILD_HOST_STATIC_LIBRARY BUILD_HOST_SHARED_LIBRARY BUILD_STATIC_LIBRARY BUILD_SHARED_LIBRARY BUILD_EXECUTABLE BUILD_HOST_EXECUTABLE BUILD_PACKAGE BUILD_PREBUILT BUILD_MULTI_PREBUILT BUILD_HOST_PREBUILT BUILD_JAVA_LIBRARY BUILD_STATIC_JAVA_LIBRARY BUILD_HOST_JAVA_LIBRARY 不同型別的模組的編譯過程會有一些相同的步驟,例如:編譯一個 Java 庫和編譯一個 APK 檔案都需要定義如何編譯 Java 檔案。為了減少程式碼冗餘,需要將共同的程式碼複用起來,複用的方式是將共同程式碼放到專門的檔案中,然後在其他檔案中包含這些檔案的方式來實現的。 模組的編譯方式定義檔案的包含關係: ![這裡寫圖片描述](https://img-blog.csdn.net/20170402232734662?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveGlhbmd6aGlob25nOA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) ## Make 編譯映象 ### make /make droid 如果在原始碼樹的根目錄直接呼叫“make”命令而不指定任何目標,則會選擇預設目標:“droid”(在 main.mk 中定義)。因此,這和執行“make droid”效果是一樣的。 droid 目標將編譯出整個系統的映象。從原始碼到編譯出系統映象,整個編譯過程非常複雜。這個過程並不是在 droid 一個目標中定義的,而是 droid 目標會依賴許多其他的目標,這些目標的互相配合導致了整個系統的編譯。 那麼需要編譯出系統映象,需要哪些依賴呢? ![這裡寫圖片描述](https://img-blog.csdn.net/20170402233525703?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQveGlhbmd6aGlob25nOA==/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast) droid 所依賴的其他 Make目標說明:
名稱 說明
apps_only 該目標將編譯出當前配置下不包含 user,userdebug,eng 標籤(關於標籤,請參見後文“新增新的模組”)的應用程式。
droidcore 該目標僅僅是所依賴的幾個目標的組合,其本身不做更多的處理。
dist_files 該目標用來拷貝檔案到 /out/dist 目錄。
files 該目標僅僅是所依賴的幾個目標的組合,其本身不做更多的處理
prebuilt 該目標依賴於 (ALLPREBUILT)(ALL_PREBUILT)的作用就是處理所有已編譯好的檔案。
$(modules_to_install) modules_to_install 變數包含了當前配置下所有會被安裝的模組(一個模組是否會被安裝依賴於該產品的配置檔案,模組的標籤等資訊),因此該目標將導致所有會被安裝的模組的編譯。
$(modules_to_check) 該目標用來確保我們定義的構建模組是沒有冗餘的。
$(INSTALLED_ANDROID_INFO_TXT_TARGET) 該目標會生成一個關於當前 Build 配置的裝置資訊的檔案,該檔案的生成路徑是:out/target/product//android-info.txt
systemimage 生成 system.img。

Build 系統中包含的其他一些 Make 目標:

Make目標說明 說明
make clean 執行清理,等同於:rm -rf out/
make sdk 編譯出 Android 的 SDK
Make目標說明 說明
make clean-sdk 清理 SDK 的編譯產物
make update-api 更新 API。在 framework API 改動之後,需要首先執行該命令來更新 API,公開的 API 記錄在 frameworks/base/api 目錄下。
make dist 執行 Build,並將 MAKECMDGOALS 變數定義的輸出檔案拷貝到 /out/dist 目錄
make all 編譯所有內容,不管當前產品的定義中是否會包含
make help 幫助資訊
make snod 從已經編譯出的包快速重建系統映象
make libandroid_runtime 編譯所有 JNI framework 內容
makeframework 編譯所有 Java framework 內容
makeservices 編譯系統服務和相關內容
make 編譯一個指定的模組,local_target 為模組的名稱
make clean- 清理一個指定模組的編譯結果
makedump-products 顯示所有產品的編譯配置資訊,例如:產品名,產品支援的地區語言,產品中會包含的模組等資訊
makePRODUCT-xxx-yyy 編譯某個指定的產品
makebootimage 生成 boot.img
# 定製 Build 系統中內容 當我們要開發一款新的 Android 產品的時候,我們首先就需要在 Build 系統中新增對於該產品的定義。在 Android Build 系統中對產品定義的檔案通常位於 device 目錄下,device 目錄下可以公司名以及產品名分為二級目錄,然後加入到系統中,如以前小米等基於Android深度定製的系統。 通常,對於一個產品的定義通常至少會包括四個檔案:AndroidProducts.mk,產品版本定義檔案,BoardConfig.mk 以及 verndorsetup.sh。 ## AndroidProducts.mk 該檔案只需要定義一個變數,名稱為“PRODUCT_MAKEFILES”。
PRODUCT_MAKEFILES := \ 
 $(LOCAL_DIR)/full_stingray.mk \ 
 $(LOCAL_DIR)/stingray_emu.mk \ 
 $(LOCAL_DIR)/generic_stingray.mk
## 產品版本定義檔案 該檔案中包含了對於特定產品版本的定義。該檔案可能不只一個,因為同一個產品可能會有多種版本。 通常情況下,我們並不需要定義所有這些變數。Build 系統的已經預先定義好了一些組合,它們都位於 /build/target/product 下,每個檔案定義了一個組合,我們只要繼承這些預置的定義,然後再覆蓋自己想要的變數定義即可。
# 繼承 full_base.mk 檔案中的定義
 $(call inherit-product, $(SRC_TARGET_DIR)/product/full_base.mk) 
 # 覆蓋其中已經定義的一些變數
 PRODUCT_NAME := full_lt26 
 PRODUCT_DEVICE := lt26 
 PRODUCT_BRAND := Android 
 PRODUCT_MODEL := Full Android on LT26
## BoardConfig.mk 該檔案用來配置硬體主機板,它其中定義的都是裝置底層的硬體特性。例如:該裝置的主機板相關資訊,Wifi 相關資訊,還有 bootloader,核心,radioimage 等資訊。 ## vendorsetup.sh 該檔案中作用是通過 add_lunch_combo 函式在 lunch 函式中新增一個選單選項。該函式的引數是產品名稱加上編譯型別,中間以“-”連線,例如:add_lunch_combo full_lt26-userdebug。/build/envsetup.sh 會掃描所有 device 和 vender 二 級目 錄下的名稱 為”vendorsetup.sh”檔案,並根據其中的內容來確定 lunch 函式的 選單選項。 在配置了以上的檔案之後,便可以編譯出我們新新增的裝置的系統映象了。 我們可以使用命令:
source build/envsetup.sh
來檢視Build 系統已經引入了剛剛新增的 vendorsetup.sh 檔案。 ## 新增新模組 在原始碼樹中,一個模組的所有檔案通常都位於同一個資料夾中。為了將當前模組新增到整個 Build 系統中,每個模組都需要一個專門的 Make 檔案,該檔案的名稱為“Android.mk”。Build 系統會掃描名稱為“Android.mk”的檔案,並根據該檔案中內容編譯出相應的產物。 注: 在 Android Build 系統中,編譯是以模組(而不是檔案)作為單位的,每個模組都有一個唯一的名稱,一個模組的依賴物件只能是另外一個模組,而不能是其他型別的物件。對於已經編譯好的二進位制庫,如果要用來被當作是依賴物件,那麼應當將這些已經編譯好的庫作為單獨的模組。對於這些已經編譯好的庫使用 BUILD_PREBUILT 或 BUILD_MULTI_PREBUILT。例如:當編譯某個 Java 庫需要依賴一些 Jar 包時,並不能直接指定 Jar 包的路徑作為依賴,而必須首先將這些 Jar 包定義為一個模組,然後在編譯 Java 庫的時候通過模組的名稱來依賴這些 Jar 包。 那麼怎麼編寫Android.mk 檔案呢? Android.mk 檔案通常以以下兩行程式碼作為開頭:
LOCAL_PATH := $(call my-dir) //設定當前模組的編譯路徑為當前資料夾路徑
 include $(CLEAR_VARS)//清理編譯環境中用到的變數
為了方便模組的編譯,Build 系統設定了很多的編譯環境變數。要編譯一個模組,只要在編譯之前根據需要設定這些變數然後執行編譯即可。常見的如: - 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是預設標籤。標籤是提供給編譯型別使用的,不同的編譯型別會安裝包含不同標籤的模組。 編譯型別說明:
名稱 說明
eng 預設型別,該編譯型別適用於開發階段。 當選擇這種型別時,編譯結果將: 安裝包含 eng, debug, user,development 標籤的模組 安裝所有沒有標籤的非 APK 模組 安裝所有產品定義檔案中指定的 APK 模組
user 該編譯型別適合用於最終釋出階段。 當選擇這種型別時,編譯結果將: 安裝所有帶有 user 標籤的模組 安裝所有沒有標籤的非 APK 模組 安裝所有產品定義檔案中指定的 APK 模組,APK 模組的標籤將被忽略
userdebug 該編譯型別適合用於 debug 階段。 該型別和 user 一樣,除了: 會安裝包含 debug 標籤的模組 編譯出的系統具有 root 訪問許可權

根據上表各種型別模組的編譯方式,要執行編譯,只需要引入表 3 中對應的 Make 檔案即可。例如,要編譯一個 APK 檔案,只需要在 Android.mk 檔案中,加入“include $(BUILD_PACKAGE)。
除此以外,Build 系統中還定義了一些便捷的函式以便在 Android.mk 中使用,包括:

  • $(call my-dir):獲取當前資料夾路徑。
  • $(call all-java-files-under, ):獲取指定目錄下的所有 Java 檔案。
  • $(call all-c-files-under, ):獲取指定目錄下的所有 C 語言檔案。
  • $(call all-Iaidl-files-under, ) :獲取指定目錄下的所有 AIDL 檔案。
  • $(call all-makefiles-under, ):獲取指定目錄下的所有 Make 檔案。
  • $(call intermediates-dir-for, , , ,
  LOCAL_PATH := $(call my-dir) 
  include $(CLEAR_VARS) 
  # 獲取所有子目錄中的 Java 檔案
  LOCAL_SRC_FILES := $(call all-subdir-java-files)             
  # 當前模組依賴的靜態 Java 庫,如果有多個以空格分隔
  LOCAL_STATIC_JAVA_LIBRARIES := static-library 
  # 當前模組的名稱
  LOCAL_PACKAGE_NAME := LocalPackage 
  # 編譯 APK 檔案
  include $(BUILD_PACKAGE)

編譯一個 Java 的靜態庫:

LOCAL_PATH := $(call my-dir) 
  include $(CLEAR_VARS) 

  # 獲取所有子目錄中的 Java 檔案
  LOCAL_SRC_FILES := $(call all-subdir-java-files) 

  # 當前模組依賴的動態 Java 庫名稱
  LOCAL_JAVA_LIBRARIES := android.test.runner 

  # 當前模組的名稱
  LOCAL_MODULE := sample 

  # 將當前模組編譯成一個靜態的 Java 庫
  include $(BUILD_STATIC_JAVA_LIBRARY)