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 から削除され、別の mobile-install ディレクトリに保存されます。これにより、アプリ全体を再インストールせずに、コードと Android リソースを個別に更新できます。ファイルの転送にかかる時間が短縮され、変更された .dex ファイルのみがデバイス上で再コンパイルされます。
アプリの一部を .apk の外部から読み込む。小さなスタブ アプリケーションが .apk に配置され、デバイス上のモバイル インストール ディレクトリから Android リソース、Java コード、ネイティブ コードを読み込み、実際のアプリに制御を移します。これは、後述するいくつかの特殊なケースを除き、アプリに対して透過的に行われます。
シャーディングされた dex 変換
シャーディングされたデクストリングは比較的簡単です。.jar ファイルがビルドされると、ツールによってほぼ同じサイズの個別の .jar ファイルにシャーディングされ、前回のビルド以降に変更されたファイルに対して dx
が呼び出されます。どのシャードを Dex にするか決定するロジックは Android に固有のものではなく、Bazel の一般的な変更プルーニング アルゴリズムを使用します。
シャーディング アルゴリズムの最初のバージョンでは、.class ファイルをアルファベット順に並べ替え、リストを同じサイズの部分に分割していましたが、これは最適ではありませんでした。クラスが追加または削除されると(ネストされたクラスや匿名クラスでも)、そのクラスの後に続くすべてのクラスが 1 つずつずれ、シャードが再びデックス化されるためです。そのため、個々のクラスではなく 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 の重点ではありません)。