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 アプリのビルドには、次のような問題があります。
デックス。デフォルトでは、「dx」はビルド内で正確に 1 回だけ呼び出されます。以前のビルドの作業を再利用する方法は認識されておらず、1 つのメソッドだけが変更された場合でも、すべてのメソッドが再度 dex されます。
デバイスにデータをアップロードする。adb は USB 2.0 接続の全帯域幅を使用しないため、大きなアプリのアップロードには長い時間がかかります。リソースや単一のメソッドなど、小さな部分のみが変更された場合でも、アプリ全体がアップロードされるため、大きなボトルネックになる可能性があります。
ネイティブ コードにコンパイルします。Android L では、新しい Android ランタイムである ART が導入されました。これは、Dalvik のようにアプリをジャスト イン コンパイルするのではなく、事前にコンパイルするものです。これにより、インストール時間が長くなり、アプリの動作速度が大幅に向上します。通常はアプリを 1 回インストールして何度も使用することになるため、これはユーザーにとってトレードオフになりますが、その結果、アプリが何度もインストールされ、各バージョンの実行回数は多くありません。
bazel mobile-install
のアプローチ
bazel mobile-install
は以下の点を改善しています。
シャーディングされた dex 変換。アプリの Java コードをビルドすると、Bazel はクラスファイルをほぼ同じサイズにシャーディングし、それぞれで
dx
を呼び出します。前回のビルドから変更されていないシャードでは、dx
が呼び出されません。増分ファイル転送。Android リソース、.dex ファイル、ネイティブ ライブラリはメインの .apk から削除され、別のモバイル インストール ディレクトリに保存されます。これにより、アプリ全体を再インストールすることなく、コードと Android リソースを個別に更新できます。したがって、ファイルの転送にかかる時間が短くなり、変更された .dex ファイルのみがデバイス上で再コンパイルされます。
.apk の外部からアプリの一部を読み込むデバイス上のスタブ インストール ディレクトリから Android リソース、Java コード、ネイティブ コードを読み込む小さなスタブ アプリが .apk に入力され、実際のアプリに制御が転送されます。以下で説明するいくつかの特殊なケースを除き、アプリはすべて透過的です。
シャーディングされたデックス
シャーディングされた dex はかなり簡単です。.jar ファイルがビルドされると、ほぼ同じサイズの別の .jar ファイルにシャーディングされ、前回のビルド以降に変更されたファイルでは dx
が呼び出されます。dex するシャードを判断するロジックは、Android に固有のものではなく、Bazel の一般的な変更プルーニング アルゴリズムを使用するだけです。
シャーディング アルゴリズムの最初のバージョンは、.class ファイルをアルファベット順で単純に並べ、次にリストを同じサイズの部分に切り取りましたが、これは最適でないことが判明しました。クラスを追加または削除すると(ネストされたクラスや匿名のクラスでも)、アルファベット順に 1 つ増え、これらのシャードが再度 dex されます。そこで、個々のクラスではなく Java パッケージをシャーディングすることを決定しました。もちろん、これにより新しいパッケージが追加または削除されても、多くのシャードが dex 変換されますが、単一のクラスを追加または削除するよりも、はるかに頻度が低くなります。
シャードの数は、BUILD ファイルによって制御されます(android_binary.dex_shards
属性を使用)。Bazel は理想的なシャードの数を自動的に検出しますが、Bazel は現在、一連のアクション(ビルド時に実行されるコマンドなど)を実行する前にそのシャードを認識する必要があります。したがって、シャードの最適な数を判別することはできません。最終的には、シャードが多ければ多いほど、試行が高速になるほど、試行時間が高速になります。スイート スポットは通常 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
ビットを設定する必要があるため、これが必要となります。これは、非 root の adb
がアクセスできる場所には設定できないためです。
これらすべてが完了すると、スタブアプリは実際の Application
クラスをインスタンス化し、スタブアプリはそれ自体へのすべての参照を Android フレームワーク内の実際のアプリに変更します。
結果
パフォーマンス
一般的に、小さな変更で大規模なアプリの構築とインストールは、4 ~ 10 倍高速になります。bazel mobile-install
以下の数値は、いくつかの Google プロダクトで計算されました。
もちろんこれは、変更の性質によって異なります。ベース ライブラリの変更後に再コンパイルするには時間がかかります。
制限事項
スタブ アプリが行うトリックは、常に機能するとは限りません。想定どおりに動作しない場合を以下に示します。
Context
がContentProvider#onCreate()
のApplication
クラスにキャストされたとき。このメソッドは、アプリの起動時にApplication
クラスのインスタンスを置き換える前に呼び出されます。したがって、ContentProvider
は引き続き実際のスタブではなくスタブ アプリケーションを参照します。おそらく、これはContext
をダウンキャストする想定ではないため、これはバグではありませんが、Google の一部のアプリではこの現象が発生するようです。bazel mobile-install
によってインストールされたリソースは、アプリ内からのみ使用できます。PackageManager#getApplicationResources()
を介して他のアプリがリソースにアクセスする場合、これらのリソースは最後の非増分インストールから取得されます。ART を実行していないデバイス。スタブ アプリケーションは Froyo 以降で正常に動作しますが、特定のケース(Java アノテーションが特定の方法)で使用された場合など、特定の状況でコードが配布されると、アプリが正しくないと判断されるバグが Dalvik に存在します。アプリがこれらのバグを修正していない限り、Dalvik にも対応する必要があります(ただし、古い Android バージョンのサポートは Google の焦点ではありません)。