Android 向けの迅速なイテレーション開発
このページでは、bazel mobile-install
によって Android の反復型開発が大幅に高速化される仕組みについて説明します。このアプローチの利点と従来のアプリ インストール方法の課題を比較して説明します。
まとめ
Android アプリの小さな変更を非常にすばやくインストールする手順は次のとおりです。
- インストールするアプリの
android_binary
ルールを見つけます。 proguard_specs
属性を削除して、ProGuard を無効にします。multidex
属性をnative
に設定します。dex_shards
属性を10
に設定します。- (Dalvik ではなく)ART を実行しているデバイスを 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
が自動的にアプリを起動します
判断に迷う場合は、例を参照するか、こちらにお問い合わせください。
はじめに
デベロッパーのツールチェーンの最も重要な特性の 1 つはスピードです。コードを変更してから 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 から削除され、別のモバイル インストール ディレクトリに格納されます。これにより、アプリ全体を再インストールすることなく、コードと Android リソースを個別に更新できます。そのため、ファイルの転送時間が短縮され、変更された .dex ファイルのみがデバイス上で再コンパイルされます。
.apk の外部からアプリの一部を読み込む。小さなスタブアプリが .apk に挿入され、デバイス上のモバイル インストール ディレクトリから Android リソース、Java コード、ネイティブ コードが読み込まれた後、実際のアプリに制御が転送されます。ただし、以下で説明するような特殊なケースを除き、すべてアプリに対して透過的です。
シャーディングされた dex 変換
シャーディングされた dex 変換は比較的簡単です。.jar ファイルがビルドされると、ツールによってそれらをほぼ同じサイズの個別の .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 ステップでは、ビルドに含まれるアプリのファイルが、デバイス上のマニフェスト ファイルと比較されます。このファイルには、デバイス上にあるアプリファイルとそのチェックサムがリストされます。新しいファイルはすべてデバイスにアップロードされ、変更されたファイルはすべて更新され、削除されたファイルはすべてデバイスから削除されます。マニフェストが存在しない場合は、すべてのファイルをアップロードする必要があると見なされます。
増分インストールのアルゴリズムは、デバイス上のファイルを変更することで偽る可能性はありますが、マニフェスト内でそのチェックサムを偽ることはできません。これは、デバイス上のファイルのチェックサムを計算することで保護できたかもしれませんが、インストール時間の増加に見合わないと判断されました。
Stub アプリケーション
スタブアプリでは、dex、ネイティブ コード、Android リソースをデバイス上の mobile-install
ディレクトリから読み込むことができます。
実際の読み込みは、BaseDexClassLoader
をサブクラス化して実装します。実装方法は詳細に説明されています。これは、アプリのクラスが読み込まれる前に行われるため、apk にあるアプリクラスはすべてデバイス上の mobile-install
ディレクトリに配置できるため、adb install
なしで更新できます。
この処理はアプリのいずれかのクラスを読み込む前に行う必要があります。そのため、.apk にアプリ クラスを含める必要はありません。つまり、それらのクラスを変更すると、完全な再インストールが必要となります。
これを行うには、AndroidManifest.xml
で指定された Application
クラスをスタブ アプリケーションに置き換えます。これにより、アプリの起動時に制御を行い、Android フレームワークの内部で Java リフレクションを使用して、できるだけ早くクラスローダーとリソース マネージャー(コンストラクタ)を適切に微調整します。
スタブアプリのもう 1 つの処理は、モバイル インストールによってインストールされたネイティブ ライブラリを別の場所にコピーすることです。これが必要なのは、ダイナミック リンカーがファイルに対して X
ビットを設定する必要があるためです。ただし、root 以外の adb
からアクセスできる場所では、設定できません。
これらすべての処理が完了すると、スタブアプリは実際の Application
クラスをインスタンス化し、それ自体へのすべての参照を Android フレームワーク内の実際のアプリに変更します。
結果
パフォーマンス
一般に、bazel mobile-install
を使用すると、小さな変更で大規模なアプリの作成とインストールが 4 ~ 10 倍高速化されます。
以下の数値は、いくつかの Google サービスで計算されています。
もちろん、これは変更の性質によって異なります。ベース ライブラリの変更後の再コンパイルには時間がかかります。
制限事項
スタブアプリの手法は、すべてのケースで機能するとは限りません。次のようなケースでは、想定どおりに機能しない場合があります。
Context
がContentProvider#onCreate()
のApplication
クラスにキャストされるとき。このメソッドは、アプリの起動時にApplication
クラスのインスタンスを置き換える前に呼び出されるため、ContentProvider
は実際のアプリではなくスタブアプリを引き続き参照します。このようなContext
をダウンキャストすることは想定されていないため、間違いなくバグではありませんが、Google の一部のアプリではこの問題が発生しているようです。bazel mobile-install
によってインストールされたリソースは、アプリ内からしか利用できません。他のアプリがPackageManager#getApplicationResources()
を介してリソースにアクセスした場合、これらのリソースは、最後の非増分インストールから取得されます。ART が実行されていないデバイス。スタブアプリは Froyo 以降でも正常に動作しますが、Java アノテーションが特定の方法で使用される場合など、特定のケースでコードが複数の .dex ファイルに分散されている場合、Dalvik にはアプリが正しくないと判断されるバグがあります。アプリでこれらのバグをくっつけない限り、Dalvik でも動作します(ただし、古い Android バージョンのサポートは厳密には重視しません)。