關於Android編譯,你須要瞭解什麼

本文來自張紹文老師的《Android開發高手課》,我把我認爲比較好的文章整理分享給你們。html

做爲一名 Android 工程師,咱們天天都會經歷無數次編譯。對於小項目來講,半分鐘或者1,2分鐘便可編譯完成,而對於大型項目來講,每次編譯可能須要花去一杯咖啡的時間。可能我講具體的數字你會更有體會,當時我在微信團隊時,全量編譯 Debug 包須要 5 分鐘,而編譯 Release 包更是要超過 15 分鐘。java

若是每次編譯能夠減小 1 分鐘,對微信整個 Android 團隊來講就能夠節約 1200 分鐘(團隊 40 人 × 天天編譯 30 次 × 1 分鐘)。因此說優化編譯速度,對於提高整個團隊的開發效率是很是重要的。android

那應該怎麼樣優化編譯速度呢?微信、Google、Facebook 等國內外大廠都作了哪些努力呢?除了編譯速度以外,關於編譯你還須要瞭解哪些知識呢?git

編譯

雖然咱們天天都在編譯,那到底什麼是編譯呢?
你能夠把編譯簡單理解爲,將高級語言轉化爲機器或者虛擬機所能識別的低級語言的過程。對於 Android 來講,這個過程就是把 Java 或者 Kotlin 轉變爲 Android 虛擬機可以運行的Dalvik 字節碼的過程。程序員

編譯的整個過程會涉及詞法分析、語法分析 、語義檢查和代碼優化等步驟。對於底層編譯原理感興趣的同窗,你能夠挑戰一下編譯原理的三大經典巨做:龍書、虎書、鯨魚書。github

但今天咱們的重點不是底層的編譯原理,而是但願一塊兒討論 Android 編譯須要解決的問題是什麼,目前又遇到了哪些挑戰,以及國內外大廠又給出了什麼樣的解決方案。android-studio

1,Android 編譯的基礎知識

不管是微信的編譯優化,仍是 Tinker 項目,都涉及比較多的編譯相關知識,所以我在 Android 編譯方面研究頗多,經驗也比較豐富。Android 的編譯構建流程主要包括代碼、資源以及 Native Library 三部分,整個流程能夠參考官方文檔的構建流程圖緩存

在這裏插入圖片描述
Gradle是 Android 官方的編譯工具,它也是 GitHub 上的一個開源項目。從 Gradle 的更新日誌能夠看到,當前這個項目還更新得很是頻繁,基本上每一兩個月都會有新的版本。對於 Gradle,我感受最痛苦的仍是 Gradle Plugin 的編寫,主要是由於 Gradle 在這方面沒有完善的文檔,所以通常都只能靠看源碼或者斷點調試的方法。最近我所在的公司就準備用Gradle搞一個渠道打包工具,對於項目的打包和構建過程,也是深有體會。微信

可是編譯實在過重要了,每一個公司的狀況又各不相同,必須強行造一套本身的「輪子」。已經開源的項目有 Facebook 的Buck以及 Google 的Bazel多線程

爲何要本身「造輪子」呢?主要有下面幾個緣由:

  • 統一編譯工具。Facebook、Google 都有專門的團隊負責編譯工做,他們但願內部的全部項目都使用同一套構建工具,這裏包括 Android、Java、iOS、Go、C++ 等。編譯工具的統一優化,全部項目都會受益。
  • 代碼組織管理架構。Facebook 和 Google 的代碼管理有一個很是特別的地方,就是整個公司的全部項目都放到同一個倉庫裏面。所以整個倉庫很是龐大,因此他們也不會使用 Git。目前 Google 使用的是Piper,Facebook 是基於HG修改的,也是一種基於分佈式的文件系統。
  • 極致的性能追求。Buck 和 Bazel 的性能的確比 Gradle 更好,內部包含它們的各類編譯優化。可是它們或多或少都有一些定製的味道,例如對 Maven、JCenter 這樣的外部依賴支持的也不是太好。

在這裏插入圖片描述
「程序員最痛恨寫文檔,還有別人不寫文檔」,因此它們的文檔也是比較少的,若是想作二次定製開發會感到很痛苦。若是你想把編譯工具切換到 Buck 和 Bazel,須要下很大的決心,並且還須要考慮和其餘上下游項目的協做。固然即便咱們不去直接使用,它們內部的優化思路也很是值得咱們學習和參考。

Gradle、Buck、Bazel 都是以更快的編譯速度、更強大的代碼優化爲目標,咱們下面一塊兒來看看它們作了哪些努力。

2. 編譯速度

回想一下咱們的 Android 開發生涯,在編譯這件事情上面究竟浪費了多少時間和生命。正如前面我所說,編譯速度對團隊效率很是重要。

關於編譯速度,咱們最關心的可能仍是編譯 Debug 包的速度,尤爲是增量編譯(incremental build)的速度,咱們但願能夠作到更加快速的調試。正以下圖所示,咱們每次代碼驗證都要通過編譯和安裝兩個步驟。
在這裏插入圖片描述
此處,咱們從編譯時間和安裝時間兩個緯度來看Android的編譯速度。

  • 編譯時間。把 Java 或者 Kotlin 代碼編譯爲「.class「文件,而後經過 dx 編譯爲 Dex 文件。對於增量編譯,咱們但願編譯儘量少的代碼和資源,最理想狀況是隻編譯變化的部分。可是因爲代碼之間的依賴,大部分狀況這並不可行。這個時候咱們只能退而求其次,但願編譯更少的模塊。Android Plugin 3.0及之後的版本使用 Implementation 代替 Compile,正是爲了優化依賴關係。
  • 安裝時間。咱們要先通過簽名校驗,校驗成功後會有一大堆的文件拷貝工做,例如 APK 文件、Library 文件、Dex 文件等。以後咱們還須要編譯 Odex 文件,這個過程特別是在 Android 5.0 和 6.0 會很是耗時。對於增量編譯,最好的優化是直接應用新的代碼,無需從新安裝新的 APK。

對於增量編譯,我先來說講 Gradle 的官方方案Instant Run。在 Android Plugin 2.3 以前,它使用的 Multidex 實現。在 Android Plugin 2.3 以後,它使用 Android 5.0 新增的 Split APK 機制。

以下圖所示,資源和 Manifest 都放在 Base APK 中, 在 Base APK 中代碼只有 Instant Run 框架,應用的自己的代碼都在 Split APK 中。
在這裏插入圖片描述

Instant Run 有三種模式,若是是熱交換和溫交換,咱們都無需從新安裝新的 Split APK,它們的區別在因而否重啓 Activity。對於冷交換,咱們須要經過adb install-multiple -r -t從新安裝改變的 Split APK,應用也須要重啓。

雖然不管哪種模式,咱們都不須要從新安裝 Base APK。這讓 Instant Run 看起來是否是很不錯,可是在大型項目裏面,它的性能依然很是糟糕,主要緣由是:

  • 多進程問題。「The app was restarted since it uses multiple processes」,若是應用存在多進程,熱交換和溫交換都不能生效。由於大部分應用都會存在多進程的狀況,Instant Run 的速度也就大打折扣。
  • Split APK 安裝問題。雖然 Split APK 的安裝不會生成 Odex 文件,可是這裏依然會有簽名校驗和文件拷貝(APK 安裝的乒乓機制)。這個時間須要幾秒到幾十秒,是不能接受的。
  • Javac 問題。在 Gradle 4.6 以前,若是項目中運用了 Annotation Processor。那很差意思,本次修改以及它依賴的模塊都須要全量 javac,而這個過程是很是慢的,可能會須要幾十秒。這個問題直到Gradle 4.7才解決,關於這個問題緣由的討論你能夠參考這個Issue

你還能夠看看這一個 Issue:「full rebuild if a class contains a constant」,假設修改的類中包含一個「public static final」的變量,那一樣也很差意思,本次修改以及它依賴的模塊都須要全量 javac。這是爲何呢?由於常量池是會直接把值編譯到其餘類中,Gradle 並不知道有哪些類可能使用了這個常量。

詢問 Gradle 的工做人員,他們出給的解決方案是下面這個:

// 原來的常量定義:
public static final int MAGIC = 23

// 將常量定義替換成方法: 
public static int magic() {
  return 23;
}

對於大型項目來講,這確定是不可行的。正如我在 Issue 中所寫的同樣,不管咱們是否是真正改到這個常量,Gradle 都會無腦的全量 javac,這樣確定是不對的。事實上,咱們能夠經過比對此次代碼修改,看看是否有真正改變某一個常量的值。

可是可能用過阿里的Freeline或者蘑菇街的極速編譯的同窗會有疑問,它們的方案爲何不會遇到 Annotation 和常量的問題?

事實上,它們的方案在大部分狀況比 Instant Run 更快,那是由於犧牲了正確性。也就是說它們爲了追求更快的速度,直接忽略了 Annotation 和常量改變可能帶來錯誤的編譯產物。Instant Run 做爲官方方案,它優先保證的是 100% 的正確性。

固然 Google 的人也發現了 Instant Run 的種種問題,在 Android Studio 3.5 以後,對於 Android 8.0 之後的設備將會使用新的方案「Apply Changes」代替 Instant Run。目前我還沒找到關於這套方案更多的資料,不過我認爲應該是拋棄了 Split APK 機制。

一直以來,我心目中都有一套理想的編譯方案,這套方案安裝的 Base APK 依然只是一個殼 APK,真正的業務代碼放到 Assets 的 ClassesN.dex 中,它的架構圖以下。
在這裏插入圖片描述

  • 無需安裝。依然使用相似 Tinker 熱修復的方法,每次只把修改以及依賴的類插入到 pathclassloader 的最前方就能夠,不熟悉的同窗能夠參考《微信 Android 熱補丁實踐演進之路》中的 Qzone 方案。
  • Oatmeal。爲了解決首次運行時 Assets 中 ClassesN.dex 的 Odex 耗時問題,咱們可使用「安裝包優化「中講過的 ReDex 中的黑科技:Oatmeal。它能夠在 100 毫秒之內生成一個徹底解釋執行的 Odex 文件。
  • 關閉 JIT。咱們經過在 AndroidManifest 中添加android:vmSafeMode=「true」來關閉虛擬機的 JIT 優化,這樣也就不會出現 Tinker 在Android N 混合編譯遇到的問題。

對於編譯速度的優化,我還有幾個建議:

  • 更換編譯機器。對於實力雄厚的公司,直接更換 Mac 或者其餘更給力的設備做爲編譯機,這種方式是最簡單的。
  • Build Cache。能夠將大部分不常改變的項目拆離出去,並使用遠端 Cache模式保留編譯後的緩存。
  • 升級 Gradle 和 SDK Build Tools。咱們應該及時去升級最新的編譯工具鏈,享受 Google 的最新優化成果。
  • 使用 Buck。不管是 Buck 的 exopackage,仍是代碼的增量編譯,Buck 都更加高效。但我前面也說過,一個大型項目若是要切換到 Buck,其實顧慮仍是比較多的。在 2014 年初微信就接入了 Buck,可是由於跟其餘項目協做的問題,致使在 2015 年切換回 Gradle 方案。

相比之下,可能目前最熱的 Flutter 中Hot Reload秒級編譯功能會更有吸引力。

固然最近幾個 Android Studio 版本,Google 也作了大量的其餘優化,例如使用AAPT2替代了 AAPT 來編譯 Android 資源。AAPT2 實現了資源的增量編譯,它將資源的編譯拆分紅 Compile 和 Link 兩個步驟。前者資源文件以二進制形式編譯 Flat 格式,後者合併全部的文件再打包。

除了 AAPT2,Google 還引入了 d8 和 R8,下面分別是 Google 提供的一些測試數據,以下圖。
在這裏插入圖片描述
在這裏插入圖片描述
那什麼是 d8 和 R8 呢?除了編譯速度的優化,它們還有哪些其餘的做用?能夠參考下面的介紹:Android D8 和 R8

3. 代碼優化

對於 Debug 包編譯,咱們更關心速度。可是對於 Release 包來講,代碼的優化更加劇要,由於咱們會更加在乎應用的性能。

下面我就分別講講 ProGuard、d八、R8 和 ReDex 這四種咱們可能會用到的代碼優化工具。

ProGuard

在微信 Release 包 12 分鐘的編譯過程裏,單獨 ProGuard 就須要花費 8 分鐘。儘管 ProGuard 真的很慢,可是基本每一個項目都會使用到它。加入了 ProGuard 以後,應用的構建過程流程以下:
在這裏插入圖片描述
ProGuard 主要有混淆、裁剪、優化這三大功能,它的整個處理流程以下:
在這裏插入圖片描述
其中優化包括內聯、修飾符、合併類和方法等 30 多種,具體介紹與使用方法你能夠參考官方文檔

D8

Android Studio 3.0 推出了d8,並在 3.1 正式成爲默認工具。它的做用是將「.class」文件編譯爲 Dex 文件,取代以前的 dx 工具。
在這裏插入圖片描述
d8 除了更快的編譯速度以外,還有一個優化是減小生成的 Dex 大小。根據 Google 的測試結果,大約會有 3%~5% 的優化。
在這裏插入圖片描述

R8

R8 在 Android Studio 3.1 中引入,它的志向更加高遠,它的目標是取代 ProGuard 和 d8。咱們能夠直接使用 R8 把「.class」文件變成 Dex。
在這裏插入圖片描述
同時,R8 還支持 ProGuard 中混淆、裁剪、優化這三大功能。因爲目前 R8 依然處於實驗階段,網上的介紹資料並很少,你能夠參考下面這些資料:
ProGuard 和 R8 對比:ProGuard and R8: a comparison of optimizers
Jake Wharton 大神的博客最近有不少 R8 相關的文章:https://jakewharton.com/blog/
R8 的最終目的跟 d8 同樣,一個是加快編譯速度,一個是更強大的代碼優化。

ReDex

若是說 R8 是將來想取代的 ProGuard 的工具,那 Facebook 的內部使用的ReDex其實已經作到了。Facebook 內部的不少項目都已經所有切換到 ReDex,再也不使用 ProGuard 了。跟 ProGuard 不一樣的是,它直接輸入的對象是 Dex,而不是「.class」文件,也就是它是直接針對最終產物的優化,所見即所得。

在前面的文章中,我已經不止一次提到 ReDex 這個項目,由於它裏面的功能實在是太強大了,具體能夠參考專欄前面的文章《包體積優化(上):如何減小安裝包大小?》。

  • Interdex:類重排和文件重排、Dex 分包優化。
  • Oatmeal:直接生成的 Odex 文件。
  • StripDebugInfo:去除 Dex 中的 Debug 信息。

此外,ReDex 中例如Type Erasure和去除代碼中的Aceess 方法也是很是不錯的功能,它們不管對包體積仍是應用的運行速度都有幫助,所以我也鼓勵你去研究和實踐一下它們的用法和效果。

可是 ReDex 的文檔也是萬年不更新的,並且裏面摻雜了一些 Facebook 內部定製的邏輯,因此它用起來的確很是不方便。目前我主要仍是直接研究它的源碼,參考它的原理,而後再直接單獨實現。

事實上,Buck 裏面其實也還有不少好用的東西,可是文檔裏面依然什麼都沒有提到,因此仍是須要「read the source code」。

  • Library Merge 和 Relinker
  • 多語言拆分
  • 分包支持
  • ReDex 支持

    持續交付

Gradle、Buck、Bazel 它們表明的都是狹義上的編譯,我認爲廣義的編譯應該包括打包構建、Code Review、代碼工程管理、代碼掃描等流程,也就是業界最近常常提起的持續集成。
在這裏插入圖片描述

目前最經常使用的持續集成工具備 Jenkins、GitLab CI、Travis CI 等,GitHub 也有提供本身的持續集成服務。每一個大公司都有本身的持續集成方案,例如騰訊的 RDM、阿里的摩天輪、大衆點評的MCI等。

下面我來簡單講一下我對持續集成的一些經驗和見解:

  • 自定義代碼檢查。每一個公司都會有本身的編碼規範,代碼檢查的目的在於防止不符合規範的代碼提交到遠程倉庫中。好比微信就定義了一套代碼規範,而且寫了專門的插件來檢測。例如日誌規範、不能直接使用 new Thread、new Handler 等,並且違反者將會獲得必定的懲罰。自定義代碼檢測能夠經過徹底本身實現或者擴展 Findbugs 插件,例如美團它們就利用 Findbugs 實現了Android 漏洞掃描工具 Code Arbiter
  • 第三方代碼檢查。業界比較經常使用的代碼掃描工具備收費的 Coverity,以及 Facebook 開源的Infer,例如空指針、多線程問題、資源泄漏等不少問題均可以掃描出來。除了增長檢測流程,我最大的體會是須要同時增長人員的培訓。我遇到不少開發者爲了解決掃描出來的問題,空指針就直接判空、多線程就直接加鎖,最後可能會形成更加嚴重的問題。
  • Code Review。關於 Code Review,集成 GitLab、Phabricator 或者 Gerrit 都是不錯的選擇。咱們必定要重視 Code Review,這也是給其餘人展現咱們「偉大」代碼的機會。並且咱們本身應該是第一個 Code Reviewer,在給別人 Review 以前,本身先以第三者的角度審視一次代碼。這樣先經過本身這一關的考驗,既尊重了別人的時間,也能夠爲本身樹立良好的技術品牌。

持續集成涉及的流程有不少,你須要結合本身團隊的現狀。若是隻是一味地去增長流程,有時候可能拔苗助長。

總結

在 Android 8.0,Google 引入了Dexlayout庫實現類和方法的重排,Facebook 的 Buck 也第一時間引入了 AAPT2。ReDex、d八、R8 其實都是相輔相成,能夠看到 Google 也在攝取社區的知識,但同時咱們也會從 Google 的新技術發展裏尋求思路。

我在寫今天的內容時還有另一個體會,Google 爲了解決 Android 編譯速度的問題,花了大量的力氣結果卻不盡如人意。我想說若是咱們勇於跳出系統的制約,可能纔會完全解決這個問題,正如在 Flutter 上面就能夠完美實現秒級編譯。其實作人、作事也是如此,咱們常常會陷入局部最優解的困局,或者走進「思惟怪圈」,這時若是能跳出路徑依賴,從更高的維度從新思考、審視全局,獲得的體會可能會徹底不同。

相關文章
相關標籤/搜索