Android Apk 編譯打包流程,瞭解一下~
theme: smartblue highlight: a11y-dark
前言
作為一個Android
開發,每天都會有相當一部分的時間花在編譯打包上,如果專案比較大的話編譯一次可能就要十幾分鍾。
那麼在編譯打包的過程中AGP
到底做了什麼?為什麼編譯那麼耗時,又該怎麼優化?要解決這些問題,首先就需要我們對編譯打包的流程有個總體的瞭解
本文主要包括以下內容
1. 編譯打包總體流程
2. 編譯打包主要步驟
3. 編譯打包過程中的Task
編譯打包總體流程
首先看下Android
官網給出的編譯打包總體流程
典型 Android
應用的構建流程如圖所示,主要分為以下幾步:
1. 編譯器將您的原始碼轉換成 DEX
檔案(Dalvik
可執行檔案,其中包括在 Android
裝置上執行的位元組碼),並將其他所有內容轉換成編譯後的資源。
2. 打包器將 DEX
檔案和編譯後的資源組合成 APK
或 AAB
(具體取決於所選的 build
目標)。
3. 打包器使用除錯或釋出金鑰庫為 APK
或 AAB
簽名。
4. 在生成最終 APK
之前,打包器會使用 zipalign
工具對應用進行優化,以減少其在裝置上執行時所佔用的記憶體
編譯打包主要步驟
關於Android
編譯打包還有一張更加複雜的圖
這個看起來是相當複雜的,但其實我們也可以把這些步驟做一個分類,跟總體流程的四個步驟做一個對應
資源與程式碼編譯
資原始檔編譯
apk
資源包含:
- 工程中res
目錄下的所有檔案
- assets
目錄下的檔案
- AndroidManifest.xml
apk
的資源編譯是編譯過程中的一項主要工作,AGP3.0.0
之後預設通過AAPT2
來編譯資源。
AAPT2
(Android Asset Packaging Tool2
)是一種構建工具,Android Studio
和 Android Gradle
外掛使用它來編譯和打包應用的資源。AAPT2
會解析資源、為資源編制索引,並將資源編譯為針對 Android
平臺進行過優化的二進位制格式。
AAPT2
做了什麼優化?
為什麼AGP3.0.0
之後預設通過AAPT2
來編譯資源呢?它又做了什麼優化呢?
AAPT2
支援通過啟用增量編譯實現更快的資源編譯。這是通過將資源處理拆分為兩個步驟來實現的:
-
1、編譯:將資原始檔編譯為二進位制格式。
把所有的Android
資原始檔進行解析,生成副檔名為.flat
的二進位制檔案。比如是png
圖片,那麼就會被壓縮處理,採用.png.flat
的副檔名。可以在build/intermediates/merged_res/
檔案下檢視生成的中間產物 -
2、連結:合併所有已編譯的檔案並將它們打包到一個軟體包中。
首先,這一步會生成輔助檔案,比如R.java
與resources.arsc
,R
檔案大家應該都比較熟悉,就是一個資源索引檔案,我們平時引用也都是通過R.
的方式引用資源id
。而resources.arsc
則是資源索引表,供在程式執行時根據id索引到具體的資源
最後,會將R
檔案,ressources.arsc
檔案和之前的二進位制檔案進行打包,打包到一個軟體包中。
這種拆分方式有助於提高增量編譯的效能。例如,如果某個檔案中有更改,您只需要重新編譯該檔案。
AIDL
檔案編譯
對於AIDL
,大家應該都很熟悉,它是一種用於程序間通訊的介面檔案。
其實它是Google
為了幫助我們進行程序間通訊的簡便寫法,最後還是需要被解析編譯為java
檔案,而做這個工作的就是aidl
工具,存在於sdk/build-tools
目錄。
這個階段的主要的工作就是將專案中的aidl
檔案編譯為java
檔案。
Java
與Kotlin
檔案編譯
- 通過
Java Compiler
編譯專案中所有的Java
程式碼,包括R.java
、.aidl
檔案生成的.java
檔案、Java
原始檔,生成.class
檔案。在對應的build
目錄下可以找到相關的程式碼 - 通過
Kotlin Compiler
編譯專案中的所有Kotlin
程式碼,生成.class檔案
註解處理器(APT
,KAPT
)生成程式碼也是在這個階段生成的。當註解的生命週期被設定為CLASS
的時候,就代表該註解會在編譯class
檔案的時候生效,並且生成java
原始檔和Class
位元組碼檔案。
Class
檔案打包成DEX
這一步就是將.class
檔案打包成dex
檔案。
有人可能會奇怪了,.class
檔案不就是JVM
可以識別的二進位制檔案嗎,為什麼還要進行一次轉化呢?
這就涉及到另一個問題:JVM
和 Dalvik(ART
的區別。
其中一個重要的區別就是Dalvik(ART)
有自己的二進位制檔案,也就是.dex
檔案,所以需要將class
檔案進行再一次轉換。
你可以把dex
檔案理解為一個class
檔案包,裡面裝著很多的class
檔案,讓這些類能夠共享資料,類似這種關係:
D8
編譯器與R8
工具
在 AGP 3.X
以後,Google
分別引入 D8
編譯器和 R8
工具作為預設的 DEX
編譯器和混淆壓縮工具。
- 在
AGP3.0.1
之後,D8
編譯器取代了Dx
,用於將class
檔案打包成DEX
,D8
編譯器編譯更快、時間更短;DEX
編譯時佔用內容更小;生成的dex
檔案大小更小;同時擁有相同或者是更好的執行時效能; - 在
AGP3.4.0
之後,預設開啟R8
,R8
是ProGuard
的替代工具,用於程式碼的壓縮(shrinking
)和混淆(obfuscation
)
在 AGP3.4.0
版本中,R8
把 desugaring
、shrinking
、obfuscating
、optimizing
和 dexing
都合併到一步進行執行。在 AGP3.4.0
以前的版本編譯流程如下:
在AGP3.4.0
之後的編譯流程如下:
生成APK
包
在資原始檔與程式碼檔案都編譯完成後,接下來就是生成apk
包了,將manifest
檔案、resources
檔案、dex
檔案、assets
檔案等等打包成一個壓縮包,也就是apk
檔案。
在老版本使用的工具是apkbuilder
,但是在最新的版本我發現沒有這個工具了,sdk
目錄下也找不到了。
在AGP3.6.0
之後,使用zipflinger
作為預設打包工具來構建APK
,以提高構建速度
zipalign
(對齊處理)
zipalign
是一種歸檔對齊工具,可對 Android
應用 (APK
) 檔案提供重要的優化
zipalign
會對apk
中未壓縮的資料進行4位元組對齊,對齊的主要過程是將APK
包中所有的資原始檔距離檔案起始偏移為4位元組整數倍,對齊後就可以使用mmap
函式讀取檔案,可以像讀取記憶體一樣對普通檔案進行操作。如果沒有4位元組對齊,就必須顯式的讀取,這樣比較緩慢並且會耗費額外的記憶體。
有的同學可能會有疑問,這個對齊處理不是應該放在簽名之後嗎?其實這裡就涉及到了簽名工具的不同帶來的對齊處理的順序不同:
- 如果使用的是
apksigner
,只能在為APK
檔案簽名之前執行zipalign
。 - 如果使用的是
jarsigner
,只能在為APK
檔案簽名之後執行zipalign
。
對APK
進行簽名
在生成APK檔案之後,必須對該apk
檔案進行簽名,否則無法被安裝。
之前大家比較熟知的簽名工具是JDK
提供的jarsigner
,而apksigner
是Google
專門為Android
提供的簽名和簽證工具。
其區別就在於jarsigner
只能進行v1
簽名,而apksigner
可以進行v2
、v3
、v4
簽名。下面我們簡單介紹下V1
簽名和V2
簽名的區別,關於V3
,V4
簽名的內容可參考:Android開發應該知道的簽名知識!
V1
簽名
v1
簽名方式主要是利用META-INFO
資料夾中以MF
、SF
和 RSA
的三個檔案,流程如下所示:
首先,將apk
中除了META-INFO
資料夾中的所有檔案進行進行摘要寫到 META-INFO/MANIFEST.MF
;然後計算MANIFEST.MF
檔案的摘要寫到CERT.SF
;最後計算CERT.SF
的摘要,使用私鑰計算簽名,將簽名和開發者證書寫到CERT.RSA
。
所以META-INFO
資料夾中這三個檔案就能保證apk
不會被修改。但是V1
簽名方案主要有兩個問題
- 一是簽名校驗慢,在簽名校驗時要針對
Apk
中所有的檔案進行校驗,這會拖累老裝置的安裝時間。 - 二是
META-INFO
資料夾不會被簽名,存在一定安全隱患
V2
簽名
Android7.0
之後,Google
推出了V2
簽名,解決V1
簽名速度慢以及簽名不完整的問題。
apk本質上是一個壓縮包,而壓縮包檔案格式一般分為三塊:
檔案資料區,中央目錄,中央目錄結束節。
而V2
要做的就是,在檔案中插入一個APK
簽名分塊,位於中央目錄部分之前,如下圖:
這樣處理之後,檔案簽名完成就無法修改了,這也是為什麼ZipAlign
對齊只能在ApkSigner
簽名之前執行的原因。
編譯打包過程中的Task
上面介紹了Apk
編譯打包過程的主要步驟,這些步驟也都是通過AGP
外掛實現的,那麼這些主要步驟又對應AGP
中的哪些Task
呢
當我們在Android Studio
中點選Run
時,便可以在控制檯看到一系列的Task
執行
``` Executing tasks: [:app:assembleDebug] in project
Task :app:preBuild UP-TO-DATE Task :app:preDebugBuild UP-TO-DATE Task :app:mergeDebugNativeDebugMetadata NO-SOURCE Task :app:compileDebugAidl NO-SOURCE Task :app:compileDebugRenderscript NO-SOURCE Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE Task :app:dataBindingMergeGenClassesDebug UP-TO-DATE Task :app:generateDebugResValues UP-TO-DATE Task :app:generateDebugResources UP-TO-DATE Task :app:mergeDebugResources UP-TO-DATE Task :app:packageDebugResources UP-TO-DATE Task :app:parseDebugLocalResources UP-TO-DATE Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE Task :app:generateDebugBuildConfig UP-TO-DATE Task :app:checkDebugAarMetadata UP-TO-DATE Task :app:mapDebugSourceSetPaths UP-TO-DATE Task :app:createDebugCompatibleScreenManifests UP-TO-DATE Task :app:extractDeepLinksDebug UP-TO-DATE Task :app:processDebugMainManifest UP-TO-DATE Task :app:processDebugManifest UP-TO-DATE Task :app:processDebugManifestForPackage UP-TO-DATE Task :app:processDebugResources UP-TO-DATE Task :app:javaPreCompileDebug UP-TO-DATE Task :app:mergeDebugShaders UP-TO-DATE Task :app:compileDebugShaders NO-SOURCE Task :app:generateDebugAssets UP-TO-DATE Task :app:mergeDebugAssets UP-TO-DATE Task :app:compressDebugAssets UP-TO-DATE Task :app:processDebugJavaRes NO-SOURCE Task :app:checkDebugDuplicateClasses UP-TO-DATE Task :app:desugarDebugFileDependencies UP-TO-DATE Task :app:mergeExtDexDebug UP-TO-DATE Task :app:mergeLibDexDebug UP-TO-DATE Task :app:mergeDebugJniLibFolders UP-TO-DATE Task :app:mergeDebugNativeLibs NO-SOURCE Task :app:stripDebugDebugSymbols NO-SOURCE Task :app:validateSigningDebug UP-TO-DATE Task :app:writeDebugAppMetadata UP-TO-DATE Task :app:writeDebugSigningConfigVersions UP-TO-DATE Task :app:compileDebugKotlin Task :app:compileDebugJavaWithJavac Task :app:mergeDebugJavaResource UP-TO-DATE Task :app:dexBuilderDebug UP-TO-DATE Task :app:mergeProjectDexDebug Task :app:packageDebug Task :app:createDebugApkListingFileRedirect UP-TO-DATE Task :app:assembleDebug
BUILD SUCCESSFUL in 2s 35 actionable tasks: 4 executed, 31 up-to-date ```
上面就是點選執行過程中執行的所有Task
,我們精簡一下,列出上面主要步驟中提到的Task
``` //aidl 轉換aidl檔案為java檔案
Task :app:compileDebugAidl
//生成BuildConfig檔案
Task :app:generateDebugBuildConfig
//獲取gradle中配置的資原始檔
Task :app:generateDebugResValues
// merge資原始檔,AAPT2 編譯階段
Task :app:mergeDebugResources
// merge assets檔案
Task :app:mergeDebugAssets Task :app:compressDebugAssets
// merge所有的manifest檔案
Task :app:processDebugManifest
//生成R檔案 AAPT2 連結階段
Task :app:processDebugResources
//編譯kotlin檔案
Task :app:compileDebugKotlin
//javac 編譯java檔案
Task :app:compileDebugJavaWithJavac
//轉換class檔案為dex檔案
Task :app:dexBuilderDebug
//打包成apk並簽名
Task :app:packageDebug ```
上面這些Task
就對應於上面說的編譯過程中的主要步驟,比如mergeDebugResources
就對應於AAPT2
的編譯階段,在Task
結束後,會在build/intermediates/merged_res/
資料夾中生成Flat
檔案
而processDebugResources
則對應於AAPT2
的連結階段,會生成R.java
與resources.arsc
,併合並所有已編譯的檔案並將它們打包到一個軟體包中
關於其他Task
內容也都比較多,感興趣的同學可以自行檢視相關原始碼,這裡就不綴述了
總結
本文主要詳細介紹了Android APK
打包編譯的總體流程,主要步驟,以及AGP
中相關的Task
。這些知識點在平常的開發中或許沒有多大用處,但是如果你要做包體積優化,或者編譯優化相關的一些工作的話,這些應該是需要了解的前置知識,希望對你有所幫助~
參考資料
Android&Kotlin編譯速度原理剖析(上)
從構建工具看 Android APK 編譯打包流程
Android D8 編譯器 和 R8 工具
Android開發應該知道的簽名知識!
- kotlin-android-extensions 外掛到底是怎麼實現的?
- 江同學的 2022 年終總結,請查收~
- kotlin-android-extensions 外掛將被正式移除,如何無縫遷移?
- 學習一下 nowinandroid 的構建指令碼
- Kotlin 預設可見性為 public,是不是一個好的設計?
- 2022年編譯加速的8個實用技巧
- 落地 Kotlin 程式碼規範,DeteKt 瞭解一下~
- Gradle 進階(二):如何優化 Task 的效能?
- 開發一個支援跨平臺的 Kotlin 編譯器外掛
- 開發你的第一個 Kotlin 編譯器外掛
- Kotlin 增量編譯是怎麼實現的?
- Gradle 都做了哪些快取?
- K2 編譯器是什麼?世界第二高峰又是哪座?
- Android 效能優化之 R 檔案優化詳解
- Kotlin 快速編譯背後的黑科技,瞭解一下~
- 別了 KAPT , 使用 KSP 快速實現 ButterKnife
- Android Apk 編譯打包流程,瞭解一下~
- 如何優雅地擴充套件 AGP 外掛
- ASM 插樁採集方法入參,出參及耗時資訊
- Transform 被廢棄,ASM 如何適配?