Android Apk 編譯打包流程,瞭解一下~

語言: CN / TW / HK

theme: smartblue highlight: a11y-dark


前言

作為一個Android開發,每天都會有相當一部分的時間花在編譯打包上,如果專案比較大的話編譯一次可能就要十幾分鍾。

那麼在編譯打包的過程中AGP到底做了什麼?為什麼編譯那麼耗時,又該怎麼優化?要解決這些問題,首先就需要我們對編譯打包的流程有個總體的瞭解

本文主要包括以下內容
1. 編譯打包總體流程 2. 編譯打包主要步驟 3. 編譯打包過程中的Task

編譯打包總體流程

首先看下Android官網給出的編譯打包總體流程

典型 Android 應用的構建流程如圖所示,主要分為以下幾步:
1. 編譯器將您的原始碼轉換成 DEX 檔案(Dalvik 可執行檔案,其中包括在 Android 裝置上執行的位元組碼),並將其他所有內容轉換成編譯後的資源。 2. 打包器將 DEX 檔案和編譯後的資源組合成 APKAAB(具體取決於所選的 build 目標)。 3. 打包器使用除錯或釋出金鑰庫為 APKAAB 簽名。 4. 在生成最終 APK 之前,打包器會使用 zipalign 工具對應用進行優化,以減少其在裝置上執行時所佔用的記憶體

編譯打包主要步驟

關於Android編譯打包還有一張更加複雜的圖

這個看起來是相當複雜的,但其實我們也可以把這些步驟做一個分類,跟總體流程的四個步驟做一個對應

資源與程式碼編譯

資原始檔編譯

apk資源包含:
- 工程中res目錄下的所有檔案 - assets目錄下的檔案 - AndroidManifest.xml

apk的資源編譯是編譯過程中的一項主要工作,AGP3.0.0之後預設通過AAPT2來編譯資源。

AAPT2Android Asset Packaging Tool2)是一種構建工具,Android StudioAndroid Gradle 外掛使用它來編譯和打包應用的資源。AAPT2 會解析資源、為資源編制索引,並將資源編譯為針對 Android平臺進行過優化的二進位制格式。

AAPT2做了什麼優化?

為什麼AGP3.0.0之後預設通過AAPT2來編譯資源呢?它又做了什麼優化呢?

AAPT2 支援通過啟用增量編譯實現更快的資源編譯。這是通過將資源處理拆分為兩個步驟來實現的:

  • 1、編譯:將資原始檔編譯為二進位制格式。
    把所有的Android資原始檔進行解析,生成副檔名為.flat的二進位制檔案。比如是png圖片,那麼就會被壓縮處理,採用.png.flat的副檔名。可以在build/intermediates/merged_res/檔案下檢視生成的中間產物

  • 2、連結:合併所有已編譯的檔案並將它們打包到一個軟體包中。
    首先,這一步會生成輔助檔案,比如R.javaresources.arscR檔案大家應該都比較熟悉,就是一個資源索引檔案,我們平時引用也都是通過R.的方式引用資源id。而resources.arsc則是資源索引表,供在程式執行時根據id索引到具體的資源
    最後,會將R檔案,ressources.arsc檔案和之前的二進位制檔案進行打包,打包到一個軟體包中。

這種拆分方式有助於提高增量編譯的效能。例如,如果某個檔案中有更改,您只需要重新編譯該檔案。

AIDL檔案編譯

對於AIDL,大家應該都很熟悉,它是一種用於程序間通訊的介面檔案。

其實它是Google為了幫助我們進行程序間通訊的簡便寫法,最後還是需要被解析編譯為java檔案,而做這個工作的就是aidl工具,存在於sdk/build-tools目錄。

這個階段的主要的工作就是將專案中的aidl檔案編譯為java檔案

JavaKotlin檔案編譯

  • 通過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可以識別的二進位制檔案嗎,為什麼還要進行一次轉化呢?

這就涉及到另一個問題:JVMDalvik(ART 的區別。

其中一個重要的區別就是Dalvik(ART)有自己的二進位制檔案,也就是.dex檔案,所以需要將class檔案進行再一次轉換。

你可以把dex檔案理解為一個class檔案包,裡面裝著很多的class檔案,讓這些類能夠共享資料,類似這種關係:

D8編譯器與R8工具

AGP 3.X 以後,Google 分別引入 D8 編譯器和 R8 工具作為預設的 DEX 編譯器和混淆壓縮工具。

  • AGP3.0.1之後,D8編譯器取代了Dx,用於將class檔案打包成DEXD8編譯器編譯更快、時間更短;DEX 編譯時佔用內容更小;生成的dex檔案大小更小;同時擁有相同或者是更好的執行時效能;
  • AGP3.4.0之後,預設開啟R8R8ProGuard 的替代工具,用於程式碼的壓縮(shrinking)和混淆(obfuscation

AGP3.4.0版本中,R8desugaringshrinkingobfuscatingoptimizingdexing 都合併到一步進行執行。在 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,而apksignerGoogle專門為Android提供的簽名和簽證工具。

其區別就在於jarsigner只能進行v1簽名,而apksigner可以進行v2v3v4簽名。下面我們簡單介紹下V1簽名和V2簽名的區別,關於V3,V4簽名的內容可參考:Android開發應該知道的簽名知識!

V1簽名

v1簽名方式主要是利用META-INFO資料夾中以MFSFRSA 的三個檔案,流程如下所示:

首先,將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.javaresources.arsc,併合並所有已編譯的檔案並將它們打包到一個軟體包中

關於其他Task內容也都比較多,感興趣的同學可以自行檢視相關原始碼,這裡就不綴述了

總結

本文主要詳細介紹了Android APK打包編譯的總體流程,主要步驟,以及AGP中相關的Task。這些知識點在平常的開發中或許沒有多大用處,但是如果你要做包體積優化,或者編譯優化相關的一些工作的話,這些應該是需要了解的前置知識,希望對你有所幫助~

參考資料

Android&Kotlin編譯速度原理剖析(上)
從構建工具看 Android APK 編譯打包流程
Android D8 編譯器 和 R8 工具
Android開發應該知道的簽名知識!