
APK
的誕生
上述以前在其餘文章裏面也常見的圖,而這張圖講述一個
APK
的誕生流程,能夠分爲如下的幾個流程html
-
aapt
工具將資源文件轉化出對應的R
文件和編譯好的文件, 可是這類資源文件中不包含assets
目錄下的文件。 -
aidl
工具將aidl
文件轉化爲Java
代碼。 -
Java Compiler
工具將上述二者以及咱們書寫好的源代碼進行整合生成咱們所熟悉的Class
文件。 -
dex
工具將會將第三方庫和Class
文件轉化二進制dex
文件。 -
apkbuilder
工具將編譯好的資源文件、源碼的二進制文件以及assets
下的資源文件內聯最後生成咱們見到的apk
文件。 -
signer
工具用於簽名使用(由於簽名工具並不侷限於Jarsigner
) -
zipalign
工具幫助優化資源索引。
aapt
/ 資源編譯階段
aapt
工具位於Android
目錄下的build-tools
中java具體使用方法請參考AAPT2[1]android
AAPT2
支持編譯res
文件目錄下的資源。調用AAPT2
進行編譯時,每次調用都應傳遞一個資源文件做爲輸入。而後,AAPT2
會解析該文件並生成一個擴展名爲.flat
的中間二進制文件。而這個二進制文件就對應着圖中的Compiled Resources
。git
aapt2 compile project_root/module_root/src/main/res/values-en/strings.xml -o compiled/
上面的我的感受比較坑的地方,
compile
後面跟着的路徑必須是完整的,但而-o
後面的又用的相對路徑。最後的結果就是如上面所說會生成一個以.flat
爲後綴的二進制文件。github
下面給出各類不一樣文件類型下將會獲得的輸出:web
輸入 | 輸出 |
---|---|
XML 資源文件(如 String 和 Style),它們位於 res/values/ 目錄下。 | 以 *.arsc.flat 做爲擴展名的資源表。 |
其餘全部資源文件 | 除 res/values/ 目錄下的文件之外的其餘全部文件都將轉換爲擴展名爲 *.flat 的二進制 XML 文件。此外,默認狀況下,全部 PNG 文件都會被壓縮,並採用 *.png.flat 擴展名。若是選擇不壓縮 PNG,您能夠在編譯期間使用 --no-crunch 選項。 |
另外aapt
工具的link
連接功能還會生成咱們一個R
的文件用於資源的惟一標示。算法
aapt2 link path-to-input-files [options] -o outputdirectory/outputfilename.apk
--manifest AndroidManifest.xml
經過定向的連接的能夠實現增量連接的效果。spring
Android Studio
自帶工具,點開APK
就可以直接解析。後端
咱們能夠將整個
int
數值分爲4個字節:緩存
-
第一位字節 0x7f
表示packageID
,用來限定資源的來源。應用資源是0x7f
,系統資源是0x01
-
次一位字節 01
表示typeID
,用來表示資源類型,如drawable
、layout
、menu
等,下一個資源的typeID
則會是02
-
後2字節 0000
指的是每個資源在對應的typeID
中出現的順序。(給出的存儲空間範圍比較大)
可是咱們在APK
解析的文件中會找到這樣的一個文件resources.arsc
,這個文件的生成一樣伴隨aapt
的連接而來。
拋去純數值的文件不講,着重看一下layout
文件可以發現v17
、watch-v20
。。其實爲佈局顯示時留出了不一樣版本選擇空間,若是你再看一下mipmap
或者drawable
還會爲不一樣的屏幕尺寸留出了選擇的餘地。
Q1:R.java和resources.arsc文件做用是什麼?
A1:resources.arsc
爲應用程序在運行時同時支持不一樣大小、密度的屏幕以及不一樣語言等提供可能。R
文件爲資源設置了惟一標示,從而可讓應用程序可以根據設備的當前配置信息來快速索引到匹配資源。
Java Compile + Dex
/ 代碼編譯
項目中其實咱們更多時候已經用Android Studio
提供的Build
功能完成了,而這同樣的能力提供方就包括Gradle
。
Gradle
是幹嗎用的?
在 關於Python的小小分享[2]曾分享過這樣一張圖。其實
Gradle
的其中一項能力就是爲咱們提供不一樣三方庫之間的依賴關係,而基礎就是Java
,因此在Build
的這樣過程當中咱們常常會看到相似這樣的一個Task
。
在正式接觸Gradle
的打包流程以前有必要了解一下什麼是Gradle
,先看下面的一段xml
文件內容。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
</dependencies>
若是你曾經作事後端開發,那麼開發中確定是常常接觸到這樣的xml
文件編寫,但這個文件格式書寫辨識能力來自於與他具備相同能力的同伴maven
。
dependencies {
compile('org.springframework:spring-context:2.5.6')
testCompile('junit:junit:4.7')
}
而像Gradle
是基於本身定義的語法來完成依賴解析,呈現方式上更是一目瞭然。那說到這裏我仍是沒去介紹Gradle
這個工具他的做用究竟是什麼?往簡單了說,就是一個項目自動構建工具唄。可是這樣的一個工具在咱們的開發過程當中到底佔到怎麼一個不可或缺的位置呢?咱們來縱觀一下一個apk
的打包流程就能夠直到他幹了什麼事情了。在寫代碼的時候咱們關注點是什麼?一般會有如下幾類:
-
源代碼文件:包括 Kotlin
、Java
、C
、AIDL
等等文件。 -
資源文件:圖片、視頻、佈局等等文件。 -
R
文件,各種資源的惟一標識。
完成以上內容的編寫,咱們可能結束了代碼編寫,而後用了一下Android Studio
中提供的各項能力。若是不出所料,你的項目就飛快的在你的測試機上開始開心的運行了。
可能到這裏你尚未感受,但若是看了這張圖呢?是否能切實的感覺到
Gradle
所提供的強大能力了,由於對咱們咱們開發者而言其實只幹了一件運行按鈕的觸發操做,可是背後Gradle
給咱們所帶來的收益是無窮無盡的。
在這裏咱們知道他頗有用了,可是爲何還要提一下他的兄弟Maven
呢?主要是爲了讓你轉化手頭的構建工具,根據官網的構建速度對比。
具體請參考文檔Gradle vs Maven:性能比較[3]

由於公司裏通常的項目都是組件化的,並且接入方會不少不少不少,因此咱們拿一個大型構建的時間對比可能更服人心。對於乾淨的構建,Gradle的速度提升了2-3倍,對於增量更改,Gradle的速度提升了約7倍,而對Gradle任務輸出進行緩存時,Gradle的速度提升了3倍。 如此之高的構建效率提高對咱們開發者而言確定也是有利有「弊」的,好比說我做爲一個抖音開發者,本來抖音的構建工具使用的是Maven
他的增量編譯構建速度本來20分鐘完成一次,那說明我如今有20分鐘的摸魚時間了,可是若是我一天要編譯10次20次呢?整體這樣折算下來一天的工做效率能夠說骨折式縮短,可能由於編譯效率太低,致使你沒法按時完成需求年終獎一無全部。可是用了Gradle
之後,效率翻倍,每次增量編譯只用10分鐘就完成了,雖然摸魚時間短了,可是效率上來了,老闆說你表現優異又給你加了3個月的獎金。
迴歸這個主題的內容,Gradle
是怎麼爲咱們提供能力的?
Proguard
+ Dex
Dex
工具就是將Class
文件轉換成二進制這裏就不作介紹
在關於proguard
的內容上,對於8成的開發者阮大概最熟悉的內容就是混淆了。
Q1:混淆帶給咱們的好處有什麼?
A1: 爲何咱們要混淆?很簡單,不想讓第三者輕易得到咱們開發的app
源碼,那他的第一個優點就出來了,讓代碼失去直觀的語義,讓一部分想竊取公司機密的外部業餘黑客望而卻步。其實這個工具還給咱們帶來了第二個優點,就是代碼內容縮短,在總體的包體積縮小起到了相當重要的做用。
那Proguard
只有這麼點做用嗎??顯然並非這樣的。從圖中能夠得知,
Proguard
針對的部分是拋去系統庫的,因此在混淆的圖中可以發現android.support
的庫仍是清晰的顯示着,我的考慮是由於若是加上系統庫進行混淆的話,可能引來奇怪的Bug
。
咱們將總體分爲4個部分:
-
shrink
—— 代碼刪減 -
optimize
—— 指令優化 -
obfuscate
—— 代碼混淆 -
preverify
—— 代碼校驗
Shrink
做爲代碼刪減確定是有刪減的入口的。ProGuard
會根據Configuration Roots
開始標記, 同時根據Roots
爲入口開始發散。標記完成之後, 刪除未被標記的類或成員。最終獲得的是精簡的ClassPool
。
Q1:那這些Roots
的來源是什麼呢?
A1:Roots
包括類,方法字段,方法指令, 來源主要有2種。
-
經過 keep
同時allowshrinking
不爲true
。計算class_specification
中類限定和限定成員 -
經過 keepclasseswithmembers
關鍵字allowshrinking
不爲true
。若是類限定和成員限定都存在。計算class_specification
中類限定和成員限定。
Q2:刪除的是那些代碼?
A2: 其實刪除的內容就是在全局範圍內並無調用點而且沒有用keep
去保留的方法或者類。
Optimize
Optimize
會在該階段經過對 代碼指令、 堆棧, 局部變量以及數據流分析。來模擬程序運行中儘量出現的狀況來優化和簡化代碼. 爲了數據流分析的須要Optimize
會屢次遍歷全部字節碼ProGuard
會開啓多線程來加快速度。
具體的優化策略詳見於ProGuard 初探的 Optimize 部分[4]
Obfuscate
代碼混淆想來是咱們最爲常見的部分了。混淆部分一共會帶來兩部分的收益:
-
代碼失去直觀的語義(由於咱們的方法或者函數命名時都會有必定的規則) -
代碼內容縮短,縮小總體的包體積
Preverify
對代碼進行預校驗。 主要校驗StackMap / StackMapTable
屬性。android
虛擬機字節碼校驗不基於StackMap /StackMapTable
。
具體內容詳見於 ProGuard 初探[5]
D8
是
Dex
的替代產品
這一解析器的引入很是重要的目的是爲了適應
Java 8
上新概念Lambda
。Java
底層是經過invokedynamic
指令來實現,因爲Dalvik/ART
並無支持invokedynamic
指令或者對應的替代功能。簡單的來講,就是Android
的dex
編譯器不支持invokedynamic
指令,致使Android
不能直接支持Java 8
。
因此Android
作的事情就是間接支持,將Lambda
變化爲能夠解析的語法而後執行。將代碼編譯之後,咱們可以發現生成的代碼中會同時生成以
Lambda
來標識的類,這就是說明了他的解析方案,而代碼的實現方式就是咱們在Java 7
中常見的方案了。
不過你以爲新產品的提高會止步於此嗎?🤫
-
編譯速度的提高 -
編譯產生的 dex
文件體積縮小
R8
是
Proguard
+Dex
的替代產品
R8
中包含了D8
+R8
R8
做爲Proguard
的替代產品,繼承了原有的功能而且作出了拓展。那在
R8
這個工具上,開發者又作出了什麼樣的突破呢?從圖中可以比較直觀地看到,R8做爲集成物,將
ProGuard
+Dex
的能力集成,不只在編譯效率上提高,而且包大小的體積也有必定的收益
apkbuilder
的話就是一個集成工具了不作講解了
簽名
爲何Android
的程序須要簽名呢?是否常常遇到這樣的狀況,同一個項目兩個臺機器上運行到同一部手機中,咱們常常會碰到關於簽名不一樣的報錯。而後咱們的作法可能就是刪除,而後從新安裝,這樣就能解決問題了,但其實致使這個問題的緣由是簽名,若是兩臺機器使用了一樣的簽名,這個問題就自動解除了。
簽名爲咱們帶來了什麼樣的好處呢?
-
使用特殊的key簽名能夠獲取到一些不一樣的權限 -
驗證數據保證不被篡改,防止應用被惡意的第三方覆蓋
經過
Android Studio
的Generate Signed Bundle or APK
方法能夠看到上述的兩種簽名的方法:Jar Signature
和Full APK Signature
,那這兩種簽名方式又有什麼區別呢?
Jar Signature
/ v1
簽名經過
Jar Signature
在APK
的表現形式又是怎麼樣的呢?v1簽名過程很簡單,一共分爲了三個部分:
-
對非目錄文件以及過濾文件進行摘要,存儲在 MANIFEST.MF
文件中。 -
對 MANIFEST.MF
文件的進行摘要以及對MANIFEST.MF
文件的每一個條目內容進行摘要,存儲在CERT.SF
文件中。 -
使用指定的私鑰對 CERT.SF
文件計算簽名,而後將簽名以及包含公鑰信息的數字證書寫入CERT.RSA
。
從這個實現流程上其實可以明顯感受出來這個簽名模式確定是存在問題的,由於最後的簽名數據至關於說向外暴露了。只要稍微注意一下數據就可以把一個APK
反編譯改完之後再編譯回來。
Full APK Signature
/ v2
咱們知道了Jar Signature
的簽名方式,那如今這個新的簽名方式又是如何實現的呢?
APK
簽名方案v2
是一種全文件簽名方案,該方案可以發現對APK
的受保護部分進行的全部更改,從而有助於加快驗證速度並加強完整性保證。
使用APK
簽名方案v2
進行簽名時,會在APK
文件中插入一個APK
簽名分塊,該分塊位於「ZIP
中央目錄」部分以前並緊鄰該部分。在「APK
簽名分塊」內,v2
簽名和簽名者身份信息會存儲在APK
簽名方案v2
分塊中。

APK 簽名方案 v2 驗證

-
找到「APK 簽名分塊」並驗證如下內容: -
「APK 簽名分塊」的兩個大小字段包含相同的值。 -
「ZIP 中央目錄結尾」緊跟在「ZIP 中央目錄」記錄後面。 -
「ZIP 中央目錄結尾」以後沒有任何數據。 -
找到「APK 簽名分塊」中的第一個「APK 簽名方案 v2 分塊」。若是 v2 分塊存在,則繼續執行第 3 步。不然,回退至使用 v1 方案驗證 APK。 -
對「APK 簽名方案 v2 分塊」中的每一個 signer 執行如下操做: -
從 signatures 中選擇安全係數最高的受支持 signature algorithm ID。安全係數排序取決於各個實現/平臺版本。 -
使用 public key 並對照 signed data 驗證 signatures 中對應的 signature。(如今能夠安全地解析 signed data 了。) -
驗證 digests 和 signatures 中的簽名算法 ID 列表(有序列表)是否相同。(這是爲了防止刪除/添加簽名。) -
使用簽名算法所用的同一種摘要算法計算 APK 內容的摘要。 -
驗證計算出的摘要是否與 digests 中對應的 digest 一致。 -
驗證 certificates 中第一個 certificate 的 SubjectPublicKeyInfo 是否與 public key 相同。 -
若是找到了至少一個 signer,而且對於每一個找到的 signer,第 3 步都取得了成功,APK 驗證將會成功。
那問題來了,這個這個v2
的整塊數據是如何計算出來的呢?
v2的詳細計算過程請見於 APK 簽名方案 v2 分塊[6]

-
每一個部分都會被拆分紅多個大小爲 1MB 的連續塊。每一個部分的最後一個塊可能會短一些。 -
每一個塊的摘要均經過字節 0xa5 + 塊的長度 + 塊的內容進行計算。 -
頂級摘要經過字節 0x5a + 塊數 + 塊的摘要的鏈接進行計算。
摘要以分塊方式計算,以便經過並行處理來加快計算速度。
v3(Android 9 及更高版本)
v3新版本簽名中加入了證書的旋轉校驗,便可以在一次的升級安裝中使用新的證書,新的私鑰來簽名APK。固然這個新的證書是須要老證書來保證的,相似一個證書鏈。
詳細內容見於:Android P v3簽名新特性[7]
v4(Android 11)
此方案會在單獨的文件 (apk-name.apk.idsig) 中生成一種新的簽名,但在其餘方面與 v2 和 v3 相似。沒有對 APK 進行任何更改。此方案支持 ADB 增量 APK 安裝。設備上安裝大型(2GB 以上)APK 可能須要很長的時間,ADB(Android 調試橋)增量 APK 安裝能夠安裝足夠的 APK 以啓動應用,同時在後臺流式傳輸剩餘數據,從而加快 APK 安裝速度。
zipalign
zipalign
是一種歸檔對齊工具,可對 Android 應用 (APK) 文件提供重要的優化。 其目的是要確保全部未壓縮數據的開頭均相對於文件開頭部分執行特定的對齊。具體來講,它會使 APK 中的全部未壓縮數據(例如圖片或原始文件)在 4 字節邊界上對齊。
使用時間點
必須在應用構建過程當中的兩個特定時間點之一使用 zipalign,具體在哪一個時間點使用,取決於所使用的應用簽名工具:
-
若是使用的是 jarsigner,則只能在爲 APK 文件簽名以後執行 zipalign。 -
若是使用的是 apksigner,則只能在爲 APK 文件簽名以前執行 zipalign。若是您在使用 apksigner 爲 APK 簽名以後對 APK 作出了進一步更改,簽名便會失效。

自此,一個能夠運行的APK
就誕生了。
APK
運行在Android
手機上
既然咱們要開始在手機上運行了,那基本還要用上adb
的工具了,這裏溫習一個安裝的命令adb install <dir>/XXXX.apk
在
Android
裏咱們須要瞭解的的就是Dalvik
和ART
兩個虛擬機了。
可是咱們得先了解一下爲何當年在有JVM
的狀況下,還要本身造出一個DVM
來知足需求呢?先思考一個問題,爲何
Android
程序明明是用Java
寫的,可以直接在JVM
上運行,還要本身再寫一個DVM
呢??
可能不少文章都這樣說,由於經過JVM
來運行,雖然可以一份代碼處處跑,可是顯然從性能上跟不上直接經過寄存器來完成全部的數據操做的。可是我以前據說過一個故事,是谷歌被Oracle
限制了JVM
的使用😵 , 因此才造了一個DVM
。而後效果又比用JVM
好,就開始流行起來了。
那爲何JVM
會比DVM
運行起來慢呢?
JVM | DVM |
---|---|
基於棧開發 | 基於寄存器開發 |
java文件 | dex文件 |
按需加載 | 一次性加載 |
在沒有引入multiDex
以前的DVM
是一次性加載,可能加載速度上會比JVM
慢,可是加載完畢之後,總體效率高,這基於的是幾個方面:
-
按需加載,致使加載不夠實時。 -
基於棧開發,對應的二進制指令更加複雜。
既然Davlik
聽起來已經這麼好了,爲何還要再開發一套ART
的虛擬機呢?
其實他的優化角度有這幾個層面:
-
採用AOT(Ahead-Of-Time,預編譯)編譯技術,它能將Java字節碼直接轉換成目標機器的機器碼。 -
更爲高效和細粒度的垃圾回收機制(GC)。
AOT(Ahead-Of-Time,預編譯)編譯技術

JIT(Just in Time)
運行時進行字節碼到本地機器碼的編譯缺點:
-
每次啓動應用都須要從新編譯 -
運行時比較耗電(由於常常有編譯的過程)
AOT(Ahead of Time)
在應用安裝時就將字節碼編譯成本地機器碼缺點:
-
應用安裝和系統升級以後的應用優化比較耗時(從新編譯,把程序代碼轉換成機器語言) -
優化後的文件會佔用額外的存儲空間(緩存轉換結果)
JIT + AOT

爲何要出現這樣的方案呢?其實咱們看無論是單純的JIT
或是AOT
方案都有本身的優缺點,爲何這麼說呢。
這是一個流量的時代,而一個安裝包的體積大小、安裝時間常常就會成爲用戶安裝時的軟肋,緣由見於 App競品技術分析 (3)減少安裝包的體積[8]。這就體現了JIT
方案的優點,由於安裝時沒有了編譯的過程,安裝速度相比較而言就更快。可是運行後呢?JIT
的優點就斷崖式降低了,這個時候有AOT
的話,可以再下一次啓動時來加速咱們的程序執行效率,可是AOT
的觸發條件是什麼?
當手機長期處於空閒或者充電狀態的時候,系統纔會進行執行 AOT 過程進行編譯,生成的機器碼緩存爲文件,因此說這個AOT
在無人干預的狀況下是一個很是不可控的過程。
更爲高效和細粒度的垃圾回收機制(GC)
關於GC
又能夠分爲這樣的幾個層面:
-
內存分配器 -
垃圾回收算法 -
超大對象存儲空間的支持 -
Moving GC策略 -
GC調度策略的多樣性
這裏咱們只對GC
垃圾回收算法作一個講解。首先咱們先作一個回顧,在關於JVM,你必須知道的那些玩意兒[9] 中我曾經提到過關於JVM
內的三種垃圾回收算法,複製收集、標記清理、標記整理三種算法,但對於JVM
而言是有將堆區經過本身的規則總體成一個生命週期。而後他與會有不少不少的垃圾回收器,好比說Serial收集器、ParNew收集器、G1回收器。。。。
但那是對於JVM
而言的,而DVM
的出場姿式又是什麼樣的呢?
對於DVM
而言,很簡單的處理方式就是和最開始的 JVM 垃圾收集器同樣Stop The World
,而後套上本身的清理算法,先標記使用中的數據,再把無用數據清理掉。這也就致使了用戶體驗到了難以用語言描述的卡頓感。
而ART
是如何在保持着Stop The World
的觀念的同時又提升了性能的呢?ART
須要垃圾收集器作的工做,拆分給應用程序自己完成,這一項任務其實就是標記了。這裏作一個盲猜,ART
的實現應該是經過添加了相似於使用標記位的東西,經過不斷更新這個值,等須要進行清理時,數據的標識其實已經處於一個完備的狀態了,可能麻煩的問題就在於這個標記位的設定了。對於清理過程的減負,Google
又引入了一項名叫packard pre-cleaning
預清理的技術來減輕須要GC
的數量來提升效率。
詳細見於 Android 5.0 ART GC 對比 Android 4.x Dalvik GC[10]
參考資料
-
Android 兼容 Java 8 語法特性的原理分析 [11] -
縮減、混淆處理和優化應用 [12] -
也談Android簽名機制 [13] -
APK 簽名方案 v2 [14] -
Android P v3簽名新特性 [15]
參考資料
AAPT2: https://developer.android.com/studio/command-line/aapt2
[2]關於Python的小小分享: https://juejin.im/post/6854573208750948359
[3]Gradle vs Maven:性能比較: https://gradle.org/gradle-vs-maven-performance/
[4]ProGuard 初探的 Optimize 部分: https://www.jianshu.com/p/4278862ef7e7
[5]ProGuard 初探: https://www.jianshu.com/p/4278862ef7e7
[6]APK 簽名方案 v2 分塊: https://source.android.com/security/apksigning/v2#apk-signature-scheme-v2-block
[7]Android P v3簽名新特性: https://xuanxuanblingbling.github.io/ctf/android/2018/12/30/signature/
[8]App競品技術分析 (3)減少安裝包的體積: https://blog.csdn.net/jspandasp/article/details/49339403
[9]關於JVM,你必須知道的那些玩意兒: https://juejin.im/post/6844904183909318664#heading-18
[10]Android 5.0 ART GC 對比 Android 4.x Dalvik GC: https://blog.csdn.net/hello2mao/article/details/42361755
[11]Android 兼容 Java 8 語法特性的原理分析: https://tech.meituan.com/2019/10/17/android-java-8.html
[12]縮減、混淆處理和優化應用: https://developer.android.com/studio/build/shrink-code?hl=zh-cn
[13]也談Android簽名機制: https://www.jianshu.com/p/a5af970ca1db
[14]APK 簽名方案 v2: https://source.android.com/security/apksigning/v2#integrity-protected-contents
[15]Android P v3簽名新特性: https://xuanxuanblingbling.github.io/ctf/android/2018/12/30/signature/
本文分享自微信公衆號 - 告物(ClericYi_Android)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。