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 アプリのビルドには、次のような問題があります。
dex 変換。デフォルトでは、「dx」はビルドで 1 回だけ呼び出され、以前のビルドの作業を再利用する方法を認識していません。1 つのメソッドのみが変更された場合でも、すべてのメソッドを再度デックスします。
デバイスへのデータのアップロード。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 パッケージをシャーディングすることにしました。もちろん、新しいパッケージが追加または削除されると、多くのシャードがインデックスに登録されますが、これは単一クラスの追加または削除よりも頻度がはるかに低くなります。
シャードの数は、BUILD ファイル(android_binary.dex_shards
属性を使用)で制御されます。理想的には、Bazel が最適なシャードの数を自動的に決定しますが、現時点では、Bazel はアクションを実行する前にアクションのセット(ビルド中に実行されるコマンドなど)を把握する必要があります。そのため、最終的にアプリに含まれる Java クラスの数を把握できないため、最適なシャードの数を決定できません。一般的に、シャードの数が多いほど、ビルドとインストールは速くなりますが、動的リンカーがより多くの処理を行う必要があるため、アプリの起動は遅くなります。通常、最適なシャード数は 10 ~ 50 です。
増分ファイル転送
アプリをビルドしたら、次はインストールします。できるだけ手間をかけずにインストールしましょう。インストール手順は次のとおりです。
- .apk のインストール(通常は
adb install
を使用) - .dex ファイル、Android リソース、ネイティブ ライブラリを mobile-install ディレクトリにアップロードする
最初のステップでは、アプリがインストールされているかどうかしかわかりません。Bazel は現在、このステップを実行する必要があるかどうかをすべてのケースで判断できないため、ユーザーが --incremental
コマンドライン オプションでこのステップを実行するかどうかを指定する必要があります。
第 2 ステップでは、ビルドに含まれるアプリのファイルが、デバイス上のマニフェスト ファイルと比較されます。このファイルには、デバイス上にあるアプリファイルとそのチェックサムがリストされます。新しいファイルはデバイスにアップロードされ、変更されたファイルは更新され、削除されたファイルはデバイスから削除されます。マニフェストが存在しない場合、すべてのファイルをアップロードする必要があるものと見なされます。
デバイス上のファイルを変更することで増分インストール アルゴリズムを欺くことは可能ですが、マニフェスト内のチェックサムは欺くことはできません。これは、デバイス上のファイルのチェックサムを計算することで防ぐことができますが、インストール時間の増加に見合う価値がないと考えられました。
スタブ アプリケーション
スタブ アプリケーションは、デバイス上の mobile-install
ディレクトリからデックス、ネイティブ コード、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 サービスで計算されています。
これは、変更の性質によって異なります。ベース ライブラリを変更した後の再コンパイルは時間がかかります。
制限事項
スタブ アプリケーションが行うトリックは、すべてのケースで機能するとは限りません。次のようなケースでは、想定どおりに機能しません。
Context
がContentProvider#onCreate()
のApplication
クラスにキャストされる場合。このメソッドは、Application
クラスのインスタンスを置き換える前に、アプリの起動時に呼び出されます。そのため、ContentProvider
は実際のアプリではなく、スタブ アプリを参照します。このようなContext
をダウンキャストすることは想定されていないため、おそらくバグではありませんが、Google のいくつかのアプリでこの問題が発生しているようです。bazel mobile-install
によってインストールされたリソースは、アプリ内からのみ使用できます。他のアプリがPackageManager#getApplicationResources()
を介してリソースにアクセスする場合、これらのリソースは最後の増分インストールから取得されます。ART が実行されていないデバイス。スタブ アプリケーションは Froyo 以降で正常に動作しますが、Dalvik にはバグがあり、Java アノテーションが特定の方法で使用されている場合など、特定のケースでコードが複数の .dex ファイルに分散されている場合、アプリが正しくないと判断されます。アプリがこれらのバグをトリガーしない限り、Dalvik でも動作するはずです(ただし、古いバージョンの Android のサポートは Google の重点ではありません)。