深入探索編譯插樁技術(一、編譯基礎)

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

現如今,Gradle + 編譯插樁 的應用場景越來越多,無論是 各種性能優化中的插件工具製作,還是用來支持 插件化、熱修復的各種插件,都會使用到這個組合,因此,掌握 Gradle + 編譯插樁 技術能夠大大提升我們的技術競爭力。從本篇開始,筆者將會與大家一起深入探索編譯插樁技術,關於整個編譯插樁技術研究系列的大綱如下所示:

通常來說,整個《深入探索編譯插樁技術系列》到第四篇 ASM 也就結束了,但是 ReDex 的功能實在是太強大了,以至於我不得不多花兩篇的篇幅來進行深入講解。需要注意的是,Dex 字節碼與 ReDex 的實現基本是以 C/C++ 語言爲主,而且其實現較爲複雜,所以我會在全部更新完 Awesome-Android-NDK 的 一至四 部分之後,纔會讓大家和我一起去深入研究 ReDex 的實現機制

衆所周知,編譯 主要分爲 詞法分析、語法分析 、語義檢查和代碼優化 等步驟。額。。等等,別慌,這篇文章並不是要講編譯原理,對於絕大多數的 Android 開發來說,我們能將 App 的編譯和打包流程理解清楚就 OK 了。因此,我們這篇文章主要講的是 App 的編譯過程,本篇包含的主要內容如下所示:

  • 1、App的編譯和打包流程
  • 2、編譯提速
  • 3、廣義的編譯-CI
  • 4、總結

本篇文章可能是從現在到今年年底 最簡單的一篇文章 了。放輕鬆,好好享受,後面可能就。。。

好了,下面,我們就先回顧下 App 的編譯和打包過程。

一、App 的編譯和打包流程

1、APK 的組成

我們都知道,APK 其實是一個 zip 類型的壓縮包,而一個典型的 APK 通常都會包含了以下七部分的內容

  • 1、AndroidManifest.xml:如果 App 是一本書,那麼這個文件就是它的 “封面” 和 “目錄” 。它記載了 App 的名稱、權限聲明、所包含的組件等一系列信息
  • 2、classes.dex:它是由項目源碼生成的 .class 文件經過進一步地轉換而生成的 Android 系統可識別的 Dalvik Byte Code。並且,由於 Android 系統中的字節碼和標準 JVM 中的字節碼是有區別的,所以如果 App 中引用了第三方 jar 包的話,那麼通常情況下它也會被包含在 classes.dex 中
  • 3、resources.arsc:資源索引表,包含編譯後的二進制資源文件。每當在 res 文件夾下放一個文件時,aapt 就會自動生成對應的 id 並保存在 .R 文件中,但 .R 文件僅僅只是保證編譯程序不會報錯,實際上在應用運行時,系統會根據 ID 尋找對應的資源路徑,而 resources.arsc 文件就是用來記錄這些 ID 和 資源文件位置對應關係 的文件
  • 4、res 目錄:未編譯的資源文件
  • 5、asserts:額外建立的資源文件夾resassets 的不同在於 res 目錄下的文件會在 .R 文件中生成對應的資源 ID,而 assets 不會自動生成對應的 ID,而是通過 AssetManager 類的接口來獲取。
  • 6、libs 目錄:如果存在的話,存放的是 ndk 編出來的 so 庫
  • 7、META-INF 目錄:用於保存 App 的簽名和校驗信息,以保證程序的完整性。當生成 APK 包時,系統會對包中的所有內容做一次校驗,然後將結果保存在這裏。而手機在安裝這一 App 時還會對內容再做一次校驗,並和 META-INF 中的值進行比較,以避免 APK 被惡意篡改。其中包含如下 三個文件,如下所示:
    • 1)、MANIFEST.MF:其中每一個資源文件都有一個對應的 SHA-256-Digest(SHA1) 簽名,MANIFEST.MF 文件的 SHA256(SHA1) 經過 base64 編碼的結果即爲 CERT.SF 中的 SHA256(SHA1)-Digest-Manifest 值。
    • 2)、CERT.SF:除了開頭處定義的 SHA256(SHA1)-Digest-Manifest 值,後面幾項的值是對 MANIFEST.MF 文件中的每項再次 SHA256(SHA1) 經過 base64 編碼後的值。
    • 3)、CERT.RSA:其中包含了公鑰、加密算法等信息。首先,對前一步生成的 CERT.SF 使用了 SHA256(SHA1)生成了數字摘要並使用了 RSA 加密,接着,利用了開發者私鑰進行簽名。然後,在安裝時使用公鑰解密。最後,將其與未加密的摘要信息(MANIFEST.MF文件)進行對比,如果相符,則表明內容沒有被修改。

接下來,我們來看看 App 的編譯和打包過程。

2、APK 的編譯打包流程

早在 深入探索 Android 包體積優化(匠心製作) 一文中我們就探討過打包的部分流程,這裏我們需要更加全面地瞭解下。Android 官方的編譯打包流程圖如下所示:

爲了 瞭解更多打包過程中的細節,我們需要查看更加詳細的舊版 APK 打包流程圖 ,如下圖所示:

打包流程可簡述爲如下 八個步驟

  • 1、首先,.aidl(Android Interface Description Language)文件需要通過 aidl 工具轉換成編譯器能夠處理的 Java 接口文件
  • 2、同時,資源文件(包括 AndroidManifest.xml、佈局文件、各種 xml 資源等等)將被 AAPT(Asset Packaging Tool)(Android Gradle Plugin 3.0.0 及之後使用 AAPT2 替代了 AAPT)處理爲最終的 resources.arsc,並生成 R.java 文件以保證源碼編寫時可以方便地訪問到這些資源
  • 3、然後,通過 Java Compiler 編譯 R.java、Java 接口文件、Java 源文件,最終它們會統一被編譯成 .class 文件
  • 4、因爲 .class 並不是 Android 系統所能識別的格式,所以還需要通過 dex 工具將它們轉化爲相應的 Dalvik 字節碼(包含壓縮常量池以及清除冗餘信息等工作)。這個過程中還會加入應用所依賴的所有 “第三方庫”
  • 5、下一步,通過 ApkBuilder 工具將資源文件、DEX 文件打包生成 APK 文件
  • 6、接着,系統將上面生成的 DEX、資源包以及其它資源通過 apkbuilder 生成初始的 APK 文件包
  • 7、然後,通過簽名工具 Jarsigner 或者其它簽名工具對 APK 進行簽名得到簽名後的 APK。如果是在 Debug 模式下,簽名所用的 keystore 是系統自帶的默認值,否則我們需要提供自己的私鑰以完成簽名過程
  • 8、最後,如果是正式版的 APK,還會利用 ZipAlign 工具進行對齊處理,以提高程序的加載和運行速度。而對齊的過程就是將 APK 文件中所有的資源文件距離文件的起始位置都偏移4字節的整數倍,這樣通過 mmap 訪問 APK 文件的速度會更快,並且會減少其在設備上運行時的內存佔用

至此,我們已經瞭解了整個 APK 編譯和打包的流程。

那麼,爲什麼 XML 資源文件要從文本格式編譯成二進制格式?

主要基於以下 兩點原因

  • 1、空間佔用更小因爲所有 XML 元素的標籤、屬性名稱、屬性值和內容所涉及到的字符串都會被統一收集到一個字符串資源池中,並且會去重。有了這個字符串資源池,原來使用字符串的地方就會被替換成一個索引到字符串資源池的整數值,從而可以減少文件的大小

  • 2、解析效率更高二進制格式的 XML 文件解析速度更快。這是由於二進制格式的 XML 元素裏面不再包含有字符串值,因此就避免了進行字符串解析,從而提高了解析效率

而 Android 資源管理框架又是如何快速定位到最匹配資源的?

主要基於兩個文件,如下所示:

  • 1、資源 ID 文件 R.java賦予每一個非 assets 資源一個 ID 值,這些 ID 值以常量的形式定義在 R.java 文件中
  • 2、資源索引表 resources.arsc用來描述那些具有 ID 值的資源的配置信息

除此之外,APK 的簽名也是至關重要的,那麼,其簽名算法的實現原理是怎樣的呢?下面我們就來了解下 APK 簽名算法的實現原理。

3、簽名算法的原理

什麼是簽名?

在 Apk 中寫入一個 “指紋”。指紋寫入以後,Apk 中有任何修改,都會導致這個指紋無效,Android 系統在安裝 Apk 進行簽名校驗時就會不通過,從而保證了安全性

那麼,爲什麼要簽名?

主要有 兩點原因,如下所示:

  • 1、確保 Apk 來源的真實性
  • 2、確保 Apk 沒有被第三方篡改

在瞭解 APK 簽名的實現之前,我們還必須知道什麼是數字摘要。

數字摘要

對一個任意長度的數據,通過一個 Hash 算法計算後,都可以得到一個固定長度的二進制數據,這個數據就稱爲 “摘要”

在簽名和校驗的流程之中,應用了許多密碼學的知識,這裏我們需要先大致瞭解一下。

Hash(散列算法)的基礎原理

Hash 算法就是 將數據(如一段文字)運算變爲另一固定長度值。它的特點主要有如下 三點

  • 1、唯一性
  • 2、固定長度比較常用的 Hash 算法有 MD5 和 SHA1,MD5 的長度是128位,SHA1 的長度是160位
  • 3、不可逆性

而常用的 Hash 算法有如下 三種

  • 1、SHA-1在密碼學中,SHA-1(安全散列算法1)是一種加密散列函數,它接受輸入併產生一個160 位(20 字節)散列值,稱爲消息摘要
  • 2、MD5MD5 消息摘要算法(英語:MD5 Message-Digest Algorithm),一種被廣泛使用的密碼散列函數,可以產生出一個128位(16字節)的散列值(hash value),用於確保信息傳輸完整一致
  • 3、SHA-2名稱來自於安全散列算法2(Secure Hash Algorithm 2)的縮寫,一種密碼散列函數算法標準,其下又可再分爲六個不同的算法標準,包括了:SHA-224、SHA-256、SHA-384、SHA-512、SHA-512/224、SHA-512/256

簽名和校驗的主要過程

簽名就是 在摘要的基礎上再進行一次加密,對摘要加密後的數據就可以當作數字簽名。

簽名過程:

簽名過程可以細分爲 三步,如下所示:

  • 1、計算摘要通過 Hash 算法提取出原始數據的摘要
  • 2、計算簽名再通過基於密鑰(私鑰)的非對稱加密算法對提取出的摘要進行加密,加密後的數據就是簽名信息
  • 3、寫入簽名將簽名信息寫入原始數據的簽名區塊內

校驗過程:

校驗過程同樣也可以分爲 三步,如下:

  • 1、提取摘要首先用同樣的 Hash 算法從接收到的數據中提取出摘要
  • 2、解密簽名使用發送方的公鑰對數字簽名進行解密,解密出原始摘要
  • 3、比較摘要如果解密後的數據和提取的摘要一致,則校驗通過;如果數據被第三方篡改過,解密後的數據和摘要將會不一致,則校驗不通過

那麼,我們該如何保證公鑰的可靠性呢?答案是 數字證書

數字證書

數字證書是 身份認證機構(Certificate Authority)頒發的,主要包含了以下 六類信息

  • 1、證書頒發機構
  • 2、證書頒發機構簽名
  • 3、證書綁定的服務器域名
  • 4、證書版本、有效期
  • 5、簽名使用的加密算法(非對稱算法,如 RSA)
  • 6、公鑰等

接收方收到消息後,需要先向 CA 驗證證書的合法性,再進行簽名校驗

需要注意的是,Apk 的證書通常是自簽名的,也就是由開發者自己製作,沒有向 CA 機構申請。Android 在安裝 Apk 時並沒有校驗證書本身的合法性,只是從證書中提取公鑰和加密算法,這也正是對第三方 Apk 重新簽名後,還能夠繼續在沒有安裝這個 Apk 的系統中繼續安裝的原因。

keystore 和證書格式

keystore 文件中包含了 私鑰、公鑰和數字證書。根據編碼不同,keystore 文件分爲很多種,Android 使用的是 Java 標準 keystore 格式 JKS(Java Key Storage),所以通過 Android Studio 導出的 keystore 文件是以 .jks 結尾的。

keystore 使用的 證書標準是 X.509,X.509 標準也有多種 編碼格式,常用的有兩種:pem(Privacy Enhanced Mail)和 der(Distinguished Encoding Rules)jks 使用的是 der 格式,但是,Android 也支持直接使用 pem 格式的證書進行簽名

下面,我們瞭解下兩種證書編碼格式的區別,如下所示:

  • DER(Distinguished Encoding Rules)二進制格式,所有類型的證書和私鑰都可以存儲爲 der 格式
  • PEM(Privacy Enhanced Mail)base64 編碼,內容以-----BEGIN xxx----- 開頭,以-----END xxx----- 結尾

jarsigner 和 apksigner 的區別

Android 提供了 兩種對 Apk 的簽名方式,一種是基於 JAR 的簽名方式,另一種是基於 Apk 的簽名方式,它們的 主要區別在於使用的簽名文件不一樣:jarsigner 使用 keystore 文件進行簽名;而 apksigner 除了支持使用 keystore 文件進行簽名外,還支持直接指定 pem 證書文件和私鑰進行簽名

在我們簽名時,除了要指定 keystore 文件和密碼外,也要指定 alias 和 key 的密碼,這是爲什麼呢?

keystore 是一個密鑰庫,也就是說它可以存儲多對密鑰和證書,keystore 的密碼是用於保護 keystore 本身的,每一對密鑰和證書是通過 alias 來區分的。所以 jarsigner 是支持使用多個證書對 Apk 進行簽名的,apksigner 也同樣支持。

Android Apk V1 驗證簽名的原理

Android Apk V1 驗證簽名的過程主要可以分爲如下 四步

  • 1、解析出 CERT.RSA 文件中的證書、公鑰,解密 CERT.RSA 中的加密數據
  • 2、解密結果和 CERT.SF 的指紋進行對比,保證 CERT.SF 沒有被篡改
  • 3、接着,將 CERT.SF 中的內容再和 MANIFEST.MF 中的指紋對比,保證 MANIFEST.MF 文件沒有被篡改
  • 4、MANIFEST.MF 中的內容和 APK 所有文件指紋逐一對比,保證 APK 沒有被篡改

在整個 App 的編譯打包過程中,Gradle 自動化構建工具發揮出了重要作用,而編譯速度可是需要我們迫切解決的一大痛點。下面,我們就來看看如何對編譯進行提速。

二、編譯提速

1、瞭解 Android Studio 3.0 依賴類型的變化

Android Studio 3.0 之前 共有 六種 依賴方式,如下所示:

  • 1、Compile對所有的 build type 以及 falvors 編譯並且打包到 APK
  • 2、Provided對所有的 build type 以及 falvors 只編譯,不打包到 APK
  • 3、APK只會打包到 APK,不參與編譯,比如引用 jar 中的類或者方法, 編譯時就會報錯
  • 4、Test compile僅對單元測試的代碼和打包的測試 APK 有效,而對 debug 或者 release APK 包無效
  • 5、Debug compile僅對 debug 模式的編譯和打包的 debug APK 有效,而對 test 或者 release APK 打包無效
  • 6、Release compile僅對 Release 模式的編譯和打包的 Release APK 有效,而對 test 或者 debug APK 打包無效

而在 Android Studio 3.0 之後,新增了兩種方式:api 和 implementation。其中 api 完全等同於 compile

api

等同於 compile, 用 api 指令編譯,表示 三方庫的依賴對 module 是可見的,即等同 app Module 可以使用此三方庫依賴。

implementation

特點是 將該依賴隱藏在內部,而不對外部公開。比如在組件化項目中,有一個 app module 和一個 base module,app moudle 引入了 base module。其中 base module 使用 implementation 依賴了 Glide 庫,因爲 implementation 是內部依賴,所以是無法調用到 Glide 庫的功能的。因此 implementation 可 以 對外隱藏不必要的接口,並且,使用它可以有效地 提高編譯速度。比如,在組件化項目中一般含有多個 Moudle 模塊,如 Module A => Module B => Moudle C, 比如 改動 Moudle C 接口的相關代碼,如果使用的是 implementation,這時候編譯只需要單獨編譯 Module B 模塊就行,但是如果使用 api 或者舊版本的 compile,由 於Module A 也可以訪問到 Moudle C,所以 Module A 部分也需要重新編譯。所以,在使用無錯的情況下,可以優先使用 implementation

2、現有編譯方案

Gradle 的官方方案 Instant Run在 Android Plugin 2.3 之前,它使用了 Multidex 實現。在 Android Plugin 2.3 之後,它使用了 Android 5.0 新增的 Split APK 機制。如下圖所示:

image

但是,如果你的應用較大,會有如下四個問題:

  • 1、多進程的限制如果應用存在多進程,熱交換和溫交換都不能生效。此時,Instant Run 的速度就會降低不少
  • 2、Split APK 安裝耗時雖然 Split APK 的安裝不會生成 Odex 文件,但是這裏依然會進行簽名校驗和文件拷貝,這可能需要幾秒到幾十秒
  • 3、Annotation Processor 需全量 javac 的問題在 Gradle 4.6 及之前,如果項目中運用了 Annotation Processor,本次修改以及它依賴的模塊都需要全量 javac,這可能會需要幾十秒
  • 4、常量需全量 javac 的問題此時,常量池會直接把值編譯到其他類中,Gradle 並不知道有哪些類使用了這個常量

阿里的 FreeLine 在大部分情況比 Instant Run 更快,但是,它 犧牲了正確性。因爲,爲了追求更快的速度,它直接忽略了 Annotation 和常量改變可能帶來錯誤的編譯產物。而 Instant Run 作爲官方方案,它優先保證了 100% 的正確性

但是,在 Android Studio 3.5 之後,Android 8.0 以後的設備將會使用新的方案 Apply Changes 去代替 Instant Run。而 ApplyChange 採用了跟 InstantRun 不一樣的原理來加快 AndroidStudio 部署安裝 APK 的流程。下面,我們就來了解下他們之間的區別。

InstantRun

InstantRun 主要解決以下兩個問題:

  • 1、減少構建和部署 app 到手機的時間
  • 2、熱更新代碼改動,無需重啓 app 或者 activity

爲了實現這兩個目標,InstantRun 通過重寫 apk 的構建流程往每個類裏去注入 Hook(鉤子) 來達到類的熱替換。關於 InstantRun 詳細的實現原理可以看看我之前寫的深入探索Android啓動速度優化一文。

對於小型的應用,InstantRun 確實很好用,能夠節省構建和部署的時間,並且不會出錯。但是,對於大型的複雜應用,它會導致更長的構建時間,同時由於 InstantRun 構建過程和正常的 app 構建存在衝突,常常出現讓開發者意想不到的錯誤。AS 開發團隊在連續幾個大版本中都嘗試去解決這些問題,但是效果不理想。

所以基於此,AS 開發者團隊 重新設計了底層的架構,推出了 ApplyChangs。和 InstantRun 不同的是,它不會在構建過程中去修改 apk。取而代之,它使用了 Android 8.0(Oreo)上支持的 Runtime Instrumentation 以及更新的設備和模擬器在運行時重定義類

ApplyChanges

對於 運行在 Android 8.0 或者更新版本上的設備和虛擬機Android Studio 現在有 三個按鈕 來控制應用程序重啓的程度:

  • Run會部署所有的改動並重啓應用程序
  • Apply Changes會嘗試應用資源和代碼的更改,並只重啓 Activity, 而不是重啓應用程序
  • Apply Code Changes會嘗試應用代碼的更改,而不重啓任何東西

通常只有方法體內部的代碼更改纔會對 Apply Changes 具有兼容性。而 ApplyChanges 的 實現原理 就是找出 AndroidStudio 構建出來的 apk 和已經安裝到手機設備 apk 的差異。找出差異後,然後將差異發送到手機上執行差異合併。ApplyChanges 的 總體架構 如下圖所示:

image

那麼,理想的編譯方案是怎麼樣的呢?

3、理想的編譯方案

我們可以把安裝的 Base APK 作爲一個殼 APK,而真正的業務代碼都放到 Assets 的 ClassesN.dex 中。該方案需要包含以下 三個優化點

  • 1、免安裝可以參考 Tinker 熱修復的實現原理,每次只把修改以及依賴的類插入到 pathclassloader 的最前方即可
  • 2、使用 ReDex 源碼中的 Oatmeal首次安裝運行時 ClassesN.dex 轉換成 Odex 很耗時,我們可以使用 ReDex 優化模塊下的 Oatmeal,通過它應用可以在 100 ms 內生成一個完全解釋執行的 Odex 文件
  • 3、關閉 JIT 優化:使用把修改和依賴的類插入到 pathclassloader 最前方的這種方式在 Android N的混合編譯 會遇到一些問題:無論是使用插入 pathlist 還是 parent classloader 的方式,若補丁修改的 class 已經存在於 app image(app image 的作用是記錄已經編譯好的 “熱代碼”,並且在啓動時一次性把它們加載到緩存,而預先加載是爲了代替用時查找以提升應用的性能),它們都是無法通過熱補丁更新的。它們在啓動 app 時已經加入到 PathClassloader 的 ClassTable 中,因此係統在查找類時會直接使用 dex 中的 class。這時我們需要關閉虛擬機的 JIT 優化,通過 在 AndroidManifest 指定 android:vmSafeMode=“true” 即可。

4、編譯速度優化

除了將電腦更換爲 Mac Pro 頂配版之外,還有以下方式可以提升編譯速度:

  • 1、及時升級 Gradle 和 Build Tools 編譯工具鏈,充分利用谷歌最新的優化成果
  • 2、可以將項目中基本不變的模塊拆離出去,使用遠端 Cache 的模式保留編譯後的緩存,具體的搭建流程可參見 Caching for faster builds
  • 3、使用 Android Gradle Plugin 3.0.0 和更高的版本,因爲此時在默認情況下啓用 AAPT2,它替代了 AAPT 來編譯資源,並實現了資源的增量編譯,其中並將資源的編譯拆分爲了兩個步驟:Compile 和 Link。Compile 負責將資源文件編譯爲二進制格式,Link 則會合並所有已編譯的文件,並將它們打包到一個軟件包中
  • 4、Android Studio 3.1 開始默認使用 D8 編譯器(3.0推出),它取代了之前的 dx 工具,將 .class 文件轉換爲 Dex 文件,使用它能夠提升編譯速度和減少生成的 Dex 的大小。此外,在 Android Studio 3.1 開始,你可以在 gradle.properties 開啓 R8,它的目標是取代混淆和 D8,對於 D8 和 R8 的詳細介紹,具體可以看看我寫的深入探索Android包體積優化一文
  • 5、此外,可以嘗試將構建系統切換到 Buck 或使用 Flutter 混合開發,Flutter 中的 Hot Reload秒級編譯功能比較強大,其堪稱爲開發者的神兵利器,它能夠在快速修改 UI,增加功能,修復bug的情況下,而不需要去重新啓動應用,即可看到改動效果
  • 6、最後,Gradle 官方有一個 build-scan 的功能,它可以生成構建期間的詳細報告,其中有性能相關的統計,可以用於幫助分析一些耗時的 task

那麼,Flutter 的 Hot Reload 的實現原理是什麼呢?

在回答這個問題之前,我們必須先了解 Flutter 的編譯模式

Flutter 的編譯模式

編譯模式大體可以分爲 兩種,如下所示:

  • AOT(Ahead Of Time)編譯是在程序運行前就已經編譯,因此,在運行時不需要進行分析、編譯,因此執行速度更快
  • JIT(Just In Time)編譯代碼可以在程序執行時期編譯,因爲要在程序執行前進行分析、編譯,JIT 編譯可能會導致程序執行時間較慢

而 Flutter 使用了與衆不同的編譯模式,在開發階段下,使用了 Kernel Snapshot 模式(對應 JIT 編譯),將 dart 代碼生成了標記化的源代碼,而在運行時編譯使用的是解釋執行。在 release 階段,iOS 使用 AOT 編譯,編譯器將 dart 代碼生成彙編代碼,最終生成 app.framwork,而 android 使用了 Core JIT 編譯,將 dart 轉化爲二進制模式,並在 VM 啓動前載入

因此,在開發階段的 Kernel Snapshot 編譯模式下,Hot Reload 會通過掃描項目文件,將有改動的 dart 文件轉化爲標記化源代碼 kernel files,併發送到正在運行的 DartVM,等待 DartVM 替換資源,然後通知 Flutter Framework 重建、重新佈局、重新繪製 WidgetsTree,即可看到改動效果

那麼,flutter 又是如何觸發 WidgetsTree 的重建呢?

Flutter framework 中 BindingBase 註冊了名爲 reassemble的Dart VM 服務,用於外部與正在運行的 Dart VM 通信,這樣,便能夠觸發根節點樹實現重建操作。當 Hot Reload 導致需重建 WidgetsTree時,reassemble 的 Dart VM 服務就會被觸發,觸發後,就會由根節點開始一步步實現widgets樹重建,其重建流程如下所示:

ext.flutter.reassemble => BindingBase.reassembleApplication => 
WidgetsBinding.performReassemble => BuildOwner.reassemble => Element.reassemble

三、廣義的編譯-CI

CI 即 持續集成,在大型開發團隊中,CI 的建設是重中之重,CI 主要包括 打包構建、Code Review、代碼工程管理、代碼掃描 等一系列流程。它的 整套運轉體系 可以簡化爲下圖:

image

1、持續集成的原因

構建 CI 的目的主要是爲了解決以下四個問題。

1、項目依賴複雜

隨着業務的發展,基礎組件庫的數量會持續上漲,這個時候組件間的關係就會變得錯綜複雜,這將會導致如下 兩個問題

  • 1、如果某個開發同學需要修改代碼,極有可能會影響到其它業務,牽一髮而動全身
  • 2、人工維護組件間複雜的依賴關係非常困難

2、瑣碎的研發流程

在日常的功能開發中,我們一般都會經 代碼開發、組件發版、組件集成、打包、測試這五個步驟。如果測試發現 Bug 需要進行修復,然後會再次經歷代碼修改、組件發版、組件集成、打包、測試,直到測試通過交付產品。傳統的研發流程如下圖所示:

image

可以看到,開發同學在整個開發流程中需要手動提交 MR、升級組件、觸發打包以及去實時監控流程的狀態,這樣肯定會嚴重影響開發的專注度,降低研發的生產力。

3、與 App 性能監控體系的融合

隨着 App從 項目初期 => 成長期 => 成熟期,對性能的要求會越來越高,爲了保障性能的足夠穩定,我們需要製造出許多性能監控的工具,以實時監控我們應用的性能。而 App 性能監控體系必須和 CI 結合起來,以實現流程的自動化和平臺化。

4、項目的編譯構建速度緩慢

隨着 App 的體積變大,依賴變多,項目的編譯構建速度會越來越慢,緩慢的編譯速度會嚴重拖垮開發同學的研發效率。因此,提升 App 的編譯構建速度刻不容緩。

2、持續集成的主要步驟

持續集成涉及的流程非常多,但是有 兩個主要的步驟是非常重要 的,具體如下所示:

1、代碼檢查

爲了防止不符合規範的代碼提交到遠程倉庫中,我們需要 自定義一套符合自身項目的編碼規範,並使用專門的插件來檢測。自定義代碼檢測可以通過完全自己實現或者擴展 Findbugs 插件,例如美團就利用 Findbugs 實現了 Android 漏洞掃描工具 Code Arbiter,其中 FindBugs 是一個靜態分析工具,它一般用來檢查類或者 JAR 文件,將字節碼與一組缺陷模式進行對比來發現可能存在的問題,它可以以獨立的 JAR 包形式運行,也可以作爲集成開發工具的插件形式而存在。而 FindBugs 插件具有着極強的可擴展性,只需要將擴展的 JAR 包導入 FindBugs 插件,重啓 AS,即可完成相關功能的擴展。

在 FindBugs 有一款專門對安全問題進行檢測的擴展插件 Find Security Bugs,該插件主要用於對 Web 安全問題進行檢測,也有極少對Android相關安全問題的檢測規則。我們只需要 定製化自己的 Find Security Bugs,通過增加檢測項來檢測儘可能多的安全問題,通過優化檢測規則來減少檢測的誤報 即可,這裏我們可以直接使用 Android_Code_Arbiter 這個插件,它 去除了其中跟 Android 漏洞無關的漏洞,保留了與 Android 相關的,並增加了其它的一些檢測項,以此形成了針對與於 Android 的源碼審計工具

此外,我們也可以使用 第三方的代碼檢查工具,例如收費的 Coverity,以及 Facebook 開源的 Infer

然而,儘管將問題代碼掃描出來了,可是還是會有不少開發同學不知道如何修改,對於這種情況,我們可以給在自定義代碼掃描工具的時候,對於每一個問題檢查項都給出對應的修改方針

最後,我們可以據此建立一個解決項目異常的流程:建立一個服務專門每天跑項目的 Lint 檢查,跑完將警告彙總分配到對應的負責人身上,並郵件告知他,直到上線。

2、Code Review

Code Review 非常重要,在每一次提交代碼時,我們都需要自己進行一次 Code Review,然後再讓別人去 Review,以建立自身良好的技術品牌。

有些同學可能會認爲 CI 並不重要,它好像跟具體的技術並無關聯。但是,我們需要知道,學會不僅僅是鑽在開發角度看問題,跳脫出來,站在用戶角度,站在產品角度,或許會有意外的收穫

四、總結

到這裏,關於 Android 編譯相關的知識就介紹完了。下面,總結一下本篇文章涉及的 三大主題

  • 1、App 的編譯和打包流程APK 的編譯打包流程、簽名算法的原理
  • 2、編譯提速瞭解 Android Studio 3.0 依賴類型的變化、現有編譯方案、理想的編譯方案、編譯速度優化
  • 3、廣義的編譯-CI持續集成的原因、持續集成的主要步驟

在本篇文章,我們即涉及到了 Android 編譯的深度方面:App 的編譯和打包流程、簽名算法的原理,也涉及到了 Android 編譯的廣度方面:持續集成。因此,在我們學習的過程中,技術就像是一棵樹,在頂部葉子上各個領域看似毫不相干,但是在一個領域越往下深入,各個領域相互交錯到的知識或者設計方式就越多,所以技術深度和廣度並不是對立面,對技術深度的探索不僅有利於你在特定領域有更深理解,更加可以幫助你輕鬆切換到另一個領域,特別是像前端的各細分領域的工作,很多領域的知識背後都殊途同歸,而技術的廣度也不是有的人說的那樣不堪,在有技術深度的基礎上,去拓展自己的技術廣度,其實會讓你對原有技術的理解變得更加地深入。

參考鏈接:

1、極客時間之Android開發高手課《關於編譯,你需要了解什麼?》

2、《深入理解Android內核設計思想》第20章 Android應用程序的編譯和打包

3、Android應用程序資源的編譯和打包過程分析

4、Caching for faster builds

5、Hot Reload秒級編譯

6、build-scan

7、Code Arbiter

8、Find Security Bugs

9、Infer

10、MCI:移動持續集成在大衆點評的實踐

11、aapt2官方教程

12、FreeLine

13、Apply Changes

14、Android N的混合編譯

Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章