Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android7.0 Ninja編譯原理

Android7.0 Ninja編譯原理

編輯:關於Android編程

引言

使在Android N的系統上,初次使用了Ninja的編譯系統。對於Ninja,最初的印象是用在了Chromium open source code的編譯中,在chromium的編譯環境中,使用ninja -C out/Default chrome命令,就可以利用源碼編譯出chrome的apk。對使用者而言,拋開對原理的探究,最直觀的印象莫過於可以清楚的看到自己當前編譯的進度。同時,對android而言,也可以感受到編譯速度的提升帶來的便捷。本文將深入分析Ninja的編譯原理,以及android上面的編譯改變。

正因為這個改變,所以在編譯android N的code的時候需要使用OpenJDK8

編譯系統的內存最少需要12G,建議16G,否則會出現JVM不足的錯誤。

8G內存的機器可以通過增大JVM默認值的方法來解決,但是經過測試,還是會偶爾出現JVM不足的錯誤

exportJAVA_OPTS='-Xmx4096M'

 

概念簡介

名詞:

Ninja

Blueprint

Soong

 

Ninja

Ninja是一個致力於速度的小型編譯系統(類似於Make);

如果把其他編譯系統比做高級語言的話,Ninja就是匯編語言

主要有兩個特點:

1、可以通過其他高級的編譯系統生成其輸入文件;

2、它的設計就是為了更快的編譯;

使用Kati把makefile轉換成Ninja files,然後用Ninja編譯

在不久的將來,當不再用Makefile(Android.mk)時,Kati將被去掉

ninja核心是由C/C++編寫的,同時有一部分輔助功能由python和shell實現。由於其開源性,所以可以利用ninja的開源代碼進行各種個性化的編譯定制。

Github地址: https://github.com/ninja-build/ninja

 

Blueprint, Soong

Blueprint和Soong是用於一起把Blueprint 文件轉換為Ninja文件。 將來需要寫Blueprint文件(Android.bp),轉換為Android.soong.mk(也可以直接寫),然後轉換為Ninja文件(build.ninja)然後用Ninja編譯。

如果Android.mk和Android.bp同時存在,Android.mk會被忽略。

如果Android.bp的同級目錄下有Android.soong.mk也會被include

 

1.ckati可執行文件的生成

在android系統中,目前還未完全切換到Ninja編譯,編譯的入口仍然是make命令, 如下commands以nexus為例:

source build/envsetup.sh

choosecombo

make -j4

在這邊可以看到,最終編譯使用的命令仍然是make.

既然是make,那就在編譯中首先include到的就是build/core/main.mk了,在main.mk中,我們可以清楚的看到對Ninja的調用:

relaunch_with_ninja :=

ifneq ($(USE_NINJA),false)

ifndef BUILDING_WITH_NINJA

relaunch_with_ninja := true

endif

endif

由於USE_NINJA默認沒有定義,所以一定會進入到這個選項中,並且將relaunch_with_ninja置為true。這樣的話,就會進入到下面的重要操作語句,去include ninja的makefile. 並且在out目錄下生成ninja_build的文件,顯示當前是使用了ninja的編譯系統。

ifeq ($(relaunch_with_ninja),true)

# Mark this is a ninjabuild.

$(shell mkdir -p $(OUT_DIR)&& touch $(OUT_DIR)/ninja_build)

includebuild/core/ninja.mk

else # !relaunch_with_ninja

ifndef BUILDING_WITH_NINJA

# Remove ninja build mark ifit exists.

$(shell rm -f $(OUT_DIR)/ninja_build)

endif

include build/core/ninja.mk的語句執行後,我們就可以看到真正定義ninja的地方了。由於前面簡介講了ninjia是基於開源項目編譯出來的輕便的編譯工具,所以這邊google肯定也對ninjia進行了修改,編譯,並且最終生成了一個可執行的應用程序。在simba6項目中,我們可以在prebuilts/ninja/linux-x86下面找到這個可執行的應用程序ninja。我們可以簡單的運行這個ninja的命令,比如ninja –h, 就可以了解到這個command的基本用法, 也可以看到本版本的ninja使用的base version為1.6.0。

./ninja -h

usage: ninja [options][targets...]

if targets are unspecified,builds the 'default' target (see manual).

options:

--versionprint ninja version ("1.6.0")

-C DIRchange to DIR before doing anything else

-f FILEspecify input build file [default=build.ninja]

-j Nrun N jobs in parallel [default=6, derived from CPUs available]

-k Nkeep going until N jobs fail [default=1]

-l Ndo not start new jobs if the load average is greater than N

-ndry run (don't run commands but act like they succeeded)

-vshow all command lines while building

-d MODEenable debugging (use -d list to list modes)

-t TOOLrun a subtool (use -t list to list subtools)

terminates toplevel options; further flagsare passed to the tool

-w FLAGadjust warnings (use -w list to list warnings)

---------------------------------------------------------------------------------------------------

 

在聲明了ninjia可執行程序的目錄之後,緊接著在mk中就提及了kati的定義。

KATI ?= $(HOST_OUT_EXECUTABLES)/ckati

目標KATI是利用源碼編譯生成的一個可執行的程序。源碼在build/kati文件夾中。這同樣是一個開源的代碼。

開源網站的地址為:

https://github.com/google/kati

我們可以clone下來最新的code,或者在源碼build/kati中直接按下面的步驟來編譯ckati的可執行程序。

一、添加軟件源

sudoadd-apt-repository ppa:ubuntu-toolchain-r/test

sudo apt-get update

二、安裝版本的命令:

sudo apt-get install gcc-4.8 g++-4.8

三、查看本地版本

四、切換版本

sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.660

sudo update-alternatives --install /usr/bin/gcc gcc/usr/bin/gcc-4.8 40

sudo update-alternatives --install /usr/bin/g++ g++/usr/bin/g++-4.6 60

sudo update-alternatives --install /usr/bin/g++ g++/usr/bin/g++-4.8 40

這裡的4.6是你本機之前的版本。

sudo update-alternatives --config gcc

sudo update-alternatives --config g++

選擇你需要的版4.8.

在選擇好了之後,執行make,即可開始編譯。編譯後會在根目錄生成ckati的可執行程序。

 

當然,android源碼是利用makefile來做的,我們可以看到ninja.mk中對ckati的makefile進行了調用。

文件地址:include build/kati/Makefile.ckati

在kati的makefile中,我們可以看到真正去編譯kati的過程。

# Rule to build ckati intoKATI_BIN_PATH

$(KATI_BIN_PATH)/ckati:$(KATI_CXX_OBJS) $(KATI_CXX_GENERATED_OBJS)

@mkdir -p $(dir $@)

$(KATI_LD) -std=c++11 $(KATI_CXXFLAGS) -o$@ $^ $(KATI_LIBS)



# Rule to build normal sourcefiles into object files in KATI_INTERMEDIATES_PATH

$(KATI_CXX_OBJS) :$(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_SRC_PATH)/%.cc

@mkdir -p $(dir $@)

$(KATI_CXX) -c -std=c++11 $(KATI_CXXFLAGS)-o $@ $<



# Rule to build generatedsource files into object files in KATI_INTERMEDIATES_PATH

$(KATI_CXX_GENERATED_OBJS):$(KATI_INTERMEDIATES_PATH)/%.o: $(KATI_INTERMEDIATES_PATH)/%.cc

@mkdir -p $(dir $@)

$(KATI_CXX)-c -std=c++11 $(KATI_CXXFLAGS) -o $@ $<

這個調用簡單解釋一下,就是編譯kati是需要依賴與KATI_CXX_OBJS和 KATI_CXX_GENERATED_OBJS這兩個變量。 KATI_CXX_OBJS的生成依賴於 KATI_INTERMEDIATES_PATH下的.o,而.o文件的生成又依賴與KATI_INTERMEDIATES_PATH下的.cc文件。在生成了所有依賴的.o文件之後,會link成編譯所需的ckati文件。具體的命令為:$(KATI_LD) -std=c++11$(KATI_CXXFLAGS) -o $@ $^ $(KATI_LIBS)

這樣的話,就完成了ckati可執行文件的生成。

流程圖可以簡單歸結如下

2.解析並使用ninja

ckati文件生成之後,我們接著來看是如何使用的。

接著回到ninja.mk文件中,如下是具體的調用。

$(KATI_BUILD_NINJA): $(KATI) $(MAKEPARALLEL)$(DUMMY_OUT_MKS) $(SOONG_ANDROID_MK) FORCE

@echo Running kati to generatebuild$(KATI_NINJA_SUFFIX).ninja... +$(hide)$(KATI_MAKEPARALLEL) $(KATI) --ninja --ninja_dir=$(OUT_DIR)--ninja_suffix=$(KATI_NINJA_SUFFIX) --regen --ignore_dirty=$(OUT_DIR)/%--no_ignore_dirty=$(SOONG_ANDROID_MK) --ignore_optional_ include=$(OUT_DIR)/%.P--detect_android_echo $(KATI_FIND_EMULATOR) -f build/core/main.mk $(KATI_GOALS)--gen_all_targets BUILDING_WITH_NINJA=true SOONG_ANDROID_MK=$(SOONG_ANDROID_MK)

可以看到使用kati,並且將很多的參數傳入到了ckati中。

在kati文件的main函數中,可以看到接受了這些參數並且進行處理。

文件地址:build/kati/main.cc

main函數:

int main(int argc, char*argv[]) {

if (argc >= 2 && !strcmp(argv[1],"--realpath")) {

HandleRealpath(argc - 2, argv + 2);

return 0;

}

Init();

string orig_args;

for (int i = 0; i < argc; i++) {

if (i)

orig_args += ' ';

orig_args += argv[i];

}

g_flags.Parse(argc, argv);

FindFirstMakefie();

if (g_flags.makefile == NULL)

ERROR("*** No targets specified and nomakefile found.");

// This depends on command line flags.

if (g_flags.use_find_emulator)

InitFindEmulator();

int r = Run(g_flags.targets, g_flags.cl_vars,orig_args);

Quit();

return r;

}

argv接受到了傳入的參數後,經過處理,轉化為了string,傳入orig_args變量,並且調用Run函數來進行後續的處理。Run函數是kati程序的核心,用於各種文件的生成,流程的執行以及處理。我們這邊只對重點內容進行分析。

任何的編譯都脫離不了環境變量的支持,在編譯的第一步,肯定要對環境變量進行設置。

在run函數的開始,就利用Linux標准C接口來進行了環境變量的讀取和設置。

具體操作為:

extern "C" char**environ;

…

for (char** p = environ; *p; p++) {



SetVar(*p, VarOrigin::ENVIRONMENT);

}

如果我們prinf打印*p的值,可以很清楚的看到該環境變量的設置。這邊只截取部分環境變量用於說明該問題:

printf("*p = %s \n", *p);

/*

*p =XDG_SESSION_PATH=/org/freedesktop/DisplayManager/Session0

*p =BUILD_ENV_SEQUENCE_NUMBER=10

*p =XDG_SEAT_PATH=/org/freedesktop/DisplayManager/Seat0

*p =ANDROID_BUILD_PATHS=/data/android_N/out/host/linux-x86/bin:/data/android_N/prebuilts/gcc/linux-x86/aarch64/aarch64-linux-android-4.9/bin:/data/android_N/prebuilts/gcc/linux-x86/arm/arm-linux-androideabi-4.9/bin:/data/android_N/development/scripts:/data/android_N/prebuilts/devtools/tools:/data/android_N/external/selinux/prebuilts/bin:/data/android_N/prebuilts/android-emulator/linux-x86_64:

*p = SSH_AUTH_SOCK=/tmp/keyring-941KY1/ssh

*p =MAKELEVEL=1

*p =DEFAULTS_PATH=/usr/share/gconf/ubuntu.default.path

*p =SESSION_MANAGER=local/chao:@/tmp/.ICE-unix/1835,unix/chao:/tmp/.ICE-unix/1835

*p =TARGET_BUILD_APPS=



* */



在設置完環境變量以後,就會開始對makefile進行部分的解析。這邊有個重要函數為

static voidReadBootstrapMakefile(const vector& targets,

vector* stmts) {

…

}

函數初始定義了一些基本的變量,比如GCC,G++,SHELL,MAKE等。並且會去解析當前編譯機器所擁有的cpu的核數,且進行合理分配。

以下是一些具體初始化的變量:

/*

* bootstrap =

*

* CC?=cc

* CXX?=g++

* AR?=ar

* MAKE_VERSION?=3.81

* KATI?=ckati

* SHELL=/bin/sh

* .c.o:

*$(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o $@ $<

*.cc.o:

*$(CXX) $(CXXFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o $@ $<

*MAKE?=make -j2

*MAKECMDGOALS?=

·CURDIR:=/data/android_N

並且會將這些字符串轉換為對應的node結構體,保存在內存變量中,方便編譯的時候使用。

for (Stmt* stmt :bootstrap_asts) {

printf("stmt = %s\n\n", stmt->DebugString().c_str());

stmt->Eval(ev);

}

以下截取部分log:

/*

stmt =AssignStmt(lhs=CC rhs=cc (cc) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)

stmt = AssignStmt(lhs=CXX rhs=g++ (g++) opstr=QUESTION_EQ dir=loc=*bootstrap*:0)

stmt =AssignStmt(lhs=AR rhs=ar (ar) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)

stmt =AssignStmt(lhs=MAKE_VERSION rhs=3.81 (3.81) opstr=QUESTION_EQ dir= loc=*bootstrap*:0)

stmt = AssignStmt(lhs=KATI rhs=ckati (ckati) opstr=QUESTION_EQ dir=loc=*bootstrap*:0)

stmt =AssignStmt(lhs=SHELL rhs=/bin/sh (/bin/sh) opstr=EQ dir=loc=*bootstrap*:0)

stmt =RuleStmt(expr=.c.o: term=0 after_term=(null) loc=*bootstrap*:0)

stmt =CommandStmt(Expr(SymRef(CC), ,SymRef(CFLAGS), , SymRef(CPPFLAGS), , SymRef(TARGET_ARCH), -c -o , SymRef(@), , SymRef(<)), loc=*bootstrap*:0)

stmt =RuleStmt(expr=.cc.o: term=0 after_term=(null) loc=*bootstrap*:0)

*/

在環境變量,編譯參數都設置成功後,就會開始GenerateNinja的重要操作。

GenerateNinja的函數定義在了ninja.cc中,以下是函數的具體實現。

void GenerateNinja(constvector& nodes,

Evaluator* ev,

const string&orig_args,

double start_time) {

NinjaGenerator ng(ev, start_time);

ng.Generate(nodes, orig_args);

} 

該函數初始化了一個 NinjaGenerator的結構體,並且繼續調用 Generate的方法。

void Generate(const vector&nodes,

const string& orig_args){

unlink(GetNinjaStampFilename().c_str());

PopulateNinjaNodes(nodes);

GenerateNinja();

GenerateShell();

GenerateStamp(orig_args);

}

Generate 函數非常的重要, PopulateNinjaNodes會對前面include的makefile進行解析,並且將node進行整理。正如前面分析的link的程序會依賴.o一樣,這裡基本會將所依賴的.o;.a; .so進行歸類,包含了所有文件下面的目錄。這裡舉一些簡單截取的例子:

node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/strlcpy.

node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/threads.o

node =out/host/linux-x86/obj/STATIC_LIBRARIES/libcutils_intermediates/dlmalloc_stubs.o

…..

node =out/host/linux-x86/obj/SHARED_LIBRARIES/libcryptohost_intermediates/src/crypto/evp/sign.o

node =out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/ex_data.o

node = out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/hkdf/hkdf.o

node = out/host/linux-x86/obj/SHARED_LIBRARIES/libcrypto-host_intermediates/src/crypto/hmac/hmac.o

….

node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IBackupAgent.java

node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IInstrumentationWatcher.java

node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/INotificationManager.java

node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/IProcessObserver.java

node = out/target/common/obj/JAVA_LIBRARIES/framework_intermediates/src/core/java/android/app/ISearchManager.java

….

node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base64.o

node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base64url.o

node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/base_switches.o

node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/bind_helpers.o

node = out/host/linux-x86/obj32/SHARED_LIBRARIES/libchrome_intermediates/base/build_time.o

…

node = out/target/product/generic/obj/SHARED_LIBRARIES/libhardware_intermediates/hardware.o

node = out/target/product/generic/obj/SHARED_LIBRARIES/libhardware_intermediates/import_includes

node = out/target/product/generic/obj/lib/libandroidfw.so.to

node = out/target/product/generic/obj/lib/libandroidfw.so

node = out/target/product/generic/symbols/system/lib/libandroidfw.so...

GenerateNinja() {

....

fp_ = fopen(GetNinjaFilename().c_str(), "wb");...

fprintf(fp_, "# Generated by kati %s\n",kGitVersion);

fprintf(fp_, "\n");

…

for (const ostringstream& buf : bufs) {

fprintf(fp_, "%s", buf.str().c_str());

}

…

fclose(fp_);

}

在整理好了依賴之後,會將所有的步驟寫入文件中。具體的操作為 GenerateNinja函數所實現。

GenerateNinja() {

....

fp_ = fopen(GetNinjaFilename().c_str(), "wb");...

fprintf(fp_, "# Generated by kati %s\n",kGitVersion);

fprintf(fp_, "\n");

…

for (const ostringstream& buf : bufs) {

fprintf(fp_, "%s", buf.str().c_str());

}

…

fclose(fp_);

}

在GenerateNinja函數中,會創建並寫入一個文件, 這個文件依賴於build target的制定。比如在nexus的編譯中,會在out目錄下生成

Build-aosp_arm.ninja文件,

該文件會非常大,但是這個也就是編譯的基礎和ninja可以明確知道自己所編譯的操作步數的由來。

具體的流程圖:

3.總結

Ninja編譯帶來的改變是巨大的,但是通過本文的分析,可以預見到後續的變化會更大且會一直存在。Android.bp何時可以完全取代makefile,ninja編譯時的test目錄的編譯其實對普通開發者來說都有些優化的空間。對這部分的研究將會持續存在

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved