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 導入了新的 Android 執行階段,可預先編譯應用程式,而非像 Dalvik 一樣及時編譯應用程式。如此一來,應用程式就能更快速地安裝,且成本也比較長。這對使用者而言是很好的缺點,因為使用者通常會安裝一次應用程式並多次使用,但所導致的開發速度變慢,因為使用者會多次安裝應用程式,且每個版本在最多次數的情況下會執行。

bazel mobile-install 的方法

bazel mobile-install 做了以下改善:

  • 分割 DEX 處理。建構應用程式的 Java 程式碼後,Bazel 會將類別檔案分割為大小相當相等的部分,並分別叫用 dx。系統不會對自上次建構以來未曾變更的資料分割叫用 dx

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

  • 從 .apk 外部載入應用程式的部分內容。一個小型虛設常式應用程式會導入 .apk,從行動裝置端安裝目錄載入 Android 資源、Java 程式碼和原生程式碼,然後將控制權轉移到實際應用程式。這一切都會對應用程式公開,但下文所述的幾個極端情況除外。

資料分割 DEX

分割 dex 十分簡單:在建構 .jar 檔案後,工具會將這些檔案分割成大小大致相等的獨立 .jar 檔案,然後針對與上次建構後變更的內容叫用 dx。判斷要 DEX 處理的資料分割的邏輯並不僅限於 Android,而是使用 Bazel 的一般變更裁舊演算法。

第一版的資料分割演算法會先按字母順序排序 .class 檔案,然後再將清單切成大小相同的部分,但事實證明,這樣的結果:新增或移除的類別 (即使是巢狀或匿名),會導致所有類別在移動後按照字母順序排列,進而導致重複 DEX 處理。因此,該公司決定對 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 提供支援)