自己動手編譯Android源碼

Android Studio代碼調試一文中,簡單的介紹了代碼調試的一些技巧.現在我們來談談android源碼編譯的一些事.(俺認爲,作爲android developer人人都應該有一份自己Android源碼,這樣我們就可以隨時對自己有疑惑的地方通過親手調試來加強理解).

本文使用最新的Ubuntu 16.04,請首先確保自己已經安裝了Git.沒安裝的同學可以通過以下命令進行安裝:

sudo apt-get install git 
git config –global user.email “test@test.com” 
git config –global user.name “test

其中[email protected]爲你自己的郵箱.

簡要說明

android源碼編譯的四個流程:1.源碼下載;2.構建編譯環境;3.編譯源碼;4運行.下文也將按照該流程講述.


源碼下載

由於某牆的原因,這裏我們採用國內的鏡像源進行下載.
目前,可用的鏡像源一般是科大和清華的,具體使用差不多,這裏我選擇清華大學鏡像進行說明.(參考:科大源,清華源)

repo工具下載及安裝

通過執行以下命令實現repo工具的下載和安裝

mkdir ~/bin
PATH=~/bin:$PATH
curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo
chmod a+x ~/bin/repo

補充說明
這裏,我來簡單的介紹下repo工具,我們知道AOSP項目由不同的子項目組成,爲了方便進行管理,Google採用Git對AOSP項目進行多倉庫管理.在聊repo工具之前,我先帶你來聊聊多倉庫項目:

我們有個非常龐大的項目Pre,該項目由很多個子項目R1,R2,...Rn等組成,爲了方便管理和協同開發,我們爲每個子項目創立自己的倉庫,整個項目的結構如下:


這裏寫圖片描述

將一個項目Pre進行分庫後會遇到這麼一個問題:如果我們想要創建Pre分支來做feature開發,這就意味着,我們需要到每個子項目中分別創建對應的分支,這個過程如果純粹靠手工做,那簡直是個災難,利索當然我們會想寫個自動化處理程序(我們假設這個工具叫做RepoUtil)來幫助我們解決這個問題.這個RepoUtil也會有版本管理之類的需求,因此我們也用Git對其管理,併爲其創建對應的倉庫.此時整個項目的結構如下:


這裏寫圖片描述


這裏RepoUtil知道整個項目Pre下的每個子項目(即維護子項目的列表),同時需要提供對這些子項目的管理功能,比如統一創建分支等.但是從"單一職責"角度來看,RepoUitl這個工具的功能過於複雜,我們完全可以將維護子項目列表這個功能抽取出來作爲一個新項目sub_projects,因爲子項目也會變化,因此,爲其創建對應的倉庫,並用Git管理,這樣的化,RepoUtil只需要通過簡單的對ub_projects進行依賴即可,此時整個項目的結構如下:


這裏寫圖片描述

AOSP項目結構和我上文的描述非常類似.repo工具對應RepoUtil,mainfest對應sub_projects.
總結一下:repo就是這麼一種工具,由一系列python腳本組成,通過調用Git命令實現對AOSP項目的管理.

建立源碼文件夾

熟悉Git的同學都應該知道,我們需要爲項目在本地創建對應的倉庫.同樣,這裏爲了方便對代碼進行管理,我們爲其創建一個文件夾.這裏我在當前用戶目錄下創建了source文件夾,後面所有的下載的源碼和編譯出的產物也都放在這裏,命令如下:

mkdir source
cd source

初始化倉庫

我們將上面的source文件夾作爲倉庫,現在需要來初始化這個倉庫了.通過執行初始化倉庫命令可以獲取AOSP項目master上最新的代碼並初始化該倉庫,命令如下:

repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest

或者使用:

repo init -u git://aosp.tuna.tsinghua.edu.cn/aosp/platform/manifest

兩者實現的效果一致,僅僅只是協議不同.
如果執行該命令的過程中,如果提示無法連接到 gerrit.googlesource.com,那麼我們只需要編輯 ~/bin/repo文件,找到REPO_URL這一行,然後將其內容修改爲:

REPO_URL = 'https://gerrit-google.tuna.tsinghua.edu.cn/git-repo'

然後重新執行上述命令即可.

補充說明
不帶參數的manifest命令用於獲取master上最新的代碼,但是可以通過-b參數指定獲取某個特定的android版本,比如我們想要獲取android-4.0.1_r1分支,那麼命令如下:

repo init -u https://aosp.tuna.tsinghua.edu.cn/platform/manifest -b android-4.0.1_r1

(AOSP項目當前所有的分支列表參看:分支列表)

同步源碼到本地

初始化倉庫之後,就可以開始正式同步代碼到本地了,命令如下:

repo sync

以後如果需要同步最新的遠程代碼到本地,也只需要執行該命令即可.在同步過程中,如果因爲網絡原因中斷,使用該命令繼續同步即可.不出意外,5個小時便可以將全部源碼同步到本地.所以呢,這個過程可以放在晚上睡覺期間完成.

(提示:一定要確定代碼完全同步了,不然在下面編譯過程出現的錯誤會讓你痛不欲生,不確定的童鞋可以多用repo sync同步幾次)


構建編譯環境

源碼下載完成後,就可以構建編譯環境了.在開始之前,我們先來看看一些編譯要求:

1. 硬件要求:
64位的操作系統只能編譯2.3.x以上的版本,如果你想要編譯2.3.x以下的,那麼需要32位的操作系統.
磁盤空間越多越好,至少在100GB以上.意思就是,你可以去買個大點的硬盤了啊
如果你想要在是在虛擬機運行linux,那麼至少需要16GB的RAM/swap.
(實際上,我非常不推薦在虛擬機中編譯2.3.x以上的代碼.)

2. 軟件要求:
1. 操作系統要求
AOSP開源中,主分支使用Ubuntu長期版本開發和測試的,因此也建議你使用Ubuntu進行編譯,下面我們列出不同版本的的Ubuntu能夠編譯那些android版本:

Android版本 編譯要求的Ubuntu最低版本
Android 6.0至AOSP master Ubuntu 14.04
Android 2.3.x至Android 5.x Ubuntu 12.04
Android 1.5至Android 2.2.x Ubuntu 10.04

2. JDK版本要求
除了操作系統版本這個問題外,我們還需要關注JDK版本問題,爲了方便,同樣我們也列出的不同Android版本的源碼需要用到的JDK版本:

Android版本 編譯要求的JDK版本
AOSP的Android主線 OpenJDK 8
Android 5.x至android 6.0 OpenJDK 7
Android 2.3.x至Android 4.4.x Oracle JDK 6
Android 1.5至Android 2.2.x Oracle JDK 5

更具體的可以參看:Google源碼編譯要求

我現在在Ubuntu 16.04下編譯AOSP主線代碼,因此需要安裝OpenJDK 8,執行命令如下:
sudo apt-get install openjdk-8-jdk
如果你需要在Ubuntu 14.04下編譯AOSP主線代碼,同樣需要安裝OpenJDK 8,此時需要執行如下命令:

sudo apt-get update
sudo apt-get install openjdk-8-jdk

如果你要編譯的是Android 5.x到android 6.0之間的系統版本,需要採用openjdk7.但是在Ubuntu 15.04及之後的版本的在線安裝庫中只支持openjdk8和openjdk9的安裝.因此,如果你想要安裝openjdk 7需要首先設置ppa:

sudo add-apt-repository ppa:openjdk-r/ppa 
sudo apt-get update

然後再執行安裝命令:

sudo apt-get install openjdk-7-jdk

有時候,我們需要編譯不同版本的android系統,就可能使用不同的jdk版本.關於jdk版本切換,可以使用如下命令:

sudo update-alternative --config java
sudo update-alternative --config javac

3. 其他要求

Google官方構建編譯環境指南中已經說明了Ubuntu14.04,Ubuntu 12.04,Ubuntu 10.04需要添加的依賴,這裏我們就不做介紹了.我原先以爲,Ubuntu16.04的設置和Ubuntu14.04的依賴設置應該差不多,但是隻能說too young too simple.
下面是Ubuntu16.04中的依賴設置:

sudo apt-get install libx11-dev:i386 libreadline6-dev:i386 libgl1-mesa-dev g++-multilib 
sudo apt-get install -y git flex bison gperf build-essential libncurses5-dev:i386 
sudo apt-get install tofrodos python-markdown libxml2-utils xsltproc zlib1g-dev:i386 
sudo apt-get install dpkg-dev libsdl1.2-dev libesd0-dev
sudo apt-get install git-core gnupg flex bison gperf build-essential  
sudo apt-get install zip curl zlib1g-dev gcc-multilib g++-multilib 
sudo apt-get install libc6-dev-i386 
sudo apt-get install lib32ncurses5-dev x11proto-core-dev libx11-dev 
sudo apt-get install libgl1-mesa-dev libxml2-utils xsltproc unzip m4
sudo apt-get install lib32z-dev ccache

(其中幾個命令中參數是重複的,但不妨礙我們)

初始化編譯環境

確保上述過程完成後,接下來我們需要初始化編譯環境,命令如下:

source build/envsetup.sh

執行該命令結果如下:


這裏寫圖片描述

不難發現該命令只是引入了其他執行腳本,至於這些腳本做什麼,目前不在本文中細說.
該命令執行成功後,我們會得到了一些有用的命令,比如最下面要用到的lunch命令.


編譯源碼

初始化編譯環境之後,就進入源碼編譯階段.這個階段又包括兩個階段:選擇編譯目標和執行編譯.

選擇編譯目標

通過lunch指令設置編譯目標,所謂的編譯目標就是生成的鏡像要運行在什麼樣的設備上.這裏我們設置的編譯目標是aosp_arm64-eng,因此執行指令:

lunch aosp_arm64-eng

編譯目標格式說明
編譯目標的格式:BUILD-BUILDTYPE,比如上面的aosp_arm-eng的BUILD是aosp_arm,BUILDTYPE是eng.

什麼是BUILD

BUILD指的是特定功能的組合的特定名稱,即表示編譯出的鏡像可以運行在什麼環境.其中,aosp(Android Open Source Project)代表Android開源項目;arm表示系統是運行在arm架構的處理器上,arm64則是指64位arm架構;處理器,x86則表示x86架構的處理器;此外,還有一些單詞代表了特定的Nexus設備,下面是常用的設備代碼和編譯目標,更多參考官方文檔
|受型號|設備代碼|編譯目標|
|---|----|---|
|Nexus 6P|angler|aosp_angler-userdebug|
|Nexus 5X|bullhead|aosp_bullhead-userdebug|
|Nexus 6|shamu|aosp_shamu-userdebug|
|Nexus 5|hammerhead|aosp_hammerhead-userdebug|

提示:如果你沒有Nexus設備,那麼通常選擇arm或者x86即可

什麼是BUILDTYPE

BUILD TYPE則指的是編譯類型,通常有三種:
-user:代表這是編譯出的系統鏡像是可以用來正式發佈到市場的版本,其權限是被限制的(如,沒有root權限,不鞥年dedug等)
-userdebug:在user版本的基礎上開放了root權限和debug權限.
-eng:代表engineer,也就是所謂的開發工程師的版本,擁有最大的權限(root等),此外還附帶了許多debug工具

瞭解編譯目標的組成之後,我們就可以根據自己目前的情況選擇了.那不知道編譯目標怎麼辦?
我們只需要執行不帶參數的lunch指令,稍後,控制檯會列出所有的編譯目標,如下:


這裏寫圖片描述


接着我們只需要輸入相應的數字即可.

來舉個例子:你沒有Nexus設備,只想編譯完後運行看看,那麼就可以選擇aosp_arm-eng.
(我在ubuntu 16.04(64位)中編譯完成後啓動虛擬機時,卡在黑屏,嘗試編譯aosp_arm64-eng解決.因此,這裏我使用了aosp_arm64-eng)

開始編譯

通過make指令進行代碼編譯,該指令通過-j參數來設置參與編譯的線程數量,以提高編譯速度.比如這裏我們設置8個線程同時編譯:

make -j8

需要注意的是,參與編譯的線程並不是越多越好,通常是根據你機器cup的核心來確定:core*2,即當前cpu的核心的2倍.比如,我現在的筆記本是雙核四線程的,因此根據公式,最快速的編譯可以make -j8.
(通過cat /proc/cpuinfo查看相關cpu信息)

如果一切順利的化,在幾個小時之後,便可以編譯完成.看到### make completed successfully (01:18:45(hh:mm:ss)) ###表示你編譯成功了.


運行模擬器

在編譯完成之後,就可以通過以下命令運行Android虛擬機了,命令如下:

source build/envsetup.sh
lunch(選擇剛纔你設置的目標版本,比如這裏了我選擇的是2)
emulator

如果你是在編譯完後立刻運行虛擬機,由於我們之前已經執行過source及lunch命令了,因此現在你只需要執行命令就可以運行虛擬機:

emulator

不出意外,在等待一會之後,你會看到運行界面:


這裏寫圖片描述

補充
既然談到了模擬器運行,這裏我們順便介紹模擬器運行所需要四個文件:

  1. Linux Kernel
  2. system.img
  3. userdate.img
  4. ramdisk.img

如果你在使用lunch命令時選擇的是aosp_arm-eng,那麼在執行不帶參數的emualtor命令時,Linux Kernel默認使用的是/source/prebuilds/qemu-kernel/arm/kernel-qemu目錄下的kernel-qemu文件;而android鏡像文件則是默認使用source/out/target/product/generic目錄下的system.img,userdata.img和ramdisk.img,也就是我們剛剛編譯出來的鏡像文件.

上面我在使用lunch命令時選擇的是aosp_arm64-eng,因此linux默認使用的/source/prebuilds/qemu-kernel/arm64/kernel-qemu下的kernel-qemu,而其他文件則是使用的source/out/target/product/generic64目錄下的system.img,userdata.img和ramdisk.img.
當然,emulator指令允許你通過參數制定使用不同的文件,具體用法可以通過emulator --help查看


模塊編譯

除了通過make命令編譯可以整個android源碼外,Google也爲我們提供了相應的命令來支持單獨模塊的編譯.

編譯環境初始化(即執行source build/envsetup.sh)之後,我們可以得到一些有用的指令,除了上邊用到的lunch,還有以下:

  - croot: Changes directory to the top of the tree.
  - m: Makes from the top of the tree.
  - mm: Builds all of the modules in the current directory.
  - mmm: Builds all of the modules in the supplied directories.
  - cgrep: Greps on all local C/C++ files.
  - jgrep: Greps on all local Java files.
  - resgrep: Greps on all local res/*.xml files.
  - godir: Go to the directory containing a file.

其中mmm指令就是用來編譯指定目錄.通常來說,每個目錄只包含一個模塊.比如這裏我們要編譯Launcher2模塊,執行指令:

mmm packages/apps/Launcher2/

稍等一會之後,如果提示:
### make completed success fully ###
即表示編譯完成,此時在out/target/product/gereric/system/app就可以看到編譯的Launcher2.apk文件了.

重新打包系統鏡像
編譯好指定模塊後,如果我們想要將該模塊對應的apk集成到系統鏡像中,需要藉助make snod指令重新打包系統鏡像,這樣我們新生成的system.img中就包含了剛纔編譯的Launcher2模塊了.重啓模擬器之後生效.

單獨安裝模塊
我們在不斷的修改某些模塊,總不能每次編譯完成後都要重新打包system.img,然後重啓手機吧?有沒有什麼簡單的方法呢?
在編譯完後,藉助adb install命令直接將生成的apk文件安裝到設備上即可,相比使用make snod,會節省很多事件.

補充
我們簡單的來介紹out/target/product/generic/system目錄下的常用目錄:
Android系統自帶的apk文件都在out/target/product/generic/system/apk目錄下;
一些可執行文件(比如C編譯的執行),放在out/target/product/generic/system/bin目錄下;
動態鏈接庫放在out/target/product/generic/system/lib目錄下;
硬件抽象層文件都放在out/targer/product/generic/system/lib/hw目錄下.


SDK編譯

如果你需要自己編譯SDK使用,很簡單,只需要執行命令make sdk即可.


錯誤集合

在編譯過程中,遇到的大部分錯誤都可以在google搜到解決方案.這裏只列舉幾個常見的錯誤:
錯誤一: You are attemping to build with the incorrect version.具體錯誤如下:


這裏寫圖片描述

如果你認真看了構建環境的的要求,那麼這個錯誤是可以避免的.當然,這個問題也很容易解決:安裝openjdk 8,別忘了使用sudo update-alternative命令切換jdk版本.

錯誤二: Out of memory error.具體錯誤如下:


這裏寫圖片描述

這個錯誤比較常見,尤其是在編譯AOSP主線代碼時,常常會因爲JVM heap size太小而導致該錯誤.
此時有兩種解決方法:
方法一:
在編譯命令之前,修改prebuilts/sdk/tools/jack-admin文件,找到文件中的這一行:
JACK_SERVER_COMMAND="java -Djava.io.tmpdir=$TMPDIR $JACK_SERVER_VM_ARGUMENTS -cp $LAUNCHER_JAR $LAUNCHER_NAME"
然後在該行添加-Xmx4096m,如:
JACK_SERVER_COMMAND="java -Djava.io.tmpdir=$TMPDIR $JACK_SERVER_VM_ARGUMENTS -Xmx4096m -cp $LAUNCHER_JAR $LAUNCHER_NAME"
然後再執行time make -8j

方法二:
在控制檯執行以下命令:

export JACK_SERVER_VM_ARGUMENTS="-Dfile.encoding=UTF-8 -XX:+TieredCompilation -Xmx4096m"
out/host/linux-x86/bin/jack-admin kill-server
out/host/linux-x86/bin/jack-admin start-server

如圖:


這裏寫圖片描述

執行完該命令後,再使用make命令繼續編譯.某些情況下,當你執行jack-admin kill-server時可能提示你命令不存在,此時去你去out/host/linux-x86/bin/目錄下會發現不存在jack-admin文件.如果我是你,我就會重新repo sync下,然後從頭來過.

錯誤三:使用emulator時,虛擬機停在黑屏界面,點擊無任何響應.此時,可能是kerner內核問題,解決方法如下:
執行如下命令:

./out/host/linux-x86/bin/emulator -partition-size 1024 -kernel ./prebuilts/qemu-kernel/arm/kernel-qemu-armv7

通過使用kernel-qemu-armv7內核 解決模擬器等待黑屏問題.而-partition-size 1024 則是解決警告: system partion siez adjusted to match image file (163 MB >66 MB)

如果你一開始編譯的版本是aosp_arm-eng,使用上述命令仍然不能解決等待黑屏問題時,不妨編譯aosp_arm64-eng試試.


結束吧

到現在爲止,你已經瞭解了整個android編譯的流程.除此之外,我也簡單的說明android源碼的多倉庫管理機制.下面,不妨自己動手嘗試一下.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章