本文來自張紹文老師的《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
不管是微信的編譯優化,仍是 Tinker 項目,都涉及比較多的編譯相關知識,所以我在 Android 編譯方面研究頗多,經驗也比較豐富。Android 的編譯構建流程主要包括代碼、資源以及 Native Library 三部分,整個流程能夠參考官方文檔的構建流程圖。緩存
Gradle是 Android 官方的編譯工具,它也是 GitHub 上的一個開源項目。從 Gradle 的更新日誌能夠看到,當前這個項目還更新得很是頻繁,基本上每一兩個月都會有新的版本。對於 Gradle,我感受最痛苦的仍是 Gradle Plugin 的編寫,主要是由於 Gradle 在這方面沒有完善的文檔,所以通常都只能靠看源碼或者斷點調試的方法。最近我所在的公司就準備用Gradle搞一個渠道打包工具,對於項目的打包和構建過程,也是深有體會。微信
可是編譯實在過重要了,每一個公司的狀況又各不相同,必須強行造一套本身的「輪子」。已經開源的項目有 Facebook 的Buck以及 Google 的Bazel。多線程
爲何要本身「造輪子」呢?主要有下面幾個緣由:
「程序員最痛恨寫文檔,還有別人不寫文檔」,因此它們的文檔也是比較少的,若是想作二次定製開發會感到很痛苦。若是你想把編譯工具切換到 Buck 和 Bazel,須要下很大的決心,並且還須要考慮和其餘上下游項目的協做。固然即便咱們不去直接使用,它們內部的優化思路也很是值得咱們學習和參考。
Gradle、Buck、Bazel 都是以更快的編譯速度、更強大的代碼優化爲目標,咱們下面一塊兒來看看它們作了哪些努力。
回想一下咱們的 Android 開發生涯,在編譯這件事情上面究竟浪費了多少時間和生命。正如前面我所說,編譯速度對團隊效率很是重要。
關於編譯速度,咱們最關心的可能仍是編譯 Debug 包的速度,尤爲是增量編譯(incremental build)的速度,咱們但願能夠作到更加快速的調試。正以下圖所示,咱們每次代碼驗證都要通過編譯和安裝兩個步驟。
此處,咱們從編譯時間和安裝時間兩個緯度來看Android的編譯速度。
對於增量編譯,我先來說講 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 看起來是否是很不錯,可是在大型項目裏面,它的性能依然很是糟糕,主要緣由是:
你還能夠看看這一個 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 中,它的架構圖以下。
android:vmSafeMode=「true」
來關閉虛擬機的 JIT 優化,這樣也就不會出現 Tinker 在Android N 混合編譯遇到的問題。對於編譯速度的優化,我還有幾個建議:
相比之下,可能目前最熱的 Flutter 中Hot Reload秒級編譯功能會更有吸引力。
固然最近幾個 Android Studio 版本,Google 也作了大量的其餘優化,例如使用AAPT2替代了 AAPT 來編譯 Android 資源。AAPT2 實現了資源的增量編譯,它將資源的編譯拆分紅 Compile 和 Link 兩個步驟。前者資源文件以二進制形式編譯 Flat 格式,後者合併全部的文件再打包。
除了 AAPT2,Google 還引入了 d8 和 R8,下面分別是 Google 提供的一些測試數據,以下圖。
那什麼是 d8 和 R8 呢?除了編譯速度的優化,它們還有哪些其餘的做用?能夠參考下面的介紹:Android D8 和 R8
對於 Debug 包編譯,咱們更關心速度。可是對於 Release 包來講,代碼的優化更加劇要,由於咱們會更加在乎應用的性能。
下面我就分別講講 ProGuard、d八、R8 和 ReDex 這四種咱們可能會用到的代碼優化工具。
在微信 Release 包 12 分鐘的編譯過程裏,單獨 ProGuard 就須要花費 8 分鐘。儘管 ProGuard 真的很慢,可是基本每一個項目都會使用到它。加入了 ProGuard 以後,應用的構建過程流程以下:
ProGuard 主要有混淆、裁剪、優化這三大功能,它的整個處理流程以下:
其中優化包括內聯、修飾符、合併類和方法等 30 多種,具體介紹與使用方法你能夠參考官方文檔。
Android Studio 3.0 推出了d8,並在 3.1 正式成爲默認工具。它的做用是將「.class」文件編譯爲 Dex 文件,取代以前的 dx 工具。
d8 除了更快的編譯速度以外,還有一個優化是減小生成的 Dex 大小。根據 Google 的測試結果,大約會有 3%~5% 的優化。
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 同樣,一個是加快編譯速度,一個是更強大的代碼優化。
若是說 R8 是將來想取代的 ProGuard 的工具,那 Facebook 的內部使用的ReDex其實已經作到了。Facebook 內部的不少項目都已經所有切換到 ReDex,再也不使用 ProGuard 了。跟 ProGuard 不一樣的是,它直接輸入的對象是 Dex,而不是「.class」文件,也就是它是直接針對最終產物的優化,所見即所得。
在前面的文章中,我已經不止一次提到 ReDex 這個項目,由於它裏面的功能實在是太強大了,具體能夠參考專欄前面的文章《包體積優化(上):如何減小安裝包大小?》。
此外,ReDex 中例如Type Erasure和去除代碼中的Aceess 方法也是很是不錯的功能,它們不管對包體積仍是應用的運行速度都有幫助,所以我也鼓勵你去研究和實踐一下它們的用法和效果。
可是 ReDex 的文檔也是萬年不更新的,並且裏面摻雜了一些 Facebook 內部定製的邏輯,因此它用起來的確很是不方便。目前我主要仍是直接研究它的源碼,參考它的原理,而後再直接單獨實現。
事實上,Buck 裏面其實也還有不少好用的東西,可是文檔裏面依然什麼都沒有提到,因此仍是須要「read the source code」。
ReDex 支持
Gradle、Buck、Bazel 它們表明的都是狹義上的編譯,我認爲廣義的編譯應該包括打包構建、Code Review、代碼工程管理、代碼掃描等流程,也就是業界最近常常提起的持續集成。
目前最經常使用的持續集成工具備 Jenkins、GitLab CI、Travis CI 等,GitHub 也有提供本身的持續集成服務。每一個大公司都有本身的持續集成方案,例如騰訊的 RDM、阿里的摩天輪、大衆點評的MCI等。
下面我來簡單講一下我對持續集成的一些經驗和見解:
持續集成涉及的流程有不少,你須要結合本身團隊的現狀。若是隻是一味地去增長流程,有時候可能拔苗助長。
在 Android 8.0,Google 引入了Dexlayout庫實現類和方法的重排,Facebook 的 Buck 也第一時間引入了 AAPT2。ReDex、d八、R8 其實都是相輔相成,能夠看到 Google 也在攝取社區的知識,但同時咱們也會從 Google 的新技術發展裏尋求思路。
我在寫今天的內容時還有另一個體會,Google 爲了解決 Android 編譯速度的問題,花了大量的力氣結果卻不盡如人意。我想說若是咱們勇於跳出系統的制約,可能纔會完全解決這個問題,正如在 Flutter 上面就能夠完美實現秒級編譯。其實作人、作事也是如此,咱們常常會陷入局部最優解的困局,或者走進「思惟怪圈」,這時若是能跳出路徑依賴,從更高的維度從新思考、審視全局,獲得的體會可能會徹底不同。