bazel 行動安裝

回報問題 查看原始碼

快速的 Android 疊代開發

本頁面說明 bazel mobile-install 如何加快 Android 的疊代式開發速度。內容說明這個方法的優點,以及傳統應用程式安裝方法的難題。

摘要

如要迅速安裝對 Android 應用程式進行小幅變更,請按照下列步驟操作:

  1. 找出要安裝的應用程式 android_binary 規則。
  2. 移除 proguard_specs 屬性即可停用 Proguard。
  3. multidex 屬性設為 native
  4. dex_shards 屬性設為 10
  5. 透過 USB 連接搭載 ART (非 Dalvik) 的裝置,並啟用 USB 偵錯功能。
  6. 執行 bazel mobile-install :your_target。應用程式啟動速度會比平常慢。
  7. 編輯程式碼或 Android 資源。
  8. 執行 bazel mobile-install --incremental :your_target
  9. 享受無需費心等待的體驗。

以下為 Bazel 提供的以下指令列選項:

  • --adb 會告知 Bazel 要使用哪個 ADB 二進位檔
  • --adb_arg 可用來在 adb 的指令列中加入額外引數。如果多個裝置連線至工作站,請選取你要安裝的裝置之一: bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target
  • --start_app 會自動啟動應用程式

如有任何問題,請參考這個範例與我們聯絡

簡介

開發人員工具鍊的最重要屬性之一是,程式碼在變更程式碼後,才可在一秒內執行,而必須等待幾分鐘,有時才能收到回應,瞭解變更是否符合預期。

遺憾的是,用來建構 .apk 的傳統 Android 工具鍊需要許多單體式、依序執行的步驟,才能建構 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 會將類別檔案分割成大小相近的部分,並分別叫用 dxdx 不會在上次建構後未變更的資料分割中叫用。

  • 漸進式檔案傳輸。Android 資源、.dex 檔案和原生程式庫會從主要 .apk 中移除,並儲存在單獨的行動裝置安裝目錄下。如此一來,您不必重新安裝整個應用程式,即可單獨更新程式碼和 Android 資源。因此轉移檔案所需的時間較短,而且只有已變更的 .dex 檔案會在裝置端重新編譯。

  • 從 .apk 外部載入應用程式的某些部分。小型虛設常式應用程式會放入 .apk 中,該 .apk 會從裝置端的行動裝置安裝目錄載入 Android 資源、Java 程式碼和原生程式碼,然後將控制權轉移到實際的應用程式。這會對應用程式公開透明,但只有少數幾個具體情況例外。

分割 Dex 處理

資料分割的 dex 作業相當簡單明瞭:.jar 檔案建立完成後,工具會將這些檔案分割成大小大致相等的個別 .jar 檔案,然後對自前一版以來有所變更的項目叫用 dx。決定哪些資料分割要執行 DEX 的邏輯並不是專為 Android 所設,而是使用 Bazel 的一般變更縮減演算法。

第一個版本的資料分割演算法只是按字母順序排列 .class 檔案,然後將清單剪輯成大小相同的部分,但事實證明這個部分並非理想:如果新增或移除類別 (即便是巢狀結構或匿名類別),則所有類別在排序後都會按字母順序移動,進而再次執行去背化處理這些資料分割。因此,決定分割 Java 套件,而非個別類別。當然,如果新增或移除新套件,仍會產生許多資料分割,但與新增或移除單一類別的頻率相比要低許多。

資料分割數量是由 BUILD 檔案控管 (使用 android_binary.dex_shards 屬性)。在理想情況下,Bazel 會自動判斷資料分割數量是最好的,但 Bazel 目前必須先知道一組動作 (例如在建構期間要執行的指令) 才能執行任何資料分割,因此無法決定最適當的資料分割數量,因為安裝後,應用程式最終會執行多少工作。一般來說,部署速度會加快,速度也就越多。理想點通常介於 10 到 50 個資料分割之間。

漸進式檔案傳輸

建構應用程式後,下一步是安裝 (建議盡量採用最最少的工作)。安裝程序包含下列步驟:

  1. 安裝 .apk (通常使用 adb install)
  2. 將 .dex 檔案、Android 資源和原生資料庫上傳至行動裝置安裝目錄

第一個步驟沒有太多成效增幅:應用程式安裝是否已安裝。Bazel 目前仰賴使用者指出是否應透過 --incremental 指令列選項執行這個步驟,因為在所有情況下,都無法判斷是否必要。

在第二個步驟中,系統會將建構作業中的應用程式檔案與裝置端資訊清單檔案進行比較,其中會列出裝置上的哪些應用程式檔案及其檢查碼。所有新檔案都會上傳到裝置,所有變更的檔案都會更新,任何已移除的檔案也會從裝置中刪除。如果沒有資訊清單,系統會假設每個檔案都需要上傳。

請注意,您可以在裝置上變更檔案 (而非資訊清單中的核對和),藉此破解漸進式安裝演算法。系統計算裝置上檔案的總和檢查碼,已防範此行為,但認為這並不值得安裝增加的安裝時間。

Stub 應用程式

虛設常式應用程式是從裝置端 mobile-install 目錄載入 dexe、原生程式碼和 Android 資源的位置。

實際載入方法是將 BaseDexClassLoader 設為子類別,並且是充分記錄的技術。這項作業會在應用程式的任何類別載入前執行,因此系統會將 APK 中的任何應用程式類別放在裝置端的 mobile-install 目錄中,以便在沒有 adb install 的情況下更新。

此步驟需要在應用程式的任何類別載入之前完成,這樣就不需要將應用程式類別加入 .apk 內,也就是說,變更這些類別時就需要重新安裝。

方法是將 AndroidManifest.xml 中指定的 Application 類別替換為虛設常式應用程式。這會控制應用程式啟動的時機,並透過 Android 架構內部的 Java 反射功能,在最快的時間點 (其建構函式) 適當調整類別載入器和資源管理員。

虛設常式應用程式的另一個事項,是將行動裝置安裝應用程式的原生資料庫複製到其他位置。這是因為動態連結器需要在檔案上設定 X 位元;這對於非根 adb 可存取的任何位置都無法做到。

完成上述所有操作後,虛設常式應用程式會例項化實際的 Application 類別,並將所有參照變更為 Android 架構中的實際應用程式。

結果

效能

一般而言,bazel mobile-install 能在小幅變更後建構及安裝大型應用程式,速度提升了 4 到 10 倍。

我們針對部分 Google 產品計算出下列數據:

當然,這取決於變更的性質:變更基礎程式庫後重新編譯所需的時間較長。

限制

至於虛設常式應用程式,並非在所有情況下都能發揮效用。以下列舉幾種無法正常運作的情況:

  • Context 轉換為 ContentProvider#onCreate() 中的 Application 類別時。系統會在應用程式啟動時呼叫此方法,之後才有機會取代 Application 類別的執行個體,因此 ContentProvider 仍會參照虛設常式應用程式,而非實際使用的應用程式。話說回來,這並非錯誤,因為您不會像這樣降低 Context,但在 Google 的幾個應用程式中似乎會發生這種狀況。

  • bazel mobile-install 安裝的資源只能在應用程式內使用。如果其他應用程式透過 PackageManager#getApplicationResources() 存取資源,這些資源將會是來自上次非漸進式安裝。

  • 未搭載 ART 的裝置。雖然虛設常式應用程式可在 Froyo 和以上版本中正常運作,但 Dalvik 發現如果應用程式的程式碼在特定情況下透過多個 .dex 檔案 (例如以特定方式使用 Java 註解) 發布,會判定應用程式正確無誤。只要您的應用程式不處理這些錯誤,應用程式應該就能與 Dalvik 搭配使用 (請注意,支援舊版 Android 並非我們的重點)