最近想要研究熱修復的原理,并自己實現一套簡單的熱修復框架(音視頻的坑剛挖好就又挖另一個坑了2333),已經在看書入門 c++,方便后續查看業內一些知名熱修復框架的源碼,另外在涉及編譯期/運行時修改代碼之前,先了解一下 apk 是如何從項目源碼被打包生成的,可能對后續的從思路上或實際操作都會所裨益。
文中涉及到的工具所在目錄:Android/sdk/build-tools
。下面開始分解并逐步實現對源碼的打包。
編譯流程
1. 生成僅包含資源文件的 apk 包和 R.java 文件
根據資源文件和 AndroidManifest.xml 由工具 AAPT 生成 R.java 文件。Android Gradle Plugin 3.0.0 以后默認使用 AAPT2,詳見 AAPT2 官方文檔。來看一下 AAPT2 的使用:
預編譯
編譯所有 Android 支持的資源文件。可以通過編譯語句將單個資源文件編譯成 .flat
后綴的過渡二進制文件
AAPT 可以編譯單個文件,例如編譯 strings.xml,會生成 values_strings.arsc.flat 文件:
aapt2 compile app/src/main/res/values/strings.xml -o test/
但一個項目不可能只有一個資源文件,通常都是編譯 整個 res 資源目錄,會生成 zip 壓縮包,包含了所有資源文件編譯后的 flat 格式文件:
aapt2 compile --dir app/src/main/res/ -o package/res.zip
參數含義:
- --dir:指定輸入目錄
- -o: 指定輸出目錄(如果輸入源是文件夾,則需要指定編譯后的(zip)文件名)
鏈接(link)
將預編譯生成的過渡二進制文件合并并打包成單獨的 APK 包,R 文件和 ProGuard 規則文件也是在這個時期生成的,生成的 APK 包不包含 DEX 字節碼并且是未簽名的(后續可使用 D8 編譯工具將 Java 字節碼編譯成 DEX 字節碼,使用 apksigner 對 APK 簽名)
aapt2 link package/res.zip \
-I ~/Library/Android/sdk/platforms/android-27/android.jar \
--java package/ \
--manifest app/src/main/AndroidManifest.xml \
-o package/res.apk
參數含義:
- -I:必要參數,指定 android.jar 目錄,因為 xml 中可能使用到了例如 android:id 等自帶的 android 命名空間
- o:指定輸出 apk 路徑
- --java:指定生成的 R 文件的路徑
- --manifest:必要參數,Manifest 文件中包含了 app 的包名和 application id
執行上述命令后報錯:style/Theme.AppCompat.Light.DarkActionBar not found.
以及 layout_constraintBottom_toBottomOf (新建的工程默認依賴了 constraint-layout庫)等各種 not found。
報這些錯是因為 link 時沒有引入第三方庫,在 Google 文檔里沒有找到相關的命令,所以先移除這些依賴,跑通整個流程后再回頭看怎么解決。
// AppTheme 暫時先移除對 Theme.Appcompat 的依賴
<style name="AppTheme" parent="android:Theme.Holo.Light.DarkActionBar">
</style>
再執行一遍命令,可以看到指定的輸出目錄已經生成了 apk 包和 R 文件:
2. 處理aidl,生成對應的java文件
因為 demo 沒有涉及到 aidl,暫且跳過。
3. 編譯 .java 文件為 .class 文件
編譯項目 src 目錄下所有 .java 文件還有之前生成的 R.java 、aidl 生成的 java 文件為相應的的 class 文件
javac -encoding utf-8 \
-target 1.8 \
-bootclasspath ~/Library/Android/sdk/platforms/android-27/android.jar \
app/src/main/java/com/yazhidev/demo/*.java package/com/yazhidev/demo/R.java \
-d package/
參數含義:
- -encoding: 指定編碼方式為 uts-8
- -target:指定 Java 版本號
- -bootclasspath:引入 Android.jar 包內的類
- -d:指定編譯生成的字節碼文件存放的路徑
4. class 文件編譯為 dex 文件
dex 文件是 Android 虛擬機所能識別、解析并運行的文件。Java 源文件被編譯為 class 文件后,需要通過 dex 編譯器將多個 class 文件整合為一個 dex 文件,從 Android Studio 3.1 開始,已經使用 D8 替代原先的 DX 作為默認的 dex 編譯器。D8 的使用很簡單:
d8 package/com/yazhidev/demo/*.class \
--classpath ~/Library/Android/sdk/platforms/android-27/android.jar \
--output ./
參數含義:
- --classpath:指定編譯需要引用到的類
- --output:指定編譯后生成的 .dex 文件的存放路徑
5. 將 dex 文件添加進 apk 包
原本這步是通過 apkbuilder 腳本來做的,現在改成用 aapt 命令來做。
aapt add package/res.apk classes.dex
需要注意的是 dex 文件前不能加路徑,否則會將路徑帶入 apk 包中。
6. 優化對齊 apk 文件
apksigner 文檔中提到,如果使用 apksigner 對 apk 簽名,則需要在簽名之前使用 zipalign 優化對齊。
zipalign 的使用很簡單:
zipalign 4 package/res.apk package/app-unsigned-aligned.apk
4 代表 32 位對齊,zipalign 可以確保所有未壓縮的數據的開頭均相對于文件開頭部分執行特定的字節對齊,這樣可減少應用消耗的 RAM 量。
7. 簽名
簽名需要私鑰,可以通過 Android Studio 生成,也可通過 JDK bin 目錄下的 keytool 工具生成。keytool 以及 apksigner 的使用可參考:從命令行構建和簽署您的應用
我自己這么就直接使用現有的 key 簽名,簽名的命令很簡單:
//apk 簽名
apksigner sign --ks key.jks --out package/app-release.apk package/app-unsigned-aligned.apk
參數含義:
- --ks:指定私鑰文件
- --out:指定簽名后的 apk 輸出路徑
//檢查 apk 的簽名
apksigner verify app.apk
至此,就完成了 Android 源碼打包成 apk 的整個流程,當然以上只是最簡單的情況,對于第三方庫、多 module 等情景下的打包流程都沒有涉及。將 apk 安裝到手機上,可以正常打開:
一打開首頁我的內心的拒絕的,這首頁可以說是丑出天際。但起碼從 0 到 1 了(化身阿 Q 瘋狂自我安慰),一番操作算是對 apk 的打包流程有了個籠統的認識,后面要了解一下 app 啟動流程,為熱修復的學習打基礎。(也許后面會回來填坑?)