Android アプリは、未処理の例外またはシグナルが原因で予期しない終了が発生するとクラッシュします。Java または Kotlin で記述されたアプリは、Throwable
クラスによって表される未処理の例外をスローした場合にクラッシュします。マシンコードまたは C++ で記述されたアプリは、実行中に SIGSEGV
などの未処理のシグナルが発生した場合にクラッシュします。
アプリがクラッシュすると、Android はアプリのプロセスを終了し、アプリが停止したことをユーザーに知らせる図 1 のようなダイアログを表示します。
アプリは、フォアグラウンドで実行されていない場合でも、クラッシュすることがあります。バックグラウンドで実行されるブロードキャスト レシーバやコンテンツ プロバイダを含め、あらゆるアプリ コンポーネントがアプリのクラッシュを引き起こす可能性があります。操作中ではないアプリがクラッシュすると、たいていのユーザーは困惑します。
アプリでクラッシュが発生する場合、このページのガイダンスが問題の診断と解決に有用です。
問題を検出する
ユーザーが使用中のアプリでクラッシュが発生していることをデベロッパーが常に把握できるとは限りません。アプリをすでに公開している場合は、Android Vitals を使用してアプリのクラッシュ発生率を確認できます。
Android Vitals
Android Vitals は、アプリのクラッシュ発生率をモニターして改善するために役立ちます。Android Vitals は、以下のクラッシュ発生率を測定します。
- クラッシュ発生率: いずれかのタイプのクラッシュが発生した、1 日のアクティブ ユーザーの割合。
ユーザーが認識したクラッシュ発生率: アプリのアクティブな使用中のクラッシュ(ユーザーが認識したクラッシュ)が 1 回以上発生した、1 日のアクティブ ユーザーの割合。アプリは、アクティビティを表示しているかフォアグラウンド サービスを実行しているときに、アクティブに使用されているとみなされます。
複数回クラッシュ発生率: クラッシュが 2 回以上発生した、1 日のアクティブ ユーザーの割合。
1 日のアクティブ ユーザーとは、1 日に 1 台のデバイスでアプリを使用するユニーク ユーザーを表し、複数のセッションにまたがる場合があります。1 人のユーザーが 1 日に複数のデバイスでアプリを使用する場合は、デバイスごとにその日のアクティブ ユーザーとしてカウントされます。複数のユーザーが 1 日に同じデバイスを使用する場合は、1 人のアクティブ ユーザーとしてカウントされます。
ユーザーが認識したクラッシュ発生率は「主な指標」のひとつであり、Google Play でのアプリの見つけやすさに影響します。この指標でカウントされるクラッシュは、ユーザーがアプリを使用中に発生するものであり、使用中断の主な原因となるため、これは重要な指標になります。
Google Play では、この指標について 2 つの不正な動作のしきい値を定義しています。
- 全体的な不正な動作のしきい値: すべてのデバイスモデルで、1 日のアクティブ ユーザーの 1.09% 以上にユーザーが認識したクラッシュが発生している。
- デバイスごとの不正な動作のしきい値: 1 つのデバイスモデルで、1 日のアクティブ ユーザーの 8% 以上にユーザーが認識したクラッシュが発生している。
アプリが全体的な不正な動作のしきい値を超えると、すべてのデバイスでアプリの見つけやすさが低下する可能性があります。一部のデバイスでアプリがデバイスごとの不正な動作のしきい値を超えると、それらのデバイスでアプリの見つけやすさが低下し、ストアの掲載情報に警告が表示される可能性があります。
Android Vitals を使用すると、アプリが過度にクラッシュするときに、Google Play Console を介してアラートを受け取ることができます。
Google Play が Android Vitals のデータを収集する方法については、Google Play Console のドキュメントをご覧ください。
クラッシュを診断する
アプリがクラッシュしていることを把握したら、クラッシュを診断します。クラッシュの解決は場合によっては困難です。しかし、クラッシュの根本原因を特定できれば、ほとんどの場合は解決策を見つけられます。
アプリのクラッシュが発生する状況はさまざまです。null 値または空の文字列の検出のように原因が明らかな場合もありますが、無効な引数が API に渡された、またはマルチスレッド化されたインタラクションが複雑すぎるなど、わかりにくい原因による場合もあります。
Android でクラッシュが発生すると、スタック トレースが生成されます。スタック トレースとは、クラッシュの時点までにプログラムで行われた関数呼び出しを順番にネストしたスナップショットです。クラッシュのスタック トレースは Android Vitals で確認できます。
スタック トレースの読み方
クラッシュを解決するには、まずクラッシュの発生場所を特定します。Google 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 設定を示しています。
ネットワーク例外
ユーザーはモバイル ネットワークや 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% 減少しました。