插件化-Apk編譯過(guò)程概述
0x00
大致的看了一下目前插件化的開源實(shí)現(xiàn),或多或少都會(huì)對(duì)Apk的編譯過(guò)程做出改動(dòng),因此嘗試分析了一下Apk的打包過(guò)程。一個(gè)Apk文件實(shí)際上就是一個(gè)zip壓縮包,我們把一個(gè)Apk解壓后,里面的內(nèi)容類似下圖。

里面每個(gè)文件是什么含義,我們待會(huì)再看。那么,如何生成一個(gè)Apk文件呢?通常情況下,我們使用一些構(gòu)建工具來(lái)編譯我們的工程,例如古老的Ant,maven,我們正在使用的gradle,以及更加黑科技的buck等,但是,這些構(gòu)建工具并不直接作用于編譯過(guò)程,打開sdk中的build-tools目錄,如下

這些就是google為我們提供的工具,通過(guò)它們,我們得以將代碼編譯成Apk,構(gòu)建工具只是這些的工具的封裝。
0x01 HelloWorld
下面我們來(lái)嘗試手動(dòng)編譯一個(gè)最簡(jiǎn)單的Apk。開始之前,先簡(jiǎn)單介紹一下我們要使用的工具。
- aapt[Android Asset Packaging Tool]這個(gè)工具主要幫助我們處理資源文件,以及創(chuàng)建,更新,查看一個(gè)Apk文件。
- dx,這個(gè)工具幫助我們把.class文件轉(zhuǎn)換成一個(gè)dex文件,dex[Dalvik Executable]文件就是Dalvik虛擬機(jī)的可執(zhí)行文件,這個(gè)文件的具體格式稍后會(huì)做簡(jiǎn)單的介紹。
- zipalign,這個(gè)工具用來(lái)優(yōu)化我們生成的apk文件,它將資源文件進(jìn)行4字節(jié)對(duì)齊,當(dāng)資源文件映射進(jìn)內(nèi)存時(shí),對(duì)齊到4字節(jié)邊界可以加快資源文件的訪問(wèn)速度。
還有兩個(gè)我們使用的工具并沒有出現(xiàn)這里,而是存在于JDK中。
- javac,很常用的工具,用來(lái)將java源碼文件編譯成字節(jié)碼文件
- keytool,創(chuàng)建簽名的工具
- jarsigner,用來(lái)對(duì)生成的apk進(jìn)行簽名的工具
下面我們來(lái)開始編譯helloWorld,首先我們編寫了一個(gè)最簡(jiǎn)單的Apk。

這個(gè)工程只有一個(gè)Activity,并且不依賴任何庫(kù)。
Step 1: 生成R.java文件
R.java是我們?cè)L問(wèn)資源的必需品,R文件是一個(gè)普通的類,其中根據(jù)資源的類型有不同的靜態(tài)內(nèi)部類,每個(gè)靜態(tài)內(nèi)部類中的靜態(tài)常量分別定義一條資源標(biāo)示符,這個(gè)類并不是我們編寫的而是由aapt工具生成的。
在工程的根目錄,執(zhí)行:
aapt package \ #打包資源文件
-f \ #強(qiáng)制覆蓋已有文件
-m \ #使R文件在-J參數(shù)指定的位置生成
-S res \ #資源目錄
-J gen \ #R.java的位置
-I $ANDROID_HOME/platforms/android-23/android.jar \ #base-package
-M AndroidManifest.xml #清單文件的路徑
Step 2: 編譯代碼
生成后的文件存放在gen目錄下,有了R.java 我們就可以使用javac來(lái)編譯我們的代碼了,繼續(xù)執(zhí)行:
javac -classpath \ #添加依賴包,多個(gè)jar包用:分割
$ANDROID_HOME/platforms/android-23/android.jar \ #sdk-23
-source 1.7 -target 1.7 \ #指明源碼版本和字節(jié)碼版本
-d ./build \ #編譯后的class文件的路徑
./java/com/haizhi/oa/buildtest/*.java \ #源碼1,這是我們寫的Activity
./gen/com/haizhi/oa/buildtest/R.java #源碼2,R.java
Step 3: 編譯為dex文件
在上一步中,我們將代碼編譯成了字節(jié)碼,但是dalvik并不能直接執(zhí)行字節(jié)碼,需要進(jìn)一步的將class文件編譯成dex文件,這個(gè)過(guò)程是通過(guò)dx這個(gè)工具實(shí)現(xiàn)的,在build目錄下,我們繼續(xù)執(zhí)行:
dx --dex --output=classes.dex . #指定輸出為classes.dex 輸入為當(dāng)前目錄
至此,我們已經(jīng)獲得了生成一個(gè)Apk需要的所有東西。
Step 4: 打包所有的資源文件
在工程的根目錄,執(zhí)行:
aapt package \
-f \
-S res \
-I $ANDROID_HOME/platforms/android-23/android.jar \
-M AndroidManifest.xml \
-F test.apk.u #生成apk文件
此時(shí),我們已經(jīng)獲得了一個(gè)apk文件,下面我們要對(duì)它簽名,首先需要使用keytool工具生成一個(gè)簽名文件,這個(gè)步驟可以自行百度。
Step 5: 將classes.dex文件加入apk中
aapt add -f test.apk.u classes.dex
Step 6: 簽名,對(duì)齊
在工程的根目錄,執(zhí)行:
簽名:
jarsigner -storepass **密*碼** -keystore ../chenlong.keystore test.apk.u chenlong
對(duì)齊:
zipalign 4 test.apk.u test.apk
經(jīng)過(guò)上述5個(gè)步驟,我們生成了一個(gè)apk,下面安裝到模擬器上執(zhí)行一下,如圖:

以上,就是一個(gè)最簡(jiǎn)單的Apk的編譯過(guò)程,其中Apk最重要的兩個(gè)部分,資源和代碼被編譯成了resources.arsc+res以及dex文件。res是實(shí)際的資源,resources.arsc則是一個(gè)索引,AssetManager通過(guò)這個(gè)索引獲取資源的實(shí)際內(nèi)容,這其中的過(guò)程比較復(fù)雜,暫時(shí)還沒有太多的分析,至于dex文件,倒是可以啰嗦兩句。
我們知道,java源碼文件編譯后生成了字節(jié)碼文件,然后被jvm執(zhí)行,字節(jié)碼文件中有一個(gè)非常重要的區(qū)域是常量池,編譯的過(guò)程中,字節(jié)碼文件并不會(huì)保存方法和字段的最終內(nèi)存布局信息,也就是說(shuō),方法和字段并不像C/C++那樣被編譯成地址,jvm在加載Class文件的時(shí)候,需要從常量池獲取對(duì)應(yīng)的符號(hào)引用,再在類創(chuàng)建時(shí)或運(yùn)行時(shí)解析并翻譯到具體的內(nèi)存地址中【參考:深入理解Java虛擬機(jī)-JVM高級(jí)特性與最佳實(shí)踐】。一個(gè)字節(jié)碼文件中,除了方法體中的內(nèi)容被編譯為字節(jié)碼指令外,大部分的信息都保存在常量池中,通過(guò)索引來(lái)訪問(wèn),包括類的名稱,類的字段,類的繼承關(guān)系,類中方法的定義等。
那么,dex文件和class文件有什么區(qū)別呢?
首先,dalvik虛擬機(jī)的字節(jié)碼指令是16位,而jvm是8位,因此,java 字節(jié)碼被轉(zhuǎn)換成dex 字節(jié)碼;其次,dex文件將多個(gè)class文件合并成一個(gè),合并了這些class文件的常量池,并作出了其他的優(yōu)化,讓dex文件執(zhí)行的更快,更節(jié)省內(nèi)存。對(duì)于dex文件的詳細(xì)格式,可以參考 dex-format,我嘗試了一下直接閱讀dex文件,講真,不是很好讀。。下圖是我們剛剛編譯出的dex文件的16進(jìn)制格式,加了一些簡(jiǎn)單的標(biāo)注和分塊,一共3012個(gè)字節(jié)。

0x02 進(jìn)階-編譯一個(gè)帶依賴的工程
在實(shí)際的編碼過(guò)程中,我們往往會(huì)去依賴一些子工程,子工程有兩種,一種是java工程,一種是Android Lib工程。java工程中不包含資源文件,編譯后的輸出是jar包,而Android Lib工程包含資源文件,編譯后的輸出為aar文件。
對(duì)于jar包,我們只需要在編譯apk的java代碼時(shí),將jar包加入classpath,然后在編譯dex文件時(shí),將jar包一起編譯進(jìn)去就可以了,但是對(duì)于aar文件,就稍微有點(diǎn)復(fù)雜了。
首先,我們還是創(chuàng)建一個(gè)工程,如圖:

這個(gè)工程依賴了design包,v7包中的appcompat,同時(shí),上述這些包又依賴了v4包,,recyclerview,support-vector-drawable,animated-vector-drawable,support-annotations。
上述這些依賴都是Android Lib工程,因此我們需要處理依賴包中的資源。首先,我們需要這些依賴的aar文件作為輸入,到哪里去找aar文件呢?最初,我在sdk下找到了這些依賴的jar包和相應(yīng)的資源目錄,但是,當(dāng)我嘗試編譯的時(shí)候,總是提示我找不到資源,我很苦惱,后來(lái)在高旭大神的指點(diǎn)下,我看了一下gradle的實(shí)現(xiàn)方式,發(fā)現(xiàn)gradle并不使用jar包+資源來(lái)重新編譯這些依賴庫(kù)而是直接使用了google提供的這些依賴庫(kù)的aar文件,于是我嘗試將編譯好的aar文件解包,再使用解包后的資源和jar包進(jìn)行編譯。
Step 1: 生成R.java文件
aapt package -f -m --auto-add-overlay \
-S res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/appcompat-v7/23.3.0/aarEx/res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/recyclerview-v7/23.3.0/aarEx/res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/design/23.3.0/aarEx/res \
-J gen \
--extra-packages android.support.v7.appcompat:android.support.v7.recyclerview:android.support.design \
-I $ANDROID_HOME/platforms/android-23/android.jar -M ./AndroidManifest.xml
其中,--auto-add-overlay 參數(shù)用來(lái)加載多個(gè)資源目錄,按照從左向右的順序,如果后面的資源重復(fù)則跳過(guò),如果不重復(fù)則新增。
--extra-packages用來(lái)對(duì)不同的資源目錄生成包名不同的R文件,多個(gè)包名通過(guò):分割。
Step 2: 編譯代碼
javac -classpath $ANDROID_HOME/extras/android/support/v7/appcompat/libs/android-support-v4.jar:\
$ANDORID_HOME/extras/android/support/annotations/android-support-annotations.jar:\
$ANDROID_HOME/platforms/android-23/android.jar:\
$ANDROID_HOME/extras/android/support/design/libs/android-support-design.jar:\
$ANDROID_HOME/extras/android/support/v7/appcompat/libs/android-support-v7-appcompat.jar:\
$ANDROID_HOME/extras/android/support/v7/recyclerview/libs/android-support-v7-recyclerview.jar \
-source 1.7 -target 1.7 \
-d ./build \
./java/com/haizhi/oa/buildtest/*.java \
./gen/com/haizhi/oa/buildtest/R.java \
./gen/android/support/design/R.java \
./gen/android/support/v7/appcompat/R.java \
./gen/android/support/v7/recyclerview/R.java
Step 3: 編譯dex文件
dx --dex --output=classes.dex . \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/support-v4/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/support-v4/23.3.0/aarEx/libs/internal_impl-23.3.0.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/design/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/appcompat-v7/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/recyclerview-v7/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/support-vector-drawable/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/animated-vector-drawable/23.3.0/aarEx/classes.jar \
/Users/chenlong/sdk/extras/android/m2repository/com/android/support/support-annotations/23.3.0/support-annotations-23.3.0.jar
Step 4: 生成apk文件
aapt package -f -m --auto-add-overlay \
-S res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/appcompat-v7/23.3.0/aarEx/res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/recyclerview-v7/23.3.0/aarEx/res \
-S /Users/chenlong/sdk/extras/android/m2repository/com/android/support/design/23.3.0/aarEx/res \
-I $ANDROID_HOME/platforms/android-23/android.jar -M ./AndroidManifest.xml \
-F test.apk.u
Step 5: 將classes.dex文件加入apk中
aapt add -f test.apk.u classes.dex
后面的簽名、對(duì)齊操作和之前一樣
最后,我們?cè)谀M器上運(yùn)行一下打包后的apk文件,如圖:

0x03 總結(jié)
編譯流程的簡(jiǎn)單分析就是這些,在上述流程中我們可以看到,主要過(guò)程是資源處理和dex文件生成上,其中對(duì)資源的處理是插件化的一個(gè)難點(diǎn),我的分析并不是很全面,比如對(duì)于多個(gè)資源目錄合并的過(guò)程,aapt自身提供的機(jī)制和gradle的實(shí)現(xiàn)就不太一樣,gradle在最終調(diào)用aapt之前已經(jīng)將資源合并,傳入aapt的只有一個(gè)合并后的資源目錄,可以參考gradle 資源合并機(jī)制,后續(xù)我會(huì)針對(duì)資源文件的處理做單獨(dú)的分析。
上述內(nèi)容如有錯(cuò)誤,懇請(qǐng)指正,我會(huì)繼續(xù)分析插件化的相關(guān)技術(shù)實(shí)現(xiàn),敬請(qǐng)期待。