快速疊代 Android 開發
本頁面將說明 bazel mobile-install
如何大幅加快 Android 的迭代式開發作業。本文將說明這種方法的優點,以及傳統應用程式安裝方法的挑戰。
摘要
如要快速安裝 Android 應用程式的小幅變更,請按照下列步驟操作:
- 找出要安裝的應用程式
android_binary
規則。 - 移除
proguard_specs
屬性,停用 Proguard。 - 將
multidex
屬性設為native
。 - 將
dex_shards
屬性設為10
。 - 透過 USB 連接執行 ART (而非 Dalvik) 的裝置,並在該裝置上啟用 USB 偵錯功能。
- 執行
bazel mobile-install :your_target
。 應用程式啟動速度會比平常慢一點。 - 編輯程式碼或 Android 資源。
- 執行
bazel mobile-install --incremental :your_target
。 - 享受不用等待的快感。
以下是一些可能實用的 Bazel 指令列選項:
--adb
會告訴 Bazel 要使用哪個 ADB 二進位檔--adb_arg
可用於在adb
的指令列中新增其他引數。這項功能的一個實用應用情境是,如果工作站連結了多部裝置,您可以選取要安裝的裝置:bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
--start_app
會自動啟動應用程式
簡介
開發人員工具鍊最重要的特徵之一就是速度:變更程式碼後,如果可以在一秒內看到程式碼執行,與需要等待數分鐘 (甚至數小時) 才能取得變更是否符合預期的任何意見回饋,兩者之間的差異可說是天壤之別。
不幸的是,傳統的 Android 工具鍊用於建構 .apk 時,需要執行許多單一、連續的步驟,而且必須完成所有這些步驟才能建構 Android 應用程式。在 Google 中,對於 Google 地圖等大型專案,等待五分鐘才能建構單行變更,並非罕見現象。
bazel mobile-install
結合變更修剪、工作區塊劃分,並巧妙地操控 Android 內部,無須變更任何應用程式程式碼,即可大幅加快 Android 的迭代開發作業。
傳統應用程式安裝問題
建構 Android 應用程式時會發生一些問題,包括:
Dex 處理。根據預設,「dx」會在建構作業中精確呼叫一次,且不會瞭解如何重複使用先前建構作業的工作:即使只有一個方法有所變更,它也會再次解析每個方法。
將資料上傳至裝置。adb 不會使用 USB 2.0 連線的完整頻寬,因此較大的應用程式可能需要花費大量時間上傳。即使只有部分內容 (例如資源或單一方法) 發生變更,整個應用程式也會上傳,因此這可能會造成重大瓶頸。
編譯為原生程式碼。Android L 推出了 ART,這是一種新的 Android 執行階段,會預先編譯應用程式,而非像 Dalvik 一樣在適當時機編譯。這麼做可讓應用程式速度大幅提升,但安裝時間會變長。這對使用者來說是個不錯的折衷,因為使用者通常會安裝一次應用程式,然後多次使用,但如果應用程式安裝次數很多,且每個版本最多只執行幾次,就會導致開發速度變慢。
bazel mobile-install
的做法
bazel mobile-install
改善了以下項目:
分割 Dex 處理。建構應用程式的 Java 程式碼後,Bazel 會將類別檔案切割成大致相等大小的部分,並分別在這些部分上叫用
dx
。dx
不會在自上次建構後未變更的區塊上叫用。增量檔案傳輸。Android 資源、.dex 檔案和原生程式庫會從主要 .apk 中移除,並儲存在單獨的行動安裝目錄下。這樣一來,您就能獨立更新程式碼和 Android 資源,而無須重新安裝整個應用程式。因此,傳輸檔案所需的時間較短,且只有變更的 .dex 檔案會在裝置上重新編譯。
從 .apk 外部載入應用程式的部分內容。將小型存根應用程式放入 .apk 中,該應用程式會從裝置端的行動安裝目錄載入 Android 資源、Java 程式碼和原生程式碼,然後將控制權轉移至實際應用程式。除了下文所述的少數極端情況外,這對應用程式來說都是透明的。
分割 Dex 處理
分割式解析相當簡單:建構 .jar 檔案後,工具會將這些檔案分割成大小相近的個別 .jar 檔案,然後針對上次建構後變更的檔案叫用 dx
。決定要將哪些分片進行 dex 的邏輯並非 Android 專屬,而是使用 Bazel 的一般變更修剪演算法。
第一版的分割演算法只是依字母順序排列 .class 檔案,然後將清單切成大小相等的部分,但這已證實不是最佳做法:如果新增或移除類別 (即使是巢狀或匿名類別),也會導致後續所有依字母順序排列的類別都會往後移動一個,導致這些分割區塊再次進行索引。因此,我們決定將 Java 套件分割,而非個別類別。當然,如果新增或移除新套件,這仍會導致許多分割區進行解析,但這比新增或移除單一類別的頻率低得多。
分割數量由 BUILD 檔案控管 (使用 android_binary.dex_shards
屬性)。在理想情況下,Bazel 會自動判斷最適合的區段數量,但 Bazel 目前必須先知道一組動作 (例如在建構期間要執行的指令),才能執行任何動作,因此無法判斷最適合的區段數量,因為它不知道應用程式最終會有多少 Java 類別。一般來說,區段數量越多,建構和安裝作業的速度就會越快,但應用程式啟動速度會變慢,因為動態連結器必須執行更多工作。最佳值通常介於 10 到 50 個分片之間。
增量檔案傳輸
建構應用程式後,下一步就是安裝應用程式,最好是盡可能以最少的力氣完成。安裝程序包含下列步驟:
- 安裝 .apk (通常使用
adb install
) - 將 .dex 檔案、Android 資源和原生資料庫上傳至 mobile-install 目錄
第一步驟的增量不大:應用程式要嘛已安裝,要嘛未安裝。Bazel 目前無法在所有情況下判斷是否需要執行此步驟,因此必須仰賴使用者透過 --incremental
指令列選項,指出是否應執行此步驟。
在第二個步驟中,系統會將建構作業產生的應用程式檔案與裝置端資訊清單檔案進行比較,後者會列出裝置上的應用程式檔案和其總和檢查碼。所有新檔案都會上傳至裝置,所有已變更的檔案都會更新,而所有已移除的檔案都會從裝置中刪除。如果沒有清單,系統會假設需要上傳所有檔案。
請注意,您可以透過變更裝置上的檔案來欺騙增量安裝演算法,但無法變更資訊清單中的總和檢查碼。我們可以透過計算裝置上檔案的總和檢查碼來防範這種情況,但這會增加安裝時間,因此我們認為不值得。
Stub 應用程式
在 Stub 應用程式中,會執行從裝置端 mobile-install
目錄載入 dex、原生程式碼和 Android 資源的魔法。
實際的載入作業是透過 BaseDexClassLoader
子類別實作,這是一種有相當詳盡說明的技術。這會在任何應用程式類別載入前發生,因此 APK 中的任何應用程式類別都可以放置在裝置端 mobile-install
目錄中,以便在沒有 adb install
的情況下進行更新。
這項操作必須在載入應用程式的任何類別之前完成,因此不需要在 .apk 中加入任何應用程式類別,否則變更這些類別就必須完全重新安裝。
方法是將 AndroidManifest.xml
中指定的 Application
類別,替換為Stub 應用程式。這會在應用程式啟動時接管控制權,並在最早的時間 (其建構函式) 使用 Android 架構內部 Java 反射,適當調整類別載入器和資源管理工具。
另一個備用程式應用程式會將由 mobile-install 安裝的原生程式庫複製到其他位置。這是必要的,因為動態連結器需要在檔案上設定 X
位元,而非根目錄 adb
無法存取任何位置。
完成所有這些操作後,輔助程式就會將實際的 Application
類別例項化,將所有對自身的參照變更為 Android 架構中的實際應用程式。
結果
成效
一般來說,bazel mobile-install
可在小幅變更後,將大型應用程式的建構和安裝速度加快 4 到 10 倍。
我們針對幾項 Google 產品計算出以下數字:
這當然取決於變更的性質:變更基礎程式庫後,重新編譯需要更多時間。
限制
並非所有情況下,Stub 應用程式執行的訣竅都能發揮作用。以下幾種情況會導致不如預期:
Context
轉換為ContentProvider#onCreate()
中的Application
類別時。這個方法會在應用程式啟動期間呼叫,因此我們無法更換Application
類別的例項,因此ContentProvider
仍會參照 Stub 應用程式,而非實際應用程式。這並非錯誤,因為您不應以這種方式下放Context
,但 Google 的部分應用程式似乎會發生這種情況。bazel mobile-install
安裝的資源只能在應用程式內使用。如果其他應用程式透過PackageManager#getApplicationResources()
存取資源,這些資源會來自上次非遞增安裝作業。未執行 ART 的裝置。雖然 Stub 應用程式在 Froyo 及後續版本上運作良好,但 Dalvik 有一個錯誤,會在特定情況下將應用程式視為不正確,例如在以「特定」方式使用 Java 註解時。只要您的應用程式不會觸發這些錯誤,就應該也能與 Dalvik 搭配運作 (但請注意,我們並未特別著重於支援舊版 Android)