現現在,Gradle + 編譯插樁 的應用場景愈來愈多,不管是 各類性能優化中的插件工具製做,仍是用來支持 插件化、熱修復的各類插件,都會使用到這個組合,所以,掌握 Gradle + 編譯插樁 技術可以大大提高咱們的技術競爭力。從本篇開始,筆者將會與你們一塊兒深刻探索編譯插樁技術,關於整個編譯插樁技術研究系列的大綱以下所示:html
一般來講,整個《深刻探索編譯插樁技術系列》到第四篇 ASM 也就結束了,可是 ReDex 的功能實在是太強大了,以致於我不得很少花兩篇的篇幅來進行深刻講解。須要注意的是,Dex 字節碼與 ReDex 的實現基本是以 C/C++ 語言爲主,並且其實現較爲複雜,因此我會在所有更新完 Awesome-Android-NDK 的 一至四 部分以後,纔會讓你們和我一塊兒去深刻研究 ReDex 的實現機制。前端
衆所周知,編譯 主要分爲 詞法分析、語法分析 、語義檢查和代碼優化 等步驟。額。。等等,別慌,這篇文章並非要講編譯原理,對於絕大多數的 Android 開發來講,咱們能將 App 的編譯和打包流程理解清楚就 OK 了。所以,咱們這篇文章主要講的是 App 的編譯過程,本篇包含的主要內容以下所示:java
本篇文章多是從如今到今年年末 最簡單的一篇文章 了。放輕鬆,好好享受,後面可能就。。。android
好了,下面,咱們就先回顧下 App 的編譯和打包過程。git
咱們都知道,APK 實際上是一個 zip 類型的壓縮包,而一個典型的 APK 一般都會包含了如下七部分的內容:github
七、META-INF 目錄:用於保存 App 的簽名和校驗信息,以保證程序的完整性。當生成 APK 包時,系統會對包中的全部內容作一次校驗,而後將結果保存在這裏。而手機在安裝這一 App 時還會對內容再作一次校驗,並和 META-INF 中的值進行比較,以免 APK 被惡意篡改。其中包含以下 三個文件,以下所示:算法
接下來,咱們來看看 App 的編譯和打包過程。json
早在 深刻探索 Android 包體積優化(匠心製做) 一文中咱們就探討過打包的部分流程,這裏咱們須要更加全面地瞭解下。Android 官方的編譯打包流程圖以下所示:api
爲了 瞭解更多打包過程當中的細節,咱們須要查看更加詳細的舊版 APK 打包流程圖 ,以下圖所示:緩存
打包流程可簡述爲以下 八個步驟:
至此,咱們已經瞭解了整個 APK 編譯和打包的流程。
主要基於如下 兩點緣由
:
空間佔用更小
:由於全部 XML 元素的標籤、屬性名稱、屬性值和內容所涉及到的字符串都會被統一收集到一個字符串資源池中,而且會去重。有了這個字符串資源池,原來使用字符串的地方就會被替換成一個索引到字符串資源池的整數值,從而能夠減小文件的大小。解析效率更高
:二進制格式的 XML 文件解析速度更快。這是因爲二進制格式的 XML 元素裏面再也不包含有字符串值,所以就避免了進行字符串解析,從而提升瞭解析效率。主要基於兩個文件,以下所示:
資源 ID 文件 R.java
:賦予每個非 assets 資源一個 ID 值,這些 ID 值以常量的形式定義在 R.java 文件中。資源索引表 resources.arsc
:用來描述那些具備 ID 值的資源的配置信息。除此以外,APK 的簽名也是相當重要的,那麼,其簽名算法的實現原理是怎樣的呢?下面咱們就來了解下 APK 簽名算法的實現原理。
在 Apk 中寫入一個 「指紋」。指紋寫入之後,Apk 中有任何修改,都會致使這個指紋無效,Android 系統在安裝 Apk 進行簽名校驗時就會不經過,從而保證了安全性。
主要有 兩點緣由
,以下所示:
在瞭解 APK 簽名的實現以前,咱們還必須知道什麼是數字摘要。
對一個任意長度的數據,經過一個 Hash 算法計算後,均可以獲得一個固定長度的二進制數據,這個數據就稱爲 「摘要」。
在簽名和校驗的流程之中,應用了許多密碼學的知識,這裏咱們須要先大體瞭解一下。
Hash 算法就是 將數據(如一段文字)運算變爲另外一固定長度值。它的特色主要有以下 三點
:
惟一性
。固定長度
:比較經常使用的 Hash 算法有 MD5 和 SHA1,MD5 的長度是128位,SHA1 的長度是160位。不可逆性
。而經常使用的 Hash 算法有以下 三種
:
SHA-1
:在密碼學中,SHA-1(安全散列算法1)是一種加密散列函數,它接受輸入併產生一個160 位(20 字節)散列值,稱爲消息摘要。MD5
:MD5 消息摘要算法(英語:MD5 Message-Digest Algorithm),一種被普遍使用的密碼散列函數,能夠產生出一個128位(16字節)的散列值(hash value),用於確保信息傳輸完整一致。SHA-2
:名稱來自於安全散列算法2(Secure Hash Algorithm 2)的縮寫,一種密碼散列函數算法標準,其下又可再分爲六個不一樣的算法標準,包括了:SHA-22四、SHA-25六、SHA-38四、SHA-5十二、SHA-512/22四、SHA-512/256。簽名就是 在摘要的基礎上再進行一次加密,對摘要加密後的數據就能夠看成數字簽名。
簽名過程能夠細分爲 三步
,以下所示:
計算摘要
:經過 Hash 算法提取出原始數據的摘要。計算簽名
:再經過基於密鑰(私鑰)的非對稱加密算法對提取出的摘要進行加密,加密後的數據就是簽名信息。寫入簽名
:將簽名信息寫入原始數據的簽名區塊內。校驗過程一樣也能夠分爲 三步
,以下:
提取摘要
:首先用一樣的 Hash 算法從接收到的數據中提取出摘要。解密簽名
:使用發送方的公鑰對數字簽名進行解密,解密出原始摘要。比較摘要
:若是解密後的數據和提取的摘要一致,則校驗經過;若是數據被第三方篡改過,解密後的數據和摘要將會不一致,則校驗不經過。那麼,咱們該如何保證公鑰的可靠性呢?答案是 數字證書
。
數字證書是 身份認證機構(Certificate Authority)頒發
的,主要包含了如下 六類信息
:
接收方收到消息後,須要先向 CA 驗證證書的合法性,再進行簽名校驗
。
須要注意的是,Apk 的證書一般是自簽名的,也就是由開發者本身製做,沒有向 CA 機構申請。Android 在安裝 Apk 時並無校驗證書自己的合法性,只是從證書中提取公鑰和加密算法
,這也正是對第三方 Apk 從新簽名後,還可以繼續在沒有安裝這個 Apk 的系統中繼續安裝的緣由。
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----- 結尾。Android 提供了 兩種對 Apk 的簽名方式
,一種是基於 JAR 的簽名方式,另外一種是基於 Apk 的簽名方式,它們的 主要區別在於使用的簽名文件不同:jarsigner 使用 keystore 文件進行簽名;而 apksigner 除了支持使用 keystore 文件進行簽名外,還支持直接指定 pem 證書文件和私鑰進行簽名。
keystore 是一個密鑰庫,也就是說它能夠存儲多對密鑰和證書,keystore 的密碼是用於保護 keystore 自己的,每一對密鑰和證書是經過 alias 來區分的。因此 jarsigner 是支持使用多個證書對 Apk 進行簽名的,apksigner 也一樣支持。
Android Apk V1 驗證簽名的過程主要能夠分爲以下 四步
:
在整個 App 的編譯打包過程當中,Gradle 自動化構建工具發揮出了重要做用,而編譯速度但是須要咱們迫切解決的一大痛點。下面,咱們就來看看如何對編譯進行提速。
在 Android Studio 3.0 以前 共有 六種
依賴方式,以下所示:
Compile
:對全部的 build type 以及 falvors 編譯而且打包到 APK。Provided
:對全部的 build type 以及 falvors 只編譯,不打包到 APK。APK
:只會打包到 APK,不參與編譯,好比引用 jar 中的類或者方法, 編譯時就會報錯。Test compile
:僅對單元測試的代碼和打包的測試 APK 有效,而對 debug 或者 release APK 包無效。Debug compile
:僅對 debug 模式的編譯和打包的 debug APK 有效,而對 test 或者 release APK 打包無效。、Release compile
:僅對 Release 模式的編譯和打包的 Release APK 有效,而對 test 或者 debug APK 打包無效。而在 Android Studio 3.0 以後,新增了兩種方式:api 和 implementation。其中 api 徹底等同於 compile
。
等同於 compile, 用 api 指令編譯,表示 三方庫的依賴對 module 是可見的
,即等同 app Module 可使用此三方庫依賴。
特色是 將該依賴隱藏在內部,而不對外部公開
。好比在組件化項目中,有一個 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
。
Gradle 的官方方案 Instant Run。在 Android Plugin 2.3 以前,它使用了 Multidex 實現。在 Android Plugin 2.3 以後,它使用了 Android 5.0 新增的 Split APK 機制。以下圖所示:
可是,若是你的應用較大,會有以下四個問題:
多進程的限制
:若是應用存在多進程,熱交換和溫交換都不能生效。此時,Instant Run 的速度就會下降很多。Split APK 安裝耗時
:雖然 Split APK 的安裝不會生成 Odex 文件,可是這裏依然會進行簽名校驗和文件拷貝,這可能須要幾秒到幾十秒。Annotation Processor 需全量 javac 的問題
:在 Gradle 4.6 及以前,若是項目中運用了 Annotation Processor,本次修改以及它依賴的模塊都須要全量 javac,這可能會須要幾十秒。常量需全量 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 經過重寫 apk 的構建流程往每一個類裏去注入 Hook(鉤子) 來達到類的熱替換。關於 InstantRun 詳細的實現原理能夠看看我以前寫的深刻探索Android啓動速度優化一文。
對於小型的應用,InstantRun 確實很好用,可以節省構建和部署的時間,而且不會出錯。可是,對於大型的複雜應用,它會致使更長的構建時間,同時因爲 InstantRun 構建過程和正常的 app 構建存在衝突,經常出現讓開發者意想不到的錯誤。AS 開發團隊在連續幾個大版本中都嘗試去解決這些問題,可是效果不理想。
因此基於此,AS 開發者團隊 從新設計了底層的架構,推出了 ApplyChangs。和 InstantRun 不一樣的是,它不會在構建過程當中去修改 apk。取而代之,它使用了 Android 8.0(Oreo)上支持的 Runtime Instrumentation 以及更新的設備和模擬器在運行時重定義類。
對於 運行在 Android 8.0 或者更新版本上的設備和虛擬機,Android Studio 如今有 三個按鈕
來控制應用程序重啓的程度:
Run
:會部署全部的改動並重啓應用程序。Apply Changes
:會嘗試應用資源和代碼的更改,並只重啓 Activity, 而不是重啓應用程序。Apply Code Changes
:會嘗試應用代碼的更改,而不重啓任何東西。一般只有方法體內部的代碼更改纔會對 Apply Changes 具備兼容性。而 ApplyChanges 的 實現原理 就是找出 AndroidStudio 構建出來的 apk 和已經安裝到手機設備 apk 的差別。找出差別後,而後將差別發送到手機上執行差別合併。ApplyChanges 的 整體架構
以下圖所示:
那麼,理想的編譯方案是怎麼樣的呢?
咱們能夠把安裝的 Base APK 做爲一個殼 APK,而真正的業務代碼都放到 Assets 的 ClassesN.dex 中
。該方案須要包含如下 三個優化點
:
免安裝
:能夠參考 Tinker 熱修復的實現原理,每次只把修改以及依賴的類插入到 pathclassloader 的最前方便可。使用 ReDex 源碼中的 Oatmeal
:首次安裝運行時 ClassesN.dex 轉換成 Odex 很耗時,咱們可使用 ReDex 優化模塊下的 Oatmeal,經過它應用能夠在 100 ms 內生成一個徹底解釋執行的 Odex 文件。關閉 JIT 優化
:使用把修改和依賴的類插入到 pathclassloader 最前方的這種方式在 Android N的混合編譯 會遇到一些問題:不管是使用插入 pathlist 仍是 parent classloader 的方式,若補丁修改的 class 已經存在於 app image(app image 的做用是記錄已經編譯好的 「熱代碼」,而且在啓動時一次性把它們加載到緩存,而預先加載是爲了代替用時查找以提高應用的性能),它們都是沒法經過熱補丁更新的。它們在啓動 app 時已經加入到 PathClassloader 的 ClassTable 中,所以系統在查找類時會直接使用 dex 中的 class。這時咱們須要關閉虛擬機的 JIT 優化,經過 在 AndroidManifest 指定 android:vmSafeMode=「true」
便可。除了將電腦更換爲 Mac Pro 頂配版以外,還有如下方式能夠提高編譯速度:
在回答這個問題以前,咱們必須先了解 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 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 主要包括 打包構建、Code Review、代碼工程管理、代碼掃描
等一系列流程。它的 整套運轉體系 能夠簡化爲下圖:
構建 CI 的目的主要是爲了解決如下四個問題。
隨着業務的發展,基礎組件庫的數量會持續上漲,這個時候組件間的關係就會變得錯綜複雜,這將會致使以下 兩個問題
:
在平常的功能開發中,咱們通常都會經 代碼開發、組件發版、組件集成、打包、測試這五個步驟。若是測試發現 Bug 須要進行修復,而後會再次經歷代碼修改、組件發版、組件集成、打包、測試,直到測試經過交付產品。傳統的研發流程以下圖所示:
能夠看到,開發同窗在整個開發流程中須要手動提交 MR、升級組件、觸發打包以及去實時監控流程的狀態,這樣確定會嚴重影響開發的專一度,下降研發的生產力。
隨着 App從 項目初期 => 成長期 => 成熟期,對性能的要求會愈來愈高,爲了保障性能的足夠穩定,咱們須要製造出許多性能監控的工具,以實時監控咱們應用的性能。而 App 性能監控體系必須和 CI 結合起來,以實現流程的自動化和平臺化。
隨着 App 的體積變大,依賴變多,項目的編譯構建速度會愈來愈慢,緩慢的編譯速度會嚴重拖垮開發同窗的研發效率。所以,提高 App 的編譯構建速度刻不容緩。
持續集成涉及的流程很是多,可是有 兩個主要的步驟是很是重要
的,具體以下所示:
爲了防止不符合規範的代碼提交到遠程倉庫中,咱們須要 自定義一套符合自身項目的編碼規範,並使用專門的插件來檢測。自定義代碼檢測能夠經過徹底本身實現或者擴展 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 檢查,跑完將警告彙總分配到對應的負責人身上,並郵件告知他,直到上線。
Code Review 很是重要,在每一次提交代碼時,咱們都須要本身進行一次 Code Review,而後再讓別人去 Review,以創建自身良好的技術品牌。
有些同窗可能會認爲 CI 並不重要,它好像跟具體的技術並沒有關聯。可是,咱們須要知道,學會不只僅是鑽在開發角度看問題,跳脫出來,站在用戶角度,站在產品角度,或許會有意外的收穫。
到這裏,關於 Android 編譯相關的知識就介紹完了。下面,總結一下本篇文章涉及的 三大主題
:
App 的編譯和打包流程
:APK 的編譯打包流程、簽名算法的原理。編譯提速
:瞭解 Android Studio 3.0 依賴類型的變化、現有編譯方案、理想的編譯方案、編譯速度優化。廣義的編譯-CI
:持續集成的緣由、持續集成的主要步驟。在本篇文章,咱們即涉及到了 Android 編譯的深度方面:App 的編譯和打包流程、簽名算法的原理,也涉及到了 Android 編譯的廣度方面:持續集成。所以,在咱們學習的過程當中,技術就像是一棵樹,在頂部葉子上各個領域看似絕不相干,可是在一個領域越往下深刻,各個領域相互交錯到的知識或者設計方式就越多,因此技術深度和廣度並非對立面,對技術深度的探索不只有利於你在特定領域有更深理解,更加能夠幫助你輕鬆切換到另外一個領域,特別是像前端的各細分領域的工做,不少領域的知識背後都異曲同工,而技術的廣度也不是有的人說的那樣不堪,在有技術深度的基礎上,去拓展本身的技術廣度,其實會讓你對原有技術的理解變得更加地深刻。
一、極客時間之Android開發高手課《關於編譯,你須要瞭解什麼?》
二、《深刻理解Android內核設計思想》第20章 Android應用程序的編譯和打包
九、Infer
十一、aapt2官方教程
十二、FreeLine
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣, Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~