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
が自動的にアプリを起動します
ご不明な点がございましたら、例をご覧になるか、お問い合わせください。
はじめに
デベロッパー ツールチェーンの最も重要な属性の 1 つはスピードです。コードを変更してから 1 秒以内に実行される場合と、変更が期待どおりに動作するかに関するフィードバックを得るまでに数分、場合によっては数時間待つ場合とでは違いが生じます。
残念ながら、.apk をビルドするための従来の Android ツールチェーンでは、多くのモノリシックの連続したステップが必要であり、そのすべてを Android アプリをビルドするために行う必要があります。Google では、Google マップのような大規模なプロジェクトでは、1 行の変更をビルドするのに 5 分待つことは珍しくありませんでした。
bazel mobile-install
は、変更のプルーニング、処理のシャーディング、Android 内部の巧妙な操作を組み合わせて使用することで、アプリのコードを変更することなく、Android の反復開発をはるかに速くします。
従来のアプリのインストールに関する問題
Android アプリのビルドには、次のような問題があります。
Dexing。デフォルトでは、"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 リソース、ネイティブ ライブラリをモバイル インストール ディレクトリにアップロードする
最初のステップでは、アプリがインストールされているかどうかに関係なく、インクリメンタリティはそれほどありません。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 の焦点ではありません)。