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
のコマンドラインに追加の引数を追加できます。この機能の便利な用途の 1 つは、ワークステーションに複数のデバイスが接続されている場合に、インストール先のデバイスを選択することです。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 アプリのビルドには、次のような問題があります。
Dexing。デフォルトでは、ビルドで「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 に配置され、デバイス上のモバイル インストール ディレクトリから Android リソース、Java コード、ネイティブ コードを読み込み、実際のアプリに制御を移します。これは、以下で説明するいくつかの特殊なケースを除き、アプリに対して透過的です。
シャーディングされた Dex 変換
シャード化された dexing は比較的簡単です。.jar ファイルがビルドされると、ツールがそれらをほぼ同じサイズの個別の .jar ファイルにシャード化し、前回のビルド以降に変更されたものに対して dx
を呼び出します。どのシャードを dex するかを決定するロジックは Android 固有のものではなく、Bazel の一般的な変更プルーニング アルゴリズムを使用するだけです。
最初のバージョンのシャーディング アルゴリズムでは、.class ファイルをアルファベット順に並べ替え、リストを同じサイズのパーツに分割していましたが、これは最適ではありませんでした。クラスが追加または削除されると(ネストされたクラスや匿名クラスでも)、そのクラスの後に続くすべてのクラスが 1 つずつシフトし、その結果、これらのシャードが再度 dex 化されることになります。そのため、個々のクラスではなく Java パッケージをシャーディングすることにしました。もちろん、新しいパッケージが追加または削除された場合は、多くのシャードのインデックス登録が行われますが、これは単一のクラスの追加または削除よりもはるかに頻度が低くなります。
シャードの数は、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 に含める必要がなくなり、クラスの変更時に完全な再インストールが必要になることを防ぐことができます。
これは、AndroidManifest.xml
で指定された Application
クラスをスタブ アプリケーションに置き換えることで実現されます。これは、アプリの起動時に制御を取得し、Android フレームワークの内部で Java リフレクションを使用して、クラスローダとリソース マネージャーを適切なタイミング(コンストラクタ)で調整します。
スタブ アプリケーションは、mobile-install によってインストールされたネイティブ ライブラリを別の場所にコピーする処理も行います。これは、動的リンカーがファイルに X
ビットを設定する必要があるためです。これは、root 以外の 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 バージョンのサポートは、必ずしも私たちの重点事項ではありません)。