1. 程式人生 > >Android 元件化開發原理和配置

Android 元件化開發原理和配置

在Application的不斷髮展過程中,我們開發者要不斷地增加新特性。更多的程式碼就意味著更長的build時間和更長的增量build時間。在工程較大的專案中,build時間要佔到10%~15%的工作時間。這不僅是浪費時間,也是測試驅動工作方式(TDD)比較困難的原因。

把Application分成多個modules可以解決這個問題。在根據功能、層級或者其他方式進行module切分Application之前,我進行了一些試驗並收集一些資料。這篇文章就是分享一下我收集的實驗資料。

在開始實驗之前,我們先了解下理論知識,解釋一下做什麼可以減少增量build時間。

理論知識

我們建立一個Android Application,至少要包含一個application module,並且在build.gradle中設定application 的gradle 外掛:

apply plugin: 'com.android.application'

當編譯這種module時將生成.apk檔案。

一個application module不可以依賴另一個application module,只可以依賴library,就是配置 gradle library 外掛的module:

apply plugin: 'com.android.library'

釋出這樣的module時,得到的將是一個.aar檔案,與.jar檔案相比,aar檔案可以包含一些android相關的東西:比如資原始檔和manifest檔案。

編譯一個application module或者library檔案,大致可以分為gradle task代表的五個階段:
1. Preparation of dependecies

 在這個階段gradle檢測module依賴的所有library是否ready。如果這個module依賴於另一個module,則另一個module也要被編譯。

  1. Merging resources and processing Manifest 在這個階段之後,資源和Manifest檔案被打包。

  2. Compiling 在這個階段處理編譯器的註解,原始碼被編譯成位元組碼。如果使用了AspectJ,也是在這個階段進行處理。

  3. Postprocessing 所有帶 “transform”字首的task都是這個階段進行處理的。其中比較重要的是“transformClassesWithMultidexlist”和“transformClassesWithDex”,這個階段生成.dex檔案。

  4. Packaging and publishing 這個階段library生成.aar檔案,application生成.apk檔案。

我們的目標是降低增量build時間。是實驗中很難提高所有階段的速度,所以我們決定集中注意力在最耗時的階段。對於只有一個module的project來說,第三和第四個階段是最耗時的,第二個階段有時也比較耗時, 但如果java檔案沒有改動,則增量build常快。

我們知道,gradle在輸入不一致時才執行task,當沒有改動時也不重新build module,根據這個理論我們可以假設:
多元件project增量build比較快,是因為只有改動過的module才需要重新編譯

我們來驗證一下是否正確!

實驗

Project使用gradle 2.2.2 版本,支援的最低版本是Android 15,但是這已經足夠覆蓋大部分Android裝置了。為了讓Project更真實,所有的module都依賴butterknife,因為正常情況下,所有的project都會依賴其他library。

Application module命名為 "app", 其他library命名為"app2", "app3"等等。

每個module有大約100個package,15000 class, 90000個方法,這樣大概有兩個dex檔案,或者三個。所以的方法儲存在 .apk檔案中,關閉minifying和shrinking。

所有的測試可以使用gradle 的profiler,只需要在命令之後加“ --profile”即可:

./gradle assembleDebug --profile

最終生成一個.html檔案。

每個過程我們重複了4~15次,確保結果的正取性。

java程式碼自動生成

徒手寫15000個class太耗時,所以我用python寫了一個程式碼生成指令碼。指令碼地址:Gist。下邊是生成程式碼的一個圖解:

pure java module

我沒有給純java程式碼module做實驗。但是我認為這種情況下會更快一些,build過程中,編譯任務較少如沒有資源合併任務時,耗時較少。

如果你對純程式碼module比較感興趣,可以在下邊評論。或者你可以clone程式碼並根據需要進行修改:gihub。但是要注意,編譯時間與編譯環境是有關係的,所以要在自己的編譯環境進行多次實驗,從而對結果進行比較。

其他

在實驗時如果同時執行一些其他任務,會增加build耗時,即使是Android studio同時開啟兩個project時,編譯時間會增加5%~10%。聽音樂,看YouTube,或者瀏覽網頁也會增加不少編譯時間。當我關掉除了Android studio以外的程式時,編譯速度增快了30%。

以下所有的實驗中,只開了Android studio和幾個必要的網頁。

開始實驗

初始狀態

開始時我建立了包含一個module的project,包含了15000個檔案。此時增量build時間是1m 10s。

3個modules

第一步把一個module分為三個module:一個application module和兩個library module。Application module依賴於library,兩個library相互獨立,每個module有大約5000個檔案和30000個方法。

如果只有application中有更新,則編譯時間是35s,這比初始狀態快了30s。但是如果library module有更新,即使另一個library module沒有改動,增量build時間會增加到50s,比初始狀態多了40s。

我們通過profile 報告來看看一下:

從上邊的圖表中我們可以library module耗時較多。我們注意到,對於兩個library module,debug和release的task都執行了。gradle執行了兩組任務而不是一組。與只有一個module相比這種情況額外耗費了40s。

如果我們可以避免這種情況則編譯時間與只有一個module的情況不會變慢,增加一些調整可能還會快於1m 10s。

而且這不是唯一的問題,我們來看一下module依賴:

dependencies {
    compile project(path: ':app2')
    compile project(path: ':app3')
}

上邊的程式碼有一個問題:如果像上邊這樣新增依賴,則application依賴於library的release版本,這與Android studio中的設定無關。Gradle Plugin User Guide中有說明:

預設情況下library只發布其release版本,並被所有依賴於library的project依賴,這與project 的build type無關。這是我們即將移除的一個臨時限制。

幸運的是,我們可以更改application的版本依賴。
首先,在build.gradle中增加如下程式碼,這會使library module同時編譯debug版本:

android {
    defaultConfig {
        defaultPublishConfig 'release'
        publishNonDefault true
    }
}

第二步, application module設定依賴如下:

dependencies {
    debugCompile project(path: ':app2', configuration: "debug")
    releaseCompile project(path: ':app2', configuration: "release")

    debugCompile project(path: ':app3', configuration: "debug")
    releaseCompile project(path: ':app3', configuration: "release")
}

此時debug版本的application將會依賴debug版本的library。我們在app2中做一些改動,重編譯看一下效果:

最明顯的區別是沒有了 :app2:packageReleaseJarArtifact,這節約了大約15秒。再加上一些其他的更新,最終時間為1m 32s,這比之前快來18秒,但是依然比初始配置慢22s。如果只有application module中有更新,則編譯時間與之前差不多34s vs 35s。

我沒有找到兩個build type(release 和debug)都編譯的合理解釋,只希望之前提到的gradle限制移除後,這個問題可以同時解決,在這裡可以找到一些issue: AOSP Issue Tracker。我也計劃花一些時間找到解決問題的其他方法,一個可能的辦法是當編譯debug版本時不執行所有的release task。

5個module

很明顯,編譯時間依賴於程式碼數量。如果減少一半的程式碼,則編譯時間可能會降低為之前的一半。如果把3個module變為原來的5個module,編譯時間將會減少大約40%左右。

如果只有application中有程式碼更新,則增量build時間只有24s,如果library中有程式碼更新,則增量build時間為50s,這比初始的1m 10s快了,但是我還有一些其他的技巧。

減少Application module的大小

不論哪一個module發生了更改,application module每次都會重新編譯。所以減小Application module很有必要。理想情況下,可以使用一個單獨對module整合整個應用,可以提供啟動介面,因為啟動介面通常要依賴很多特性。

這就是3+1,5+1配置的思路,在兩種配置中,都有一個較小的application module依賴於3個或者5個module,其他module相互獨立,並且大小相同,實驗結果如下:

我們可以看到有效降低了增量build時間。即使是library module中發生了更新,5+1的配置也比初始狀態快了一半。這是一個不錯的進步。

關於Butterknife

這裡需要說明一下,為什麼要加入butterknife 依賴。

在初始配置中 incremental compilation耗時1m 10s中的45s,但是如果移除butterknife則編譯只需要15s,提速三倍。整個不包含butterknife的增量build過程只需要40s。

這是library的問題嗎?

這是因為project中不允許Annotation processors 的增量build。可以在這裡看到相關的issue Gradle Jira, AOSP Issue Tracker,design docs。有一個comment是這樣的:

Annotation precessors 不支援java增量build,它依賴於gradle中的變化。
我們在配置了 
com.neenbedankt.android-apt的project中禁用它,因此它不是一個重要的問題。

個人覺得不應該在整個工程中移除Annotation processors,Dagger和Butterknife非常有用,但是可以設定某些module不依賴它們,這樣可以提高增量build速度。

其他技巧-設定API版本

編譯並不是唯一影響build時間的因素,生成DEX檔案同樣耗時。特別是當超過DEX 的限制時。使用multidex配置會增加build時間,build時需要判斷class需要放置到哪個DEX檔案中。參考 Android Studio documentation 中關於Android執行時Android系統對於用於多個dex的application的處理:

Android 5.0 之後 ART支援從APK檔案中載入DEX檔案。ART會在app安裝時提前編譯,掃描classN.dex檔案,編譯為單獨的.oat檔案。

這減少了build時間,原因是每個module生成自己的DEX檔案,然後不經修改直接放置到APK中。如果我們看一下build過程,則可以看到transformClassesWithMultidexlist並不會執行。編譯過程速度加快,在 這裡可以檢視更多資訊。

更快的編譯配置

配置在debug中使用API 21,我測試了5+1 的情況,結果如下:

即使是library中改動增量build時間只有17s。但是當所有module依賴於一個module,當被依賴的module更新時,增量build時間從17s增加到42s。

使用測試驅動方式(Test Driven Way)開發 Library Module

測試驅動式(TDD)開發的難點就在於single module的build時間。TDD需要不斷執行test,在一分鐘內多次執行test是基本的實踐方式。但是當build需要耗時一分鐘或者更長時,則TDD沒有很好的效果。

在最終的配置中執行一個module只需要耗時9s,所以可以不斷執行test。

總結

首先,最重要的,元件化project可以顯著提高build速度,但是需要合理配置。

其次,如果module拆分不合理,則build時間可能增加,因為gradle會build release和debug兩個版本。

再次,元件化用於TDD非常有效,因為一個小的moudle build時間非常短。

第四, 同時執行很多工很減慢build速度,所以可以適當提升硬體配置。