插件化-Apk編譯過程概述
0x00
大致的看了一下目前插件化的開源實現,或多或少都會對Apk的編譯過程做出改動,因此嘗試分析了一下Apk的打包過程。一個Apk文件實際上就是一個zip壓縮包,我們把一個Apk解壓后,里面的內容類似下圖。
里面每個文件是什么含義,我們待會再看。那么,如何生成一個Apk文件呢?通常情況下,我們使用一些構建工具來編譯我們的工程,例如古老的Ant,maven,我們正在使用的gradle,以及更加黑科技的buck等,但是,這些構建工具并不直接作用于編譯過程,打開sdk中的build-tools目錄,如下
這些就是google為我們提供的工具,通過它們,我們得以將代碼編譯成Apk,構建工具只是這些的工具的封裝。
0x01 HelloWorld
下面我們來嘗試手動編譯一個最簡單的Apk。開始之前,先簡單介紹一下我們要使用的工具。
- aapt[Android Asset Packaging Tool]這個工具主要幫助我們處理資源文件,以及創建,更新,查看一個Apk文件。
- dx,這個工具幫助我們把.class文件轉換成一個dex文件,dex[Dalvik Executable]文件就是Dalvik虛擬機的可執行文件,這個文件的具體格式稍后會做簡單的介紹。
- zipalign,這個工具用來優化我們生成的apk文件,它將資源文件進行4字節對齊,當資源文件映射進內存時,對齊到4字節邊界可以加快資源文件的訪問速度。
還有兩個我們使用的工具并沒有出現這里,而是存在于JDK中。
- javac,很常用的工具,用來將java源碼文件編譯成字節碼文件
- keytool,創建簽名的工具
- jarsigner,用來對生成的apk進行簽名的工具
下面我們來開始編譯helloWorld,首先我們編寫了一個最簡單的Apk。
這個工程只有一個Activity,并且不依賴任何庫。
Step 1: 生成R.java文件
R.java是我們訪問資源的必需品,R文件是一個普通的類,其中根據資源的類型有不同的靜態內部類,每個靜態內部類中的靜態常量分別定義一條資源標示符,這個類并不是我們編寫的而是由aapt工具生成的。
在工程的根目錄,執行:
aapt package \ #打包資源文件
-f \ #強制覆蓋已有文件
-m \ #使R文件在-J參數指定的位置生成
-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來編譯我們的代碼了,繼續執行:
javac -classpath \ #添加依賴包,多個jar包用:分割
$ANDROID_HOME/platforms/android-23/android.jar \ #sdk-23
-source 1.7 -target 1.7 \ #指明源碼版本和字節碼版本
-d ./build \ #編譯后的class文件的路徑
./java/com/haizhi/oa/buildtest/*.java \ #源碼1,這是我們寫的Activity
./gen/com/haizhi/oa/buildtest/R.java #源碼2,R.java
Step 3: 編譯為dex文件
在上一步中,我們將代碼編譯成了字節碼,但是dalvik并不能直接執行字節碼,需要進一步的將class文件編譯成dex文件,這個過程是通過dx
這個工具實現的,在build目錄下,我們繼續執行:
dx --dex --output=classes.dex . #指定輸出為classes.dex 輸入為當前目錄
至此,我們已經獲得了生成一個Apk需要的所有東西。
Step 4: 打包所有的資源文件
在工程的根目錄,執行:
aapt package \
-f \
-S res \
-I $ANDROID_HOME/platforms/android-23/android.jar \
-M AndroidManifest.xml \
-F test.apk.u #生成apk文件
此時,我們已經獲得了一個apk文件,下面我們要對它簽名,首先需要使用keytool工具生成一個簽名文件,這個步驟可以自行百度。
Step 5: 將classes.dex文件加入apk中
aapt add -f test.apk.u classes.dex
Step 6: 簽名,對齊
在工程的根目錄,執行:
簽名:
jarsigner -storepass **密*碼** -keystore ../chenlong.keystore test.apk.u chenlong
對齊:
zipalign 4 test.apk.u test.apk
經過上述5個步驟,我們生成了一個apk,下面安裝到模擬器上執行一下,如圖:
以上,就是一個最簡單的Apk的編譯過程,其中Apk最重要的兩個部分,資源和代碼被編譯成了resources.arsc+res以及dex文件。res是實際的資源,resources.arsc則是一個索引,AssetManager通過這個索引獲取資源的實際內容,這其中的過程比較復雜,暫時還沒有太多的分析,至于dex文件,倒是可以啰嗦兩句。
我們知道,java源碼文件編譯后生成了字節碼文件,然后被jvm執行,字節碼文件中有一個非常重要的區域是常量池,編譯的過程中,字節碼文件并不會保存方法和字段的最終內存布局信息,也就是說,方法和字段并不像C/C++那樣被編譯成地址,jvm在加載Class文件的時候,需要從常量池獲取對應的符號引用,再在類創建時或運行時解析并翻譯到具體的內存地址中【參考:深入理解Java虛擬機-JVM高級特性與最佳實踐】。一個字節碼文件中,除了方法體中的內容被編譯為字節碼指令外,大部分的信息都保存在常量池中,通過索引來訪問,包括類的名稱,類的字段,類的繼承關系,類中方法的定義等。
那么,dex文件和class文件有什么區別呢?
首先,dalvik虛擬機的字節碼指令是16位,而jvm是8位,因此,java 字節碼被轉換成dex 字節碼;其次,dex文件將多個class文件合并成一個,合并了這些class文件的常量池,并作出了其他的優化,讓dex文件執行的更快,更節省內存。對于dex文件的詳細格式,可以參考 dex-format,我嘗試了一下直接閱讀dex文件,講真,不是很好讀。。下圖是我們剛剛編譯出的dex文件的16進制格式,加了一些簡單的標注和分塊,一共3012個字節。
0x02 進階-編譯一個帶依賴的工程
在實際的編碼過程中,我們往往會去依賴一些子工程,子工程有兩種,一種是java工程,一種是Android Lib工程。java工程中不包含資源文件,編譯后的輸出是jar包,而Android Lib工程包含資源文件,編譯后的輸出為aar文件。
對于jar包,我們只需要在編譯apk的java代碼時,將jar包加入classpath,然后在編譯dex文件時,將jar包一起編譯進去就可以了,但是對于aar文件,就稍微有點復雜了。
首先,我們還是創建一個工程,如圖:
這個工程依賴了design包,v7包中的appcompat,同時,上述這些包又依賴了v4包,,recyclerview,support-vector-drawable,animated-vector-drawable,support-annotations。
上述這些依賴都是Android Lib工程,因此我們需要處理依賴包中的資源。首先,我們需要這些依賴的aar文件作為輸入,到哪里去找aar文件呢?最初,我在sdk下找到了這些依賴的jar包和相應的資源目錄,但是,當我嘗試編譯的時候,總是提示我找不到資源,我很苦惱,后來在高旭大神的指點下,我看了一下gradle的實現方式,發現gradle并不使用jar包+資源來重新編譯這些依賴庫而是直接使用了google提供的這些依賴庫的aar文件,于是我嘗試將編譯好的aar文件解包,再使用解包后的資源和jar包進行編譯。
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
參數用來加載多個資源目錄,按照從左向右的順序,如果后面的資源重復則跳過,如果不重復則新增。
--extra-packages
用來對不同的資源目錄生成包名不同的R文件,多個包名通過:
分割。
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
后面的簽名、對齊操作和之前一樣
最后,我們在模擬器上運行一下打包后的apk文件,如圖:
0x03 總結
編譯流程的簡單分析就是這些,在上述流程中我們可以看到,主要過程是資源處理和dex文件生成上,其中對資源的處理是插件化的一個難點,我的分析并不是很全面,比如對于多個資源目錄合并的過程,aapt自身提供的機制和gradle的實現就不太一樣,gradle在最終調用aapt之前已經將資源合并,傳入aapt的只有一個合并后的資源目錄,可以參考gradle 資源合并機制,后續我會針對資源文件的處理做單獨的分析。
上述內容如有錯誤,懇請指正,我會繼續分析插件化的相關技術實現,敬請期待。