Android の迅速な反復開発
このページでは、bazel mobile-install を使用して Android の反復開発
を大幅に高速化する方法について説明します。このアプローチのメリットと、
従来のアプリ インストール方法の課題について説明します。
概要
Android アプリに小さな変更をすばやくインストールするには、次の操作を行います。
- インストールするアプリの
android_binaryルールを見つけます。 proguard_specs属性を削除して、Proguard を無効にします。multidex属性をnativeに設定します。dex_shards属性を10に設定します。- ART(Dalvik ではない)を実行しているデバイスを USB で接続し、USB デバッグを有効にします。
bazel mobile-install :your_targetを実行します。アプリの起動が通常より少し 遅くなります。- コードまたは Android リソースを編集します。
bazel mobile-install --incremental :your_targetを実行します。- 待つ必要がなくなります。
Bazel のコマンドライン オプションで役立つものの一部を次に示します。
--adbは、使用する adb バイナリを Bazel に伝えます。--adb_argを使用すると、adbのコマンドラインに追加の引数を追加できます。 この方法の便利な用途の一つに、ワークステーションに複数のデバイスが接続されている場合に、インストール先のデバイスを選択することがあります。bazel mobile-install --adb_arg=-s --adb_arg=<SERIAL> :your_target--start_appはアプリを自動的に起動します。
不明な点がある場合は、 例 を参照するか、Google までお問い合わせください。
はじめに
デベロッパーのツールチェーンの最も重要な属性の一つに速度があります。コードを変更して 1 秒以内に実行できる場合と、変更が期待どおりに機能するかどうかについてフィードバックを得るまでに数分、場合によっては数時間待つ必要がある場合とでは、大きな違いがあります。
残念ながら、.apk をビルドするための従来の Android ツールチェーンには 多くのモノリシックなシーケンシャル ステップが含まれており、Android アプリをビルドするには、これらのステップをすべて実行する必要があります。Google では、Google マップなどの大規模なプロジェクトで、1 行の変更をビルドするのに 5 分かかることは珍しくありませんでした。
bazel mobile-install は、変更のプルーニング、ワーク シャーディング、Android 内部の巧妙な操作を組み合わせて使用することで、Android の反復開発を大幅に高速化します。アプリのコードを変更する必要はありません。
従来のアプリ インストールの問題
Android アプリのビルドには、次のような問題があります。
dex 変換。デフォルトでは、ビルドで「dx」が 1 回だけ呼び出されます。以前のビルドの作業を再利用する方法がわからないため、1 つのメソッドのみが変更された場合でも、すべてのメソッドが再度 dex 変換されます。
デバイスへのデータのアップロード。adb は USB 2.0 接続の全帯域幅を使用しないため、サイズの大きいアプリのアップロードに時間がかかることがあります。リソースや単一のメソッドなど、一部のみが変更された場合でも、アプリ全体が アップロードされるため、ボトルネックになる可能性があります。
ネイティブ コードへのコンパイル。Android L では、新しい Android ランタイムである ART が導入されました。 ART は、Dalvik のようにジャストインタイムでコンパイルするのではなく、アプリを事前コンパイルします。 これにより、インストール時間が長くなる代わりに、アプリの速度が大幅に向上します。ユーザーは通常、アプリを 1 回インストールして何度も使用するため、これはユーザーにとって良いトレードオフですが、アプリが 何度もインストールされ、各バージョンが数回しか実行されない開発では、速度が低下します。
bazel mobile-install のアプローチ
bazel mobile-install では、次のように改善されています。
シャーディングされた dex 変換。アプリの Java コードをビルドした後、Bazel はクラス ファイルをほぼ同じサイズのパーツに分割し、それらに対して個別に
dxを呼び出します。dxは前回のビルドから変更されていないシャードでは呼び出されません。増分ファイル転送。Android リソース、.dex ファイル、ネイティブ ライブラリは、メインの .apk から削除され、別の mobile-install ディレクトリに保存されます。これにより、アプリ全体を再インストールせずに、コードと Android リソースを個別に更新できます。そのため、 ファイルの転送に時間がかからず、変更された .dex ファイルのみが デバイス上で再コンパイルされます。
.apk の外部からアプリの一部を読み込む。小さなスタブ アプリケーションが .apk に配置され、デバイス上の mobile-install ディレクトリから Android リソース、Java コード、ネイティブ コード を読み込み、実際のアプリに制御を転送します。これは、後述するいくつかの特殊なケース を除き、アプリに対して透過的です。
シャーディングされた dex 変換
シャーディングされた dex 変換は比較的簡単です。.jar ファイルがビルドされると、a
ツール
によってほぼ同じサイズの個別の .jar ファイルに分割され、前回のビルドから変更されたファイルに対して
dxが呼び出されます。dex 変換するシャードを決定するロジックは Android に固有のものではなく、Bazel の一般的な変更プルーニング アルゴリズムを使用します。
シャーディング アルゴリズムの最初のバージョンでは、.class ファイルがアルファベット順に並べ替えられ、リストが同じサイズのパーツに分割されましたが、これは最適ではありませんでした。クラスが追加または削除されると(ネストされたクラスや匿名クラスでも)、その後のすべてのクラスがアルファベット順に 1 つずつシフトし、それらのシャードが再度 dex 変換されることになります。そのため、個々のクラスではなく、Java パッケージをシャーディングすることにしました。もちろん、新しいパッケージが追加または削除された場合でも、多くのシャードが dex 変換されますが、これは単一のクラスの追加または削除よりも頻度がはるかに 少なくなります。
シャードの数は、BUILD ファイル(
android_binary.dex_shards 属性を使用)によって制御されます。理想的には、Bazel が
最適なシャード数を自動的に判断しますが、Bazel は現在、アクションのセット(ビルド中に実行されるコマンドなど)を
把握してから実行する必要があるため、アプリに最終的に含まれる Java クラスの数がわからないため、最適なシャード数を
判断できません。一般に、シャードが多いほど、ビルドと
インストールの速度は速くなりますが、動的
リンカーの処理が増えるため、アプリの起動が遅くなります。通常、最適なのは 10 ~ 50 個のシャードです。
増分ファイル転送
アプリをビルドしたら、次のステップはインストールです。できるだけ手間をかけずにインストールすることをおすすめします。インストールは次の手順で行います。
- .apk のインストール(通常は
adb installを使用) - .dex ファイル、Android リソース、ネイティブ ライブラリを mobile-install ディレクトリにアップロードする
最初のステップでは、増分はあまりありません。アプリがインストールされるか
されないかのどちらかです。Bazel は、このステップが必要かどうかをすべての場合で判断できないため、ユーザーがこのステップを実行するかどうかを
--incremental コマンドライン オプションを使用して指定する必要があります。
2 番目のステップでは、ビルドのアプリファイルが、デバイス上のアプリファイルとその チェックサムを一覧表示するデバイス上の マニフェスト ファイルと比較されます。新しいファイルはデバイスにアップロードされ、変更されたファイルは 更新され、削除されたファイルは デバイスから削除されます。マニフェストが存在しない場合は、すべてのファイルをアップロードする必要があるとみなされます。
デバイス上のファイルを変更しても、マニフェストのチェックサムを変更しないことで、増分インストール アルゴリズムをだますことができます。デバイス上のファイルのチェックサムを計算することで、これを防ぐことができましたが、インストール時間の増加に見合うものではないと判断されました。
スタブ アプリケーション
スタブ アプリケーションは、デバイス上の mobile-install ディレクトリから dex、ネイティブ コード、
Android リソースを読み込むためのマジックが行われる場所です。
実際の読み込みは BaseDexClassLoader をサブクラス化して実装されており、
十分に文書化された手法です。これはアプリの
クラスが読み込まれる前に行われるため、apk 内のアプリケーション クラスをデバイス上の mobile-install ディレクトリに配置して、adb install を使用せずに更新できます。
アプリの クラスが読み込まれる前にこれを行う必要があります。これにより、アプリケーション クラスを .apk に含める必要がなくなり、これらのクラスを変更するには完全な 再インストールが必要になります。
これは、Application クラスを
AndroidManifest.xmlで指定された
スタブ アプリケーションに置き換えることで実現されます。アプリが起動すると、このクラスが制御を引き継ぎ、Android フレームワークの内部で Java リフレクションを使用して、クラスローダーと
リソース マネージャーを適切なタイミング(コンストラクタ)で調整します。
スタブ アプリケーションが行うもう一つのことは、mobile-install によってインストールされたネイティブ ライブラリ
を別の場所にコピーすることです。動的リンカーでは、ファイルにXビットを設定する必要があります。これは、ルート以外のadbがアクセスできる場所では実行できません。
これらの処理がすべて完了すると、スタブ アプリケーションは実際の
Application クラスをインスタンス化し、Android フレームワーク内の実際の
アプリケーションへの参照をすべて変更します。
結果
パフォーマンス
一般に、bazel mobile-install を使用すると、小さな変更後の大規模なアプリのビルド
とインストールが 4 ~ 10 倍高速化されます。
次の数値は、いくつかの Google プロダクトで計算されたものです。
これもちろん、変更の内容によって異なります。ベース ライブラリを変更した後の再コンパイルには時間がかかります。
制限事項
スタブ アプリケーションのトリックは、すべてのケースで機能するわけではありません。 次のようなケースでは、期待どおりに動作しません。
ContentProvider#onCreate()でContextがApplicationクラスにキャストされる場合。このメソッドは、アプリケーション 起動時に、Applicationクラスのインスタンスを置き換える前に呼び出されます。そのため、ContentProviderは実際のアプリケーションではなく、スタブ アプリケーション を参照します。このようにContextをダウンキャストすることは想定されていないため、これはバグではありませんが、Google の一部のアプリで発生しているようです。bazel mobile-installによってインストールされたリソースは、アプリ内からのみ使用できます。PackageManager#getApplicationResources()を介して他のアプリがリソースにアクセスする場合、これらのリソースは最後の増分以外のインストールからのものになります。ART を実行していないデバイス。スタブ アプリケーションは Froyo 以降で正常に動作しますが、Dalvik には、Java アノテーションが特定の方法で使用されている場合など、コードが複数の .dex ファイルに分散されている場合に、アプリが 正しくないと判断するバグが あります。アプリがこれらのバグをトリガーしない限り、Dalvik でも動作するはずです (ただし、古い Android バージョンのサポートは重視していません) 。