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 秒以内に実行されるのと、変更が期待どおりの結果になるかどうかに関するフィードバックを得るまでに数分から数時間待たなければならないのとでは、違いが生じます。
残念ながら、.apk をビルドする従来の Android ツールチェーンでは、多くのモノリシックな手順が必要であり、そのすべてを行って Android アプリをビルドする必要があります。Google では、Google マップのような大規模なプロジェクトでは、1 行の変更の作成に 5 分待つのは珍しいことではありません。
bazel mobile-install
では、変更プルーニング、ワーク シャーディング、Android 内部の巧妙な操作を組み合わせて使用することで、アプリのコードを変更することなく、Android の反復型開発を高速化します。
従来のアプリのインストールに関する問題
Android アプリのビルドには、次のような問題があります。
デックス。デフォルトでは、「dx」はビルドで 1 回だけ呼び出され、以前のビルドの作業を再利用する方法を認識しません。1 つのメソッドだけが変更された場合でも、すべてのメソッドを再度 dex を実行します。
デバイスにデータをアップロードする。adb は USB 2.0 接続の全帯域幅を使用するわけではないため、大規模なアプリではアップロードに時間がかかることがあります。リソースや 1 つのメソッドなどの小さな部分が変更された場合でも、アプリ全体がアップロードされるため、これが大きなボトルネックになる可能性があります。
ネイティブ コードへのコンパイル。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 変換が行われますが、1 つのクラスを追加または削除するより頻度は低くなります。
シャードの数は、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 リフレクションを使用して、早い段階でクラスローダーとリソース マネージャー(そのコンストラクタ)を適切に微調整します。
また、スタブアプリケーションは、モバイル インストールによってインストールされたネイティブ ライブラリを別の場所にコピーすることもできます。ダイナミック リンカーは、ファイルに対して 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 以降で適切に動作しますが、Java アノテーションが特定の方法で使用される場合など、特定のケースでコードが複数の .dex ファイルに分散されると、Dalvik にはアプリが正しくないと判断するバグがあります。アプリがこれらのバグを修正しない限り、Dalvik でも動作します(ただし、古い Android バージョンのサポートは Google の焦点ではありません)。