天天看點

阿裡零售通 App 工程提效實踐:提升 50% 的編譯速度

阿裡零售通 App 工程提效實踐:提升 50% 的編譯速度

前言

目前,大多數 Android 工程都是基于

Gradle

工具進行建構和編譯的,一開始,當你的工程不夠複雜,或者還隻是小型項目的情況下,基本都不需要去關心建構優化的事情,而随着業務變得複雜、代碼量的增多以及越來越多的依賴,原有的 單 module 工程變成了多 module 工程,建構時間變得也越來越多。

說到這裡,有的同學可能會有疑惑,對于大項目來說,這麼多子產品和依賴,本來就需要更多的編譯時間,還怎麼減少建構時間?恰恰相反,實際上越大的項目越能省出來時間。

為了讓開發者引起對建構分析的重視,Gradle 官方在最近的版本更新中推出了一個神器

build scan

,可視化的深入分析和診斷所有建構相關的資料,并基于此分析結果幫助開發者找出建構問題以及針對建構性能進行優化。

背景

零售通買家端 App 是面向線下小店的一個補貨工具,可以快速幫助小店老闆通過手機完成進貨等功能。随着業務的不斷發展,我們的工程規模和代碼量也得到了極大的發展,目前我們的用戶端工程裡的 module 數量達到了40多個,而涉及到的相關依賴庫則有 200 多個,已然成為一個中大型項目,可以想象每次編譯的時候,我和我的小夥伴們是多麼痛苦。

問題

  • 編譯速度慢到懷疑人生,一般,我們的項目正常編譯時間大概在每次 3 分多鐘,如果中間連續編譯好幾次,時間也會相應成倍增加,同時,電腦 cpu 運轉的聲音也會越來越響,會有種錯覺是在小霸王學習機上進行開發,印象裡最深的有一次編譯花了十幾分鐘時間。當然,後面變聰明了,隻要聽到小霸王學習機的聲音,就意味着可以重新開機電腦了。
    > 電腦組態:公司标配 MacBook Pro,16G 記憶體,i7 處理器
               
  • 莫名其妙的編譯問題,無法通過 IDE 的

    Run

    按鈕運作,必須要執行指令行編譯

    ./gradlew clean assembleDebug

    ,本來時間就很慢了,加上

    clean

    指令後,更是雪上加霜。
  • module 找不到間接依賴庫的類,中間有一次更新 Android Studio 版本後,工程裡的 module 裡的間接依賴庫裡的類都找不到了,直接飙紅了。不過,并不影響正常編譯和運作。原因是 *.iml 檔案裡的沒有自動生成間接依賴的 library 導緻的。應該是 Android Studio 和 Gradle 低版本相容問題導緻,後面更新到最新版本後解決。

以上編譯相關的問題,幾乎每天都在消耗着我和我的小夥伴的寶貴時間,也嚴重影響了日常的開發效率,是以,當看到多個小夥伴在長時間編譯後受不了而投來的幽幽眼神後,我想,是時候開始解決這些問題了。

解決

1. 更新 Gradle 版本

在開始任何分析優化工作前,第一件事情是更新你的 Gradle 版本,這也是最簡單見效的方法,新的版本,通常意味更好的性能和特性,這也是一條來自 Gradle 官方的優化建議

A guide on performance tuning would normally start with profiling and something about premature optimisation being the root of all evil. Profiling is definitely important and the guide discusses it later, but there are some things you can do that will impact all your builds for the better at the flick of a switch.

Use latest Gradle and JVM versions, The Gradle team works continuously on improving the performance of different aspects of Gradle builds.

但是,對于我們的工程來說,更新 Gradle 版本并不是一件輕松的事情,我們有專門的内部打包平台,并引入了它的打包插件,而内部打包插件強依賴了低版本的 Gradle 。雖然,内部打包插件也提供了基于新版本的插件,但是,嘗試更新後産生了一些插件相容問題,而且,後續我們也無法跟随官方的 Gradle 版本進行更新,考慮到通常隻有在發版本或者內建測試的時候才會使用内部平台打包。于是,我們采用了另一種方式,通過腳本的控制,讓本地日常開發和編譯使用新版本的 Gradle,内部平台打包依然走老版本的 Gradle 打包。

在更新 Gradle 和 Android Gradle Plugin 版本後,所帶來的編譯時間的提升非常明顯。是以,如果你使用的 Gradle 版本越低,那麼更新新版本後,所帶來的提升也越明顯。

2. 優化和減少 module

我們回顧下 Gradle 的建構生命周期:

  • 初始化階段: 在初始化階段,支援單個或者多項目建構,它決定哪些項目子產品要參與建構,并為每個項目子產品建立一個工程執行個體。
  • 配置階段: 在這一階段,通常是配置每個項目子產品。

    并執行所有項目子產品的建構中的一部分腳本。

  • 任務執行階段: 在初始化和配置階段執行完成後,Gradle 開始執行每個參與的任務。

Gradle 提供了一個

--profile

指令,來幫助我們了解在每個階段所花費的時間,并生成一個報告。比如,你可以執行以下指令來擷取到一份報告,位于

rootProject/build/report/profile/***.html

./gradlew assembleDebug --profile
           

上圖是在我們項目執行的指令後生成的報告,總建構時間花了 3分多種。

  • Summary:建構時間概要
  • Configuration:配置階段花費的時間
  • Dependency Resolution:依賴解析花費的時間
  • Task Execution:每個任務執行的時間,也是耗時最多的階段
Tips:Summary 概要裡的 Task Execution 時間是每個子產品累計相加,實際上多子產品的任務是并行執行的。

Task Execution 裡是每個子產品編譯所花費的具體時間。同時,也可以看出編譯一個 module 的成本還是比較大的,因為有很多 task 需要執行。是以,接下來的工作就是減少和優化這些的子產品。

在重新梳理了下我們的項目子產品後,發現有一部分 module 裡面其實就隻有兩三個類,完全沒有必要單獨 module,可以轉移到 app 或者 common-business 子產品裡,而有一些 module 裡的核心邏輯已經抽成了獨立 aar 引用,針對殘留的代碼邏輯,則進行了優化。在完成這些工作後,我們的工程子產品數量由 40 多個減少到了 20 多個,是的,我幹掉了将近一半的 module。

建議:能不建 module,就不要建立 module,如果确實需要,可以使用 aar 方式代替,aar 編譯是有緩存的話,理論上應該比 module 要快的,具體我沒有對比過。有興趣的同學,可以實際對比下試試

3. 一些配置優化

  • 增加 snapshot 緩存政策開關,有時候,為了 snapshot 版本的變動可以實時生效,會加上配置

    cacheChangingModulesFor 0, 'seconds'

    ,但是,這樣就會在每次編譯都要去雲端比對是否有變動。是以,你可以通過在

    local.properties

    增加開關來控制,在不需要的時候,關閉它。
    configurations.all {           
    resolutionStrategy {
    if (rootProject.ext.cacheChangingModulesForDisable == false) {
           cacheChangingModulesFor 0, 'seconds'
           //針對  dynamic (例如2.+)的配置同理
           //cacheDynamicVersionsFor 0, 'seconds'
       }           
    }
    }
                   
  • 避免編譯不必要的資源,比如不必要的語言本地化

    android {

    ...

    productFlavors {

    dev {
      ...
      // 隻編譯以下語言和分辨率的資源
      resConfigs "zh", 'zh-rCN', "xxhdpi"
    }
    ...           
  • 不同的 Gradle 版本,一些配置優化也會有差別,更多配置優化可以參考 [Gradle 提速:每天為你省下一杯喝咖啡的時間

    ](

    https://juejin.im/post/5be105fde51d455bad089fed)

4. 其他優化

  • 腳本邏輯優化,如避免使用一些網絡請求或者 IO 操作。
  • 去除重複和不必要的依賴

結果

好了,經過以上種種努力,終于到了收獲的時刻了。因為 Gradle 每次編譯的時間誤差還是比較大的,為了盡可能保證比對結果的客觀性,我們使用以下指令分别在優化前和優化後進行了 3 次編譯:

gradlew --profile --recompile-scripts --offline --rerun-tasks assembleDebug
           
  • --recompile-scripts

    : 繞過緩存,強制重新編譯腳本
  • --rerun-tasks

    : 強制重新運作所有 task,忽略任何優化
  • offline

    : 離線模式編譯
建構次數 優化前 優化後 減少率
第一次 3分20秒 1分59秒 - 40%
第二次 2分50秒 1分7秒 - 60%
第三次 2分40秒 54秒 - 67%

以上資料可以看到,在優化後的工程裡,編譯次數越多,編譯時間越少,實際所提升的速度也遠超過 50%。

而且,更新版本後,終于可以通過

Run

按鈕編譯了,最快的一次增量編譯時間隻有 9s !

總結

  1. 更新到最近的gradle 版本 并采用最新文法 比如implementation 代替compile等
  2. 代碼層面的優化 比如減少module 第三方庫 使用arr依賴
  3. 其他的一些優化配置等

感想

雖然我們的編譯時間減少到了 1 分多鐘,但仍然有較大優化空間,比如為了相容老的 Gradle 版本,目前我們還沒有辦法使用

implementation

替換

compile

,這樣,就可以有效的減少編譯時的依賴項,另外,後續也會借助于

build scan

去更深入分析和了解我們的建構資訊。

如果你所在的項目,建構編譯時間成了一種負擔,那麼,很有必要引起你的重視。也許,每個項目的實際情況可能都不一樣,但是,希望本文可以為你提供一些思路,我相信,随着你的深入分析之後,一定會有更好的辦法去解決,另外,重要的是,很少有這樣的性能優化,可以在短時間内帶來實際的提升效果,是以,現在,立刻開始你的建構優化吧。

如果,對建構優化有什麼問題或者不了解的地方,歡迎留言讨論。

參考

[Gradle 提速:每天為你省下一杯喝咖啡的時間

https://www.atatech.org/articles/123899)

繼續閱讀