2016.05.13

Instant Run におけるActiveAndroid のエラー原因とその対応策

Pocket

こんにちは、次世代システム研究室T.M です。


先日、Android Studio 2.0 のstable が発表されました。昨年末にpreview が発表された時、Instant Run が話題になり、私もとりあえずInstant Run を体験したくて、インストールし利用してみたところ、アプリの実行に時間がかからないことに感動しました。しかし、業務で開発する上でpreview というのは手を出しにくく、この数ヶ月の間、1 回のビルドに3分ほどかかるアプリの開発に耐えてきました。ついに、stable が出たということで、早速2.0 をインストールし、いままでは3 分ほどかかったアプリのビルドを行いました。

Instant Run の実行

1回目の実行では、アプリをビルドする必要があり、いつも通り、つまり3 分程度ビルドに時間がかかりました。時間がかかるのはしかたがないのですが、ここで一つ問題が発生しました。それは、ビルドが終わり、アプリが立ち上がったところで、クラッシュしました。下記のエラーログが吐き出され、アプリに利用しているDB のライブラリである、ActiveAndroid 周りでエラーが起きているのが確認できました。
Process: xxx.xxx.xxx.debug, PID: 10812
java.lang.RuntimeException: Unable to start activity ComponentInfo{xxx.xxx.xxx.debug/xxx.xxx.xxx.xxx.xxx.XXXActivity}: java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String com.activeandroid.TableInfo.getTableName()' on a null object reference
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2416)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)
  at android.app.ActivityThread.-wrap11(ActivityThread.java)
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)
  at android.os.Handler.dispatchMessage(Handler.java:102)
  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.NullPointerException: Attempt to invoke virtual method 'java.lang.String com.activeandroid.TableInfo.getTableName()' on a null object reference
  at com.activeandroid.Cache.getTableName(Cache.java:156)
  at com.activeandroid.query.From.addFrom(From.java:169)
  at com.activeandroid.query.From.toSql(From.java:250)
  at com.activeandroid.query.From.executeSingle(From.java:311)
  at xxx.xxx.xxx.xxx.xxx.xxx.xxx(Xxx.java:xx)
  at xxx.xxx.xxx.xxx.xxx.xxx.xxx(Xxx.java:xx)
  at xxx.xxx.xxx.xxx.xxx.xxx.xxx(Xxx.java:xx)
  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2369)
  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2476)?
  at android.app.ActivityThread.-wrap11(ActivityThread.java)?
  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1344)?
  at android.os.Handler.dispatchMessage(Handler.java:102)?
  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)?

エラー調査

Android Studio 1.5 では、正常にビルドでき、アプリを実行することができます。そこで、エラーを調査するため、Android Studio 1.5と2.0 の両方でデバッグし、変数の値が異なる部分を探しました。そうして見つけたのが、ModelInfo.java のscanForModel というメソッドです。下図の上が1.5、下が2.0 の変数表示です。


ActiveAndroid ではModel クラスを継承しているクラスをテーブルとするため、dex ファイルにある全てのクラスを検査して、テーブルとしています。しかし、該当の箇所でのフィールドの値を見ると、1.5 ではすべてのクラスが入っており、配列の大きさが7,000 弱となっている一方、2.0 では、31 個のクラスしか入っておらず、しかもテーブルと関係のないクラスしかありませんでした。そのため、テーブルを定義することができず、テーブル名を取得する部分でnull になってしまったようです。つまり、1.5 と2.0 でdex ファイルの中身が異なるため、エラーが発生したものと考えられます。

そこで、dex ファイルを確認するため、Android Studio 2.0 でビルドしたapk を展開したところ、以下の様なファイルがありました。


ちなみに、今までは以下の様なファイルでした。


instant-run.zip というファイルが増えており、classes.dex のファイルサイズが小さくなっていました。instant-run.zip を展開すると中には、多数のdex ファイルがありました。名前から明らかにInstant Run のためのものであり、Instant Run のためにdex ファイルの構造が変わっており、エラーが発生したと推測できます。そこで、Android Studio 2.0 でInstant Run をオフにしたところ、正常にビルドでき、アプリを実行することができました。このことから、Instant Run のせいでアプリが実行できないことが確認できました。

従来のビルドでは、単一のdex ファイルにまとめるため、ソースコードの小さな変更であったとしても、すべてをビルドし直していましたが、Instant Run ではdex ファイルを複数に分割することで、変更のあった箇所のみの最低限のビルドを行い、掛かる時間を短縮しています。そのため、dex ファイルの構造が変わり、dex ファイルを直接見ているActiveAnroid でエラーが発生しました。

対応策

Instant Run を使わなければ、正常にアプリを実行することができますが、開発においてInstant Run がなければ、とても非効率であり、使わないという選択はできれば取りたくありません。むしろ、ActiveAndroid を使わないという方が良いように思います。最近のAndroid のORM のライブラリはActiveAndroid より優れていると言われているものが多々あり、わざわざActiveAndroid を利用する必要がないのではないでしょうか。しかし、既存のアプリでActiveAndroid を利用していると、変更することが難しく、そのまま使い続けなければいけません。そのような場合、AndroidManifest でAA_DB_NAME やAA_DB_VERSION のようにAA_MODELS でModel を継承したクラスを記述することで、Instant Run でも正常にテーブルを定義することができ、数秒でビルドをすることができ、アプリを実行することができます。
<meta-data
    android:name="AA_MODELS"
    android:value="xxx.xxx.xxx.Xxx, xxx.xxx.xxx.Xxx, xxx.xxx.xxx.Xxx" />

さいごに

Instant Run のおかげで、今まで3 分ほどかかっていたビルドが数秒で終わるようになり、とても効率よく開発ができるようになりました。Instant Run によりdex ファイルの構造が変わりましたが、一般的な開発ではdex ファイルを触る機会がないかと思いますので、特に問題ないかと思います。ライブラリでもdex ファイルを操作しているものは多くはないと予想しますが、もしAndroid Studio 2.0 にアップグレードしてアプリが実行できなくなった場合、Instant Run を疑ったらよいかと思います。

参考

次世代システム研究室では、アプリケーション開発や設計を行うアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ 募集職種一覧 からご応募をお願いします。

皆さんのご応募をお待ちしています。