Android アプリは、未処理の例外またはシグナルが原因で予期しない終了が発生するとクラッシュします。Java で作成されたアプリは、Throwable
クラスによって表される未処理の例外をスローするとクラッシュします。マシンコードまたは C++ で作成されたアプリは、SIGSEGV
などの未処理のシグナルが実行時に存在するとクラッシュします。
アプリがクラッシュすると、Android によってアプリのプロセスが終了され、図 1 に示すように、アプリが停止したことをユーザーに知らせるダイアログが表示されます。
図 1. Android デバイスでのアプリのクラッシュ
アプリは、フォアグラウンドで実行されていなくてもクラッシュします。バックグラウンドで実行されるブロードキャスト レシーバやコンテンツ プロバイダを含め、あらゆるアプリ コンポーネントがアプリのクラッシュを引き起こす可能性があります。操作中ではないアプリがクラッシュすると、たいていのユーザーは困惑します。
アプリでクラッシュが発生する場合、このページのガイダンスが問題の診断と解決に有用です。
問題を検出する
ユーザーがアプリを使用しているときに、クラッシュが発生していることを常に把握できるとは限りません。すでにアプリを公開している場合は、Android Vitals を使用してアプリのクラッシュ発生率を確認できます。
Android Vitals
Android Vitals は、アプリのクラッシュ発生率をモニタリングして改善するのに役立ちます。Android Vitals では、以下のクラッシュ発生率を測定します。
- クラッシュ発生率: 1 日のアクティブ ユーザーのうち、なんらかのクラッシュが発生したユーザーの割合(%)です。
- ユーザーが認識したクラッシュ率: 1 日のアクティブ ユーザーのうち、アプリの使用中のクラッシュ(ユーザーが認識したクラッシュ)が 1 回以上発生したユーザーの割合(%)です。アクティビティを表示しているか、フォアグラウンド サービスを実行している場合、アプリは使用中とみなされます。
- 複数回クラッシュ発生率: 1 日のアクティブ ユーザーのうち、クラッシュが 2 回以上発生したユーザーの割合(%)です。
1 日のアクティブ ユーザーとは、1 日に 1 台のデバイスでアプリを使用するユニーク ユーザーのことで、複数のセッションでアプリを使用する場合もあります。ユーザーが 1 日に複数のデバイスでアプリを使用した場合、デバイスごとにその日のアクティブ ユーザーとしてカウントされます。複数のユーザーが同じデバイスを 1 日に使用した場合は、1 人のアクティブ ユーザーとしてカウントされます。
ユーザーが認識したクラッシュ発生率は主な指標のひとつであり、Google Play でのアプリの見つけやすさに影響します。この指標が重要な理由は、ここにカウントされるクラッシュはユーザーがアプリを使用中に発生するものであり、使用中断の原因となるためです。
Play では、この指標に 2 つの不正な動作のしきい値が定義されています。
- すべての不正な動作のしきい値: 1 日のアクティブ ユーザーのうち、すべてのデバイスモデルでユーザーが認識したクラッシュが発生したユーザーの割合が 1.09% 以上。
- デバイスごとの不正な動作のしきい値: 1 日のアクティブ ユーザーのうち、1 つのデバイスモデルでユーザーが認識したクラッシュが発生したユーザーの割合が 8% 以上。
アプリがすべての不正な動作のしきい値を超えると、そのアプリはすべてのデバイスで見つかりづらくなります。アプリが一部のデバイスでデバイスごとの不正な動作のしきい値を超えると、そのアプリはそれらのデバイスで見つかりづらくなり、ストアの掲載情報に警告が表示されることがあります。
Android Vitals では、アプリが頻繁にクラッシュする場合に Google Play Console を介してアラートを受け取ることができます。
Google Play が Android Vitals のデータを収集する方法については、Play Console のドキュメントをご覧ください。
クラッシュを診断する
アプリがクラッシュを報告していることがわかったら、そのクラッシュを診断します。クラッシュの解決は時として困難です。 しかし、クラッシュの根本原因を特定できれば、ほとんどの場合は解決策を見つけられます。
アプリのクラッシュが発生する状況はさまざまです。null 値または空の文字列の検出のように原因が明らかな場合もありますが、無効な引数が API に渡された、またはマルチスレッド化されたインタラクションが複雑すぎるなど、わかりにくい原因による場合もあります。
Android でクラッシュが発生すると、スタック トレースが生成されます。スタック トレースとは、クラッシュの時点までにプログラムで行われた関数呼び出しを順番にネストしたスナップショットです。クラッシュのスタック トレースは Android Vitals で確認できます。
スタック トレースの読み方
クラッシュを解決するには、まずクラッシュの発生場所を特定します。Play Console または logcat ツールの出力を使用している場合は、レポート詳細で参照可能なスタック トレースを使用できます。スタック トレースを参照できない場合は、アプリを手動でテストするか、クラッシュを経験しているユーザーに協力を依頼して、logcat の使用中にローカルでクラッシュを再現する必要があります。
次のトレースは、Java プログラミング言語を使用して記述されたアプリのクラッシュの例を示しています。
--------- beginning of crash
AndroidRuntime: FATAL EXCEPTION: main
Process: com.android.developer.crashsample, PID: 3686
java.lang.NullPointerException: crash sample
at com.android.developer.crashsample.MainActivity$1.onClick(MainActivity.java:27)
at android.view.View.performClick(View.java:6134)
at android.view.View$PerformClick.run(View.java:23965)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:156)
at android.app.ActivityThread.main(ActivityThread.java:6440)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:746)
--------- beginning of system
スタック トレースには、クラッシュのデバッグに不可欠な、次の 2 つの情報が表示されます。
- スローされた例外のタイプ。
- 例外がスローされたコードのセクション。
一般的に、スローされた例外のタイプは、何が問題かを知るための強力なヒントになります。例外が IOException
または OutOfMemoryError
、あるいはそれ以外かを確認して、該当する例外クラスに関するドキュメントを参照します。
スタック トレースの 2 行目には、例外がスローされたソースファイルのクラス、メソッド、ファイル、行番号が示されます。呼び出された関数ごとに、直前の呼び出しサイト(スタック フレームと呼びます)が別の行に表示されます。スタックをたどりながらコードを調べることで、誤った値を渡している場所を見つけられることがあります。コードがスタック トレースに表示されていなければ、どこかで無効なパラメータを非同期処理に渡している可能性があります。たいていの場合、スタック トレースの行をひとつひとつ調べて、使用した API クラスを探し出し、渡したパラメータが正しいか、適切な場所から API を呼び出したという点について確認すれば、何が起きたかを把握できます。
C / C++ コードを持つアプリのスタック トレースは、ほぼ同じように動作します。
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
Build fingerprint: 'google/foo/bar:10/123.456/78910:user/release-keys'
ABI: 'arm64'
Timestamp: 2020-02-16 11:16:31+0100
pid: 8288, tid: 8288, name: com.example.testapp >>> com.example.testapp <<<
uid: 1010332
signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x0
Cause: null pointer dereference
x0 0000007da81396c0 x1 0000007fc91522d4 x2 0000000000000001 x3 000000000000206e
x4 0000007da8087000 x5 0000007fc9152310 x6 0000007d209c6c68 x7 0000007da8087000
x8 0000000000000000 x9 0000007cba01b660 x10 0000000000430000 x11 0000007d80000000
x12 0000000000000060 x13 0000000023fafc10 x14 0000000000000006 x15 ffffffffffffffff
x16 0000007cba01b618 x17 0000007da44c88c0 x18 0000007da943c000 x19 0000007da8087000
x20 0000000000000000 x21 0000007da8087000 x22 0000007fc9152540 x23 0000007d17982d6b
x24 0000000000000004 x25 0000007da823c020 x26 0000007da80870b0 x27 0000000000000001
x28 0000007fc91522d0 x29 0000007fc91522a0
sp 0000007fc9152290 lr 0000007d22d4e354 pc 0000007cba01b640
backtrace:
#00 pc 0000000000042f89 /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::Crasher::crash() const)
#01 pc 0000000000000640 /data/app/com.example.testapp/lib/arm64/libexample.so (com::example::runCrashThread())
#02 pc 0000000000065a3b /system/lib/libc.so (__pthread_start(void*))
#03 pc 000000000001e4fd /system/lib/libc.so (__start_thread)
ネイティブ スタック トレースにクラスと関数レベルの情報が表示されない場合は、ネイティブ デバッグ シンボル ファイルを生成して Google Play Console にアップロードする必要があります。詳しくは、クラッシュのスタック トレースの難読化解除をご覧ください。ネイティブ コードでのクラッシュに関する一般的な情報については、ネイティブ コードでのクラッシュの診断をご覧ください。
クラッシュを再現するためのヒント
エミュレータを起動したりデバイスをパソコンに接続したりするだけでは、問題を完全には再現できないかもしれません。多くの場合、開発環境は、帯域幅、メモリ、ストレージなどのリソースに余裕があります。例外のタイプを確認することで、不足しているリソースを特定したり、Android のバージョン、デバイスタイプ、アプリのバージョン間の相関を発見したりすることができます。
メモリエラー
OutOfMemoryError
が発生する場合は、メモリ容量が小さいエミュレータを作成してテストします。図 2 は、デバイスのメモリ容量を制御できる AVD Manager 設定を示しています。
図 2. AVD Manager のメモリ設定
ネットワーク例外
ユーザーはモバイル ネットワークや Wi-Fi ネットワークの受信可能範囲を頻繁に出入りします。したがって、一般的に、アプリのネットワーク例外は、エラーとしてではなく、予期せず発生する正常な動作状態として扱う必要があります。
ネットワーク例外(UnknownHostException
など)を再現する必要がある場合は、アプリがネットワークを使用しようとしたときに機内モードをオンにしてみてください。
また、ネットワーク速度のエミュレーションまたはネットワーク遅延(あるいはその両方)をエミュレータで選択することにより、ネットワーク品質を低下させる方法もあります。AVD Manager の [Speed] と [Latency] の設定を使用するか、-netdelay
フラグと -netspeed
フラグを指定してエミュレータを起動します(次のコマンドラインの例を参照)。
emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm
この例では、すべてのネットワーク リクエストに対して、20 秒の遅延を設定し、アップロードとダウンロードの速度を 14.4 Kbps に設定しています。エミュレータのコマンドライン オプションの詳細については、コマンドラインからのエミュレータの起動をご覧ください。
logcat で確認する
クラッシュを再現する手順を実施できるようになったら、logcat
などのツールを使用してさらに情報を収集します。
logcat の出力には、デベロッパーが出力したログメッセージが、システムによって出力された他のメッセージとともに表示されます。アプリの実行中にログを出力すると CPU の負荷が増えて電池が消耗するため、特別に追加した Log
ステートメントをオフにすることを忘れないでください。
null ポインタ例外によるクラッシュを防ぐ
null ポインタ例外(ランタイム エラー タイプ NullPointerException
で識別される)は、通常、null であるオブジェクトにアクセスしようとしたときに、そのメソッドを呼び出すか、そのメンバーにアクセスすることで発生します。null ポインタ例外は、Google Play でアプリがクラッシュする最大の原因です。 null の目的は、オブジェクトが存在しないこと、たとえば、まだオブジェクトが作成されていないか、割り当てられていないことを示すことです。null ポインタ例外を回避するには、操作対象のオブジェクト参照が null でないことを確認してから、オブジェクトでメソッドを呼び出したり、そのメンバーへのアクセスを試みる必要があります。オブジェクト参照が null である場合は、このケースを適切に処理します。たとえば、オブジェクト参照に対するオペレーションを実行する前にメソッドを終了し、デバッグログに情報を書き込みます。
呼び出される各メソッドのパラメータごとに null チェックは必要でないため、IDE またはオブジェクトのタイプに基づいて null 値許容を示すことができます。
Java プログラミング言語
以下のセクションは、Java プログラミング言語に適用されます。
コンパイル時の警告
IDE からコンパイル時の警告を受け取るには、メソッドのパラメータにアノテーションを付けて @Nullable
と @NonNull
で値を返します。この警告は、null 可能オブジェクトが必要であることを示しています。
これらの null チェックは、null であることがわかっているオブジェクトを対象としています。@NonNull
オブジェクトに対する例外は、コード内で対処が必要なエラーを示します。
コンパイル時のエラー
null 可能性は意味のある型であるため、使用する型に埋め込むことで、null のコンパイル時チェックを実行できます。オブジェクトが null になることがわかっており、null 可能性を処理する必要がある場合、Optional
などのオブジェクトにラップできます。null 可能性は、常に型である必要があります。
Kotlin
Kotlin では、null 可能性が型システムに含まれています。たとえば、変数を null 可能や null 不可として宣言しておく必要があります。null 可能性型は、以下のように ?
で表示されます。
// non-null
var s: String = "Hello"
// null
var s: String? = "Hello"
null 不可変数に null 値を指定することはできません。null 可能変数は、非 null として使用する前に null 可能性をチェックする必要があります。
null を明示的にチェックしない場合は、?.
safe call 演算子を使用できます。
val length: Int? = string?.length // length is a nullable int
// if string is null, then length is null
null 可能オブジェクトについては、null ケースに対処することをおすすめします。そうしないと、アプリに予期しない状態が生じる可能性があります。こうしたエラーは、NullPointerException
でアプリがクラッシュしてはじめて見つかります。
null をチェックする方法を次にいくつか示します。
if
チェックval length = if(string != null) string.length else 0
スマート キャストと null チェックにより、Kotlin コンパイラが文字列値が null でないことを認識するため、safe call 演算子を指定しなくとも、参照を直接使用できます。
?:
Elvis 演算子この演算子を使用すると、「オブジェクトが null でない場合はオブジェクトを返し、それ以外の場合は別の値を返す」ことができます。
val length = string?.length ?: 0
Kotlin では、引き続き NullPointerException
がスローされます。この場合の最も一般的な状況を以下に紹介します。
NullPointerException
を明示的にスローする場合。- null アサーション
!!
演算子を使用している場合。この演算子は任意の値を非 null 型に変換し、値が null の場合はNullPointerException
をスローします。 - プラットフォーム型の null 参照にアクセスする場合。
プラットフォーム型
プラットフォーム型は、Java のオブジェクト宣言です。これらの型は特別な方法で処理します。null チェックは強制適用されないため、null でないことが Java の場合と同様に保証されます。プラットフォーム型参照にアクセスすると、Kotlin はコンパイル時のエラーを生成しませんが、これらの参照によってランタイム エラーが引き起こされる可能性があります。Kotlin ドキュメントにある次の例をご覧ください。
val list = ArrayList<String>() // non-null (constructor result) list.add("Item")
val size = list.size // non-null (primitive int) val item = list[0] // platform
type inferred (ordinary Java object) item.substring(1) // allowed, may throw an
// exception if item == null
Kotlin は、プラットフォーム値が Kotlin 変数に指定された場合に型の推論を使用します。または型を想定して定義することもできます。Java の参照の正しい null 可能性ステータスを確認するには、Java コードで null 可能性アノテーション(@Nullable
など)を使用することをおすすめします。Kotlin コンパイラはこれらの参照を、プラットフォーム型ではなく、実際の null 可能型または null 不可型として表します。
Java Jetpack API には必要に応じて @Nullable
または @NonNull
のアノテーションが付けられており、同様のアプローチが Android 11 SDK で行われています。この SDK の型は Kotlin で使用され、null 可能型または null 不可型として表されます。
Kotlin の型システムにより、アプリでは NullPointerException
クラッシュが大幅に減少することが確認されています。たとえば、Google Home アプリでは、新機能開発を Kotlin へ移行した際に、null ポインタ例外に起因するクラッシュが 30% 減少しました。