Edited at

Androidアプリの開発を爆速化させるInstant Runを今日から使おう

More than 3 years have passed since last update.


TL;DR

Android Studio 2.0 Preview で登場した Instant Run はとにかくアプリ開発を 5 倍ぐらい高速化できるので可及的速やかに導入すべき (20秒でわかる動画 :eyes:)


Instant Run とは

Instant Run は、 Android アプリ開発中に動いているアプリを インストールし直すことなく 実行中に動的に修正することができる仕組みです。エミュレータだけでなく実機でも動きます。 Java だと Hot Code Replace とか、 Visual Studio だと Edit and Continue とか呼ばれるやつです。

今までモバイルアプリ開発で公式にこれができるプラットフォームはなかったと思うので結構革新的。 (非公式だと JRebel for Android とかありました)

2015/11/22 (現地時間) の Android Dev Summit 2015 で Android Studio 2.0 の新機能の一つとして発表されました。キーノートでの紹介動画はこちらから:

Android Dev Summitキーノートでの紹介

お急ぎの方はデモをこの辺から見るといい感じです。


必要なもの


  • Android Studio 2.0 以上

  • Android Gradle Plugin 2.0 以上

  • 実行ターゲットは ICS (android-14) 以上のエミュレータおよび実機


現状できること


  • インスタンスメソッドの実装を置き換える

  • スタティックメソッドの実装を置き換える

  • クラスの追加や削除

  • Stringリソースの追加、削除、変更 (アクティビティの再起動が必要)

そのほかできることできないことの詳細は、 公式の Instant Run ドキュメント で読めます。


使ってみる

導入自体は簡単で、 Android Studio を 2.0.0 以上に上げて、プロジェクトの buildToolsVersion を 23 以上にすれば完了です。以下はその手順説明です。


準備編


1. Android Studio 2.0.0 をダウンロード & インストール

Android Studio の Canary channel を使ってる場合はアップデートのダイアログが出ます。

でも普段の開発で何が起きても文句言えない環境を使い続けるなんてドMなことはしないと思うので、普通はAndroid Tools Project Site からダウンロードしてインストールして、いつも通りセットアップしましょう。既存の開発環境と共存できるはずです。


新規プロジェクトで試してみる編

Android Studio 2.0 で新規プロジェクトを作ると自動的に Instant Run が有効になります。


1. 新規プロジェクトを作成

適当に新しいプロジェクトを作成します。今回は Minimum SDK Version は 15 で作りましたが、特に問題なく動作しました。テンプレートは Blank Activity を選びました。


2. ビルド&実行

いつも通りのビルドですが、 Gradle のタスクが増えています。

Screen Shot 2015-11-24 at 9.09.42 AM.png


  • incrementalDebugBuildInfoGenerator

Screen Shot 2015-11-24 at 9.10.18 AM.png


  • generateDebugInstantRunAppInfo

  • transformClassesWithInstantRunVerifierForDebug

  • transformClassesWithInstantRunForDebug

あとそれらしいログがぞろぞろと出ます。

実行してみると、いつもの

Screenshot_20151124-091210.png Screenshot_20151124-091217.png

こんな感じで、 FloatingActionButton を押すと Snackbar が出るやつです。

起動時に見慣れないログが出ます。おそらく ClassLoader が独自のものに置き換えられて adb 越しに Socket で dex をやりとりしていそう?

Screen Shot 2015-11-24 at 9.10.34 AM.png


3. メソッドの実装を書き換えてみる (hot swap)

では早速書き換えてみましょう。 "Replace with your own action" と言ってる部分を書き換えてみます。


before

fab.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});

これを日本語に書き換えて、ついでにアクションも作ってしまいます。


after

fab.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View view) {
Snackbar.make(view, "こんにちはこんにちは!!", Snackbar.LENGTH_LONG)
.setAction("はい!!", null).show();
}
});

早速実機 (Nexus 6P) に反映させてみます。ツールバーを見てみるといつもの Run ボタンに速そうな ⚡️ マークが付いています。 Ctrl-R でも実行できます。

Screen Shot 2015-11-24 at 9.15.44 AM.png

実行されるタスクは assembleDebug ではなく incrementalDebugSupportDex でした。

Screen Shot 2015-11-24 at 9.22.15 AM.png

ビルド 2 秒未満。

Screen Shot 2015-11-24 at 9.21.49 AM.png

実機側でのコード適用約 0.2 秒。

Screenshot_20151124-093420.png

実機側には Toast が表示され、 Activity の再起動なしでコード変更が適用されたと言ってます。実際 FAB を押すと、

Screenshot_20151124-091858.png

実質2秒で変更が反映されてしまいました。これは捗るのでは…!


4. Stringリソースを書き換えてみる (warm swap)

次に String リソースを書き換えてみましょう。


before

<string name="action_settings">Settings</string>


これを


after

<string name="action_settings">設定だよ</string>


に書き換えて、同じように実行してみます。

同じように Ctrl-R すると、 incrementalDebugSupportDex が実行されて差分ビルドが行われ、コード変更が適用されます。その後、こんな Toast が出ます。

Screen Shot 2015-11-24 at 9.16.07 AM.png

リソースの適用は Activity の再起動が必要なのですが、再起動は Ctrl-Shift-R でできるようです。

Screen Shot 2015-11-24 at 9.23.51 AM.png

で、実行するとこんな感じ。

Screenshot_20151124-105543.png

ちなみにこの Activity 再起動を伴うコード置換は warm swap と呼ばれており、行われているのは画面回転時のような処理なので、再起動してもライフサイクルイベントをきちんとハンドルしている Activity であれば瞬時に元通りです。(詳細は最後に追記)


既存プロジェクトに導入してみる編

新規プロジェクトで動きは分かったので既存プロジェクトに導入してみましょう。やることは buildToolsVersion を最新にするだけです。


1. 設定画面でプロジェクトをアップデートする

Preferences を開いて、 Instant Run を開きます。

Screen Shot 2015-11-24 at 9.53.15 AM.png

Update Project を押すと、 build.gradle が更新されます。主には


build.gradle

classpath 'com.android.tools.build:gradle:2.0.0-alpha1'



build.gradle

buildToolsVersion "23.0.0"



gradle.properties

distributionUrl=https\://services.gradle.org/distributions/gradle-2.8-all.zip


という具合に更新されるんですが、複数プロジェクトの buildToolsVersion は値に何が設定されていても単純に文字列置換してるような節があるので、ルートプロジェクトで一括指定している場合などはそっちは変更されずに現在のプロジェクトだけ置き換えられてしまうので注意です。

あと現時点で既に build tools は "23.0.2" が最新で警告されるので大人しく従うのが良いと思います。

Screen Shot 2015-11-24 at 11.08.30 AM.png

これだけで Instant Run が有効になって使えるようになります。すごくシンプルなプロジェクトでしか試してないので大きなプロジェクトの場合は別の課題があるかも知れません。


2. エラーで失敗してしまう場合

いざ!と思って Sync project するとこんなエラーで躓くかもしれません。自分は躓きました。

Error:Cause: com.android.sdklib.repository.FullRevision

原因は Jake Wharton の SDK Manager Plugin です。これを書いてる時点で既に Pull Request が Merge されており、すぐにリリースされると思います。 2.0.0-alpha1 リリース数時間以内の出来事で爆速すぎてシビれます。今すぐ試したい場合は、 jitpack.io を使って master を dependency に突っ込むか、 Android Studio で試す限りは必要ないので一時的に取り除くのも手です。


既知・未知の罠など

まだ Preview なこともあり、地味な罠がいくつかあります。


匿名クラスの定義を追加すると死ぬ

最初のサンプルで調子にのって、 FAB の onClickListener を定義してみます。

fab.setOnClickListener(new View.OnClickListener() {

@Override
public void onClick(View view) {
Snackbar.make(view, "こんにちはこんにちは!!", Snackbar.LENGTH_LONG)
.setAction("はい!!", new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "やったね!", Toast.LENGTH_SHORT).show();
}
});
}
}

いざ変更を適用して、 FAB をクリックしてみると死にます。

FATAL EXCEPTION: main

Process: sh.nothing.instantrun, PID: 27524
java.lang.NoClassDefFoundError: Failed resolution of: Lsh/nothing/instantrun/MainActivity$1$1;
at sh.nothing.instantrun.MainActivity$1$override.onClick(MainActivity.java:27)
at sh.nothing.instantrun.MainActivity$1$override.access$dispatch(MainActivity.java)
at sh.nothing.instantrun.MainActivity$1.onClick(MainActivity.java:0)
at android.view.View.performClick(View.java:5198)
at android.view.View$PerformClick.run(View.java:21147)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5417)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
Caused by: java.lang.ClassNotFoundException: Didn't find class "sh.nothing.instantrun.MainActivity$1$1" on path: DexPathList[[dex file "/data/data/sh.nothing.instantrun/files/studio-fd/dex-temp/classes0x0000.dex"],nativeLibraryDirectories=[/data/data/sh.nothing.instantrun/files/studio-fd/lib, /vendor/lib64, /system/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
at java.lang.ClassLoader.loadClass(ClassLoader.java:469)
at sh.nothing.instantrun.MainActivity$1$override.onClick(MainActivity.java:27) 
at sh.nothing.instantrun.MainActivity$1$override.access$dispatch(MainActivity.java) 
at sh.nothing.instantrun.MainActivity$1.onClick(MainActivity.java:0) 
at android.view.View.performClick(View.java:5198) 
at android.view.View$PerformClick.run(View.java:21147) 
at android.os.Handler.handleCallback(Handler.java:739) 
at android.os.Handler.dispatchMessage(Handler.java:95) 
at android.os.Looper.loop(Looper.java:148) 
at android.app.ActivityThread.main(ActivityThread.java:5417) 
at java.lang.reflect.Method.invoke(Native Method) 
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:726) 
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616) 
Suppressed: java.lang.ClassNotFoundException: Didn't find class "sh.nothing.instantrun.MainActivity$1$1" on path: DexPathList[[zip file "/data/app/sh.nothing.instantrun-1/base.apk"],nativeLibraryDirectories=[/data/app/sh.nothing.instantrun-1/lib/arm64, /vendor/lib64, /system/lib64]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
at java.lang.ClassLoader.loadClass(ClassLoader.java:511)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
... 13 more
Suppressed: java.lang.ClassNotFoundException: sh.nothing.instantrun.MainActivity$1$1
at java.lang.Class.classForName(Native Method)
at java.lang.BootClassLoader.findClass(ClassLoader.java:781)
at java.lang.BootClassLoader.loadClass(ClassLoader.java:841)
at java.lang.ClassLoader.loadClass(ClassLoader.java:504)
... 14 more
Caused by: java.lang.NoClassDefFoundError: Class not found using the boot class loader; no stack trace available

この辺はまだ対応していないみたいです。


strings.xml を編集して Instant Run にならないパターン

最初、動作を試そうとして app_name を変更してたんですが、何回やってもフルビルドになってしまう状況にハマりました。

そんなとき Android Studio の右上をよく見ると警告のバルーンが出ています。

Screen Shot 2015-11-24 at 9.42.11 AM.png

AndroidManifest.xml や、内部で参照されるリソースに変更を加えると常にフルビルドになってしまいます。 5 分ぐらい気づきませんでした。


導入すると7倍重くなる事例 (クリーンビルドが遅い)

Screen Shot 2015-11-24 at 12.29.01 PM.png

クリーンビルド時は transformClassesWithDexForDebug で時間がかかるようです。大きいプロジェクトだとまだ時期尚早かも……

ただ一回ビルドしてインストールできると、即座に反映されるようになります。


時々 Instant Run してくれなくなる

なにやっても毎回フルインストール (assembleDebug) になってしまうときがあります。 Android Dev Summit のライブデモで前半失敗してたのもこの状況だと思います。このときは Android Studio を立ち上げ直したら直ったりします。ライブデモではアプリを一度アンインストールしてました。もっといい方法があるかもしれない。


既知の問題

We Want Your Feedback! より雑に翻訳。



  • minSdkVersion < 21 で multi-dex を使っている場合、メソッド数が 65K 上限に近いメイン dex ファイルはビルドできません。メイン dex で必要なクラス数を減らしてメソッド数に余裕を作るようにアプリの修正が必要かもしれません。

  • Instant Run はたくさんの端末でテストしていますが、すべての端末でテストすることは不可能です。使っている端末で Instant Run が動かなかった場合は教えてください。

  • ときどき IDE とアプリのコネクションが切れてしまい、フルビルドが走る場合があることを確認しています。

  • サードパーティの Gradle プラグイン、特に新しい transforms API を使うようにアップデートされていないプラグインではまだテストできていません。もし問題が発生した場合はお知らせください。

  • Data Binding は今回のビルドでは動きません。これは早いうちに直します。

結構大事なのがさらっと最後に書かれていますね!!


まとめ

ごく小さな Blank Activity プロジェクトで、1行コード修正した場合だとこんな感じ。


  • 従来: ビルドに約 3.8 秒、インストールに約 6 秒 = 約 10 秒

  • 現在: 差分ビルドに約 1.8 秒、反映に約 0.2 秒 = 約 2 秒

5倍速い!

実際の作業ではこれに加え アプリが起動してから目的のアクティビティまで遷移する という時間もゼロになるので、さらに差は広がります。

現時点だとクリーンビルドが遅いので、プロジェクトのサイズによって導入が難しかったりするかも知れませんが、自分の環境においては、ちょっと大きめのプロジェクト (3 プロジェクト .java ファイル 230 個ぐらい) でも差分のビルド速度は変わりませんでした。 (むしろ 1.5秒 とか出て速くなったりした)

デバッグのイテレーションが短くなることは非常に大事です。だって修正が 2 秒で確認できるっていったら、下手すると Web 開発より速い。本命がついに(やっと)出たー!という感動を分かち合いたい!と思ってこの記事を書きました。デスクトップアプリケーション開発だと当たり前にできてたことが、ようやくモバイルアプリ開発にもやってきて大歓喜。一度使い始めたら、もうこれがなかった頃が信じられなくなります。

まだできないこともありますが、フィードバックくれ!!って言ってるので積極的にフィードバックしてよくしていきたいですね。


追記


Run セッション

従来と違って Instant Run 実行中は Android Studio 側で Run セッションがアクティブなままになります。 Stop を押すとアプリの実行が止まります。 ( System.exit() が呼ばれるっぽい ) 実行中にプロセスとのコネクションが切れると Instant Run セッションは終了し、その後は再び通常のビルド & インストールが行われます。

この副作用で、 Instant Run 実行中に Instant Run にならない Run セッションを開始すると実行中のアプリが突然終了します。例えば ApplicationManifest.xml の変更後の Run はこれに当たります。以前はインストール開始までは動いていたので、それまでに少し余裕がありましたが、本バージョンからは Ctrl-R はいきなり既存プロセスが死ぬことがあるので心の準備が必要です。


Activity の再起動の挙動について

Ctrl-Shift-R で Activity が再起動します。流れとしては一般的なアクティビティ再生成のライフサイクルイベント通り onPause onStop onDestroy が呼ばれて、 onCreatesavedInstanceState が渡ってくる感じです。デモ動画でも「画面回転時と似たようなもの」と言っていて、一見 configuration 変更のような動作ですが onConfigurationChanged は呼ばれないため別の何かです。

Activity 再起動を伴わないコード変更は、特に Activity 側で通知されるイベントはなさそうな感じ。


おまけ

Instant Runで手元の高速化は万全!次はチームのアプリ開発環境を改善しよう!

開発を捗らせる Tips を集めた(い) モバイルDevOps Advent Calendar 2015 やってるのでゆっくり購読していってね! 1日目書きました!!