デバッグの概要

1. 始める前に

ソフトウェアを使用したことがあれば、バグに遭遇したことがあるはずです。バグとは、アプリのクラッシュや、ある機能が想定どおりに動作しないなど、意図しない動作を引き起こすソフトウェアのエラーのことをいいます。経験に関係なく、デベロッパーは誰でもコードを記述する際にバグを混入させます。Android デベロッパーにとって最も重要なスキルは、バグを特定して修正することです。アプリのリリース全体がバグの修正に特化していることも珍しくありません。たとえば、次に示す Google マップのバージョン詳細をご覧ください。

9d5ec1958683e173.png

バグを修正するプロセスをデバッグと呼びます。著名なコンピュータ サイエンティストである Brian Kernighan 氏は、「最も効果的なデバッグツールは依然として、慎重な検討と、適切に配置された print ステートメントである」とかつて述べたことがあります。Kotlin の println() ステートメントについては以前の Codelab で見慣れているかもしれませんが、プロフェッショナルな Android デベロッパーは、プログラムの出力をより効率的に整理するためにロギングを使用します。この Codelab では、Android Studio でロギングを使用する方法と、デバッグツールとして利用する方法を学びます。また、スタック トレースと呼ばれるエラー メッセージログを読み解き、バグを特定して解決する方法も学びます。最後に、自力でバグを調査する方法と、Android Emulator からの出力を、実行中のアプリのスクリーンショットまたは GIF としてキャプチャする方法を学びます。

前提条件

  • Android Studio でプロジェクトを操作する方法を理解している。

学習内容

このトレーニングを修了すると、以下のことができるようになります。

  • android.util.Logger を使用してログを書き込む。
  • さまざまなログレベルをどのような場合に使用するかを理解する。
  • シンプルで強力なデバッグツールとしてログを使用する。
  • スタック トレース内の有意義な情報を見つける方法を理解する。
  • アプリのクラッシュを解決するためにエラー メッセージを検索する。
  • Android Emulator からスクリーンショットとアニメーション GIF をキャプチャする。

必要なもの

  • Android Studio がインストールされているパソコン

2. 新しいプロジェクトの作成

大規模で複雑なアプリは使用せず、空のプロジェクトから始めて、ログ ステートメントと、それらを使用してデバッグを行う方法を確認します。

まず、次の手順に沿って、新しい Android Studio プロジェクトを作成します。

  1. [New Project] 画面で、[Empty Activity] を選択します。

72a0bbf2012bcb7d.png

  1. アプリに「Debugging」という名前を付けます。言語を Kotlin に設定します。その他の項目は変更しないでください。

60a1619c07fae8f5.png

プロジェクトを作成すると、新しい Android Studio プロジェクトが開き、MainActivity.kt という名前のファイルが表示されます。

e3ab4a557c50b9b0.png

3.ロギングとデバッグ出力

これまでのレッスンでは、Kotlin の println() ステートメントを使用してテキスト出力を生成しました。Android アプリでは、Log クラスを使用して出力をログに記録する方法がおすすめです。出力をログに記録する関数はいくつかあり、Log.v()Log.d()Log.i()Log.w()、または Log.e() の形式があります。これらのメソッドは 2 つのパラメータを取ります。1 つ目は「タグ」と呼ばれ、ログメッセージのソースを識別する文字列(たとえば、テキストをログに記録したクラスの名前)です。2 つ目は実際のログメッセージです。

空のプロジェクトでロギングの使用を開始する手順は次のとおりです。

  1. MainActivity.kt で、クラス宣言の前に TAG という定数を追加し、その値をクラス名 MainActivity に設定します。
private const val TAG = "MainActivity"
  1. 次に示すように、MainActivity クラスに logging() という新しい関数を追加します。
fun logging() {
    Log.v(TAG, "Hello, world!")
}
  1. onCreate()logging() を呼び出します。新しい onCreate() メソッドは次のようになります。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
}
  1. アプリを実行して、実際のログを確認します。ログは画面下部の [Logcat] ウィンドウに表示されます。Logcat にはデバイス(またはエミュレータ)の他のプロセスからの出力も表示されるので、プルダウン メニューからアプリ(com.example.debugging)を選択して、目的のアプリと関係のないログを除外します。

199c65d11ee52b5c.png

出力ウィンドウで「Hello, world!」という出力を確認できるはずです。必要であれば、[Logcat] ウィンドウの上部にある検索ボックスに「hello」と入力して、すべてのログを検索します。

92f258013bc15d12.png

ログレベル

異なる文字が名前に付加されている異なるログ関数が存在するのは、それぞれが異なるログレベルに対応しているためです。出力する情報の種類に応じ、異なるログレベルを使用して Logcat 出力をフィルタできます。よく使用する主なログレベルは 5 つあります。

ログレベル

ユースケース

ERROR

ERROR ログには、なんらかの重大な問題が発生したことが報告されます。たとえば、アプリがクラッシュした理由などが示されます。

Log.e(TAG, "The cake was left in the oven for too long and burned.").

WARN

WARN ログには、エラーほど重大ではないものの、より重大なエラーを回避するために修正する必要がある点が報告されます。たとえば、非推奨の関数(使用が推奨されず、新しい代替手段の使用が望ましい)を呼び出した場合などです。

Log.w(TAG, "This oven does not heat evenly. You may want to turn the cake around halfway through to promote even browning.")

INFO

INFO ログには、「オペレーションが正常に完了した」などの有用な情報が示されます。

Log.i(TAG, "The cake is ready to be served.").println("The cake has cooled.")

DEBUG

DEBUG ログには、問題の調査に役立つ可能性がある情報が示されます。このログは、Google Play ストアで公開するビルドなどのリリースビルドには含まれません。

Log.d(TAG, "Cake was removed from the oven after 55 minutes. Recipe calls for the cake to be removed after 50 - 60 minutes.")

VERBOSE

VERBOSE ログは、その名のとおり最も詳細なログレベルです。デバッグログは詳細ログと比較すると若干主観的であると考えられますが、一般的に詳細ログは機能の実装後に削除できるのに対して、デバッグログはその後もデバッグに役立つ可能性があります。このログもリリースビルドには含まれません。

Log.v(TAG, "Put the mixing bowl on the counter.")Log.v(TAG, "Grabbed the eggs from the refrigerator.")Log.v(TAG, "Plugged in the stand mixer.")

各タイプのログレベル(特に DEBUGVERBOSE)をどのような場合に使用するべきかについて、決まったルールはありません。ソフトウェア開発チームは、各ログレベルをどのような場合に使用するかについて独自のガイドラインを設けることができます。特定のログレベル(たとえば VERBOSE)をまったく使用しないと決めてもかまいません。上記の 2 つのログレベルはリリースビルドに含まれないため、それらを使用してデバッグしても、公開されたアプリのパフォーマンスには影響しません。これに対して、println() ステートメントはリリースビルドに残るため、パフォーマンスに悪影響を及ぼすことに注意してください。

それぞれのログレベルが Logcat でどのように表示されるかを見てみましょう。

  1. MainActivity.kt で、logging() メソッドの内容を次のように置き換えます。
fun logging() {
    Log.e(TAG, "ERROR: a serious error like an app crash")
    Log.w(TAG, "WARN: warns about the potential for serious errors")
    Log.i(TAG, "INFO: reporting technical information, such as an operation succeeding")
    Log.d(TAG, "DEBUG: reporting technical information useful for debugging")
    Log.v(TAG, "VERBOSE: more verbose than DEBUG logs")
}
  1. アプリを実行して Logcat で出力を確認します。必要に応じて、com.example.debugging プロセスからのログのみが表示されるように出力をフィルタします。出力をフィルタして、「MainActivity」タグのあるログのみを表示することもできます。そうするには、[Logcat] ウィンドウの右上にあるプルダウン メニューから [Edit Filter Configuration] を選択します。

383ec6d746bb72b1.png

  1. 次に示すように、[Log Tag] に「MainActivity」と入力し、フィルタの名前を作成します。

e7ccfbb26795b3fc.png

  1. 「MainActivity」タグのあるログメッセージのみが表示されるようになりました。

4061ca006b1d278c.png

クラス名の前にログレベルを表す文字が付いていることに注目してください(例: W/MainActivity)。また、WARN ログは青色で表示され、ERROR ログは先ほどの例の致命的なエラーと同様に赤色で表示されています。

  1. プロセスでデバッグ出力をフィルタできるのと同様に、ログレベルでも出力をフィルタできます。デフォルトでは、このフィルタは [Verbose] に設定されており、VERBOSE とそれより上のログレベルが表示されます。プルダウン メニューから [Warn] を選択すると、WARN および ERROR レベルのログのみが表示されます。

c4aa479a8dd9d4ca.png

  1. 今度はプルダウンを [Assert] に変更し、ログが表示されないことを確認します。これにより、ERROR レベル以下がすべて除外されます。

ee3be7cfaa0d8bd1.png

println() ステートメントをやや問題視しすぎているように思われるかもしれませんが、構築するアプリの規模が大きくなると、それだけ Logcat の出力も増えます。異なるログレベルを使用すると、最も有用な関連情報をピックアップすることができます。ログを使用する方法は効果的であると考えられており、Android 開発では println() より好まれます。デバッグログと詳細ログはリリースビルドのパフォーマンスに影響しないからです。さまざまなログレベルでログをフィルタすることもできます。適切なログレベルを選択すると、開発チーム内のそれほどコードに精通していないメンバーにとってもログが理解しやすくなり、バグの特定と解決がずっと簡単になります。

4. エラー メッセージを含むログ

バグを混入させる

空のプロジェクトで行えるデバッグはほとんどありません。Android デベロッパーが遭遇するバグの多くはアプリのクラッシュに関連するものです。クラッシュは明らかに質の低いユーザー エクスペリエンスです。このアプリをクラッシュさせるコードを追加してみましょう。

算数の授業で、数をゼロで割ることはできないと習ったことを覚えていると思います。コード内でゼロ除算を行うとどうなるかを確認してみましょう。

  1. MainActivity.ktlogging() 関数の上に次の関数を追加します。このコードは、最初に 2 つの数を設定し、次に repeat を使用して、分子(numerator)を分母(denominator)で除算した結果を 5 回ログに記録しています。repeat ブロック内のコードを実行するたびに、分母の値を 1 ずつ減らします。最後の 5 回目の反復処理で、アプリはゼロ除算を実行しようとします。
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(5) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}
  1. onCreate()logging() の呼び出しの後に、division() 関数の呼び出しを追加します。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    logging()
    division()
}
  1. アプリを再度実行して、クラッシュすることを確認します。下にスクロールして MainActivity.kt クラスのログを確認すると、前に定義した logging() 関数のログ、division() 関数の詳細ログ、次いでアプリがクラッシュした理由を示す赤色のエラーログを確認できます。

12d87f287661a66.png

スタック トレースの内容

クラッシュ(例外とも言います)を記述するエラーログは、スタック トレースと呼ばれます。スタック トレースは、例外が発生するまでに呼び出されたすべての関数を、最近呼び出されたものから順に表示します。出力の全体は次のとおりです。

Process: com.example.debugging, PID: 14581
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

大量のテキストですが、幸いなことに、エラーを正確に絞り込むために必要なテキストは、通常はこの中の一部だけです。上から順に説明します。

  1. java.lang.RuntimeException:
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.ArithmeticException: divide by zero

最初の行には、アプリがアクティビティを開始できなかったことが記述されています。これが、アプリがクラッシュした原因です。次の行には、もう少し詳しい情報があります。具体的には、アクティビティを開始できなかった原因は、ArithmeticException です。もっと具体的に言うと、ArithmeticException のタイプは「ゼロ除算」(divide by zero)です。

  1. Caused by:
Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

「Caused by」の行まで下にスクロールすると、ここにも「ゼロ除算」エラーが発生したことが記述されています。こちらでは、エラーが発生した正確な関数(division())と正確な行番号(21)も示されています。[Logcat] ウィンドウのファイル名と行番号がハイパーリンクされています。出力には、エラーが発生した関数の名前(division())と、この関数を呼び出した関数(onCreate())も表示されています。

このバグは故意に混入されたものなので、この結果に驚きはありません。しかし、未知のエラーの原因を特定する必要がある場合には、例外のタイプ、関数名、行番号を正確に把握することは非常に有益です。

「スタック トレース」と呼ばれる理由

「スタック トレース」という用語は、エラーから出力されるテキストの名前としては奇妙に思えるかもしれません。その意味をより良く理解するには、関数スタックについてもう少し詳しく知る必要があります。

ある関数が別の関数を呼び出したとき、デバイスは 2 つ目の関数が完了するまで、最初の関数の後続のコードを実行しません。2 つ目の関数の実行が完了すると、最初の関数は中断した箇所から再開されます。2 つ目の関数によって呼び出される関数についても同様です。2 つ目の関数の実行は、3 つ目の関数(および 3 つ目の関数が呼び出す関数)が完了するまで再開されません。最初の関数は、2 つ目の関数の実行が完了するまで再開されません。これは、現実世界におけるスタック(皿やトランプなどを積み重ねた山)に似ています。皿を取りたいときは、スタックの一番上にある皿を取ります。スタックの下の方にある皿は、その上にある皿をすべて取らない限り、取ることができません。

関数スタックは、次のようなコードで表現できます。

val TAG = ...

fun first() {
    second()
    Log.v(TAG, "1")
}

fun second() {
    third()
    Log.v(TAG, "2")
    fourth()
}

fun third() {
    Log.v(TAG, "3")
}

fun fourth() {
    Log.v(TAG, "4")
}

first() を呼び出すと、次の順序で番号がログに記録されます。

3
2
4
1

なぜでしょうか?最初の関数は、呼び出されると直ちに second() を呼び出すので、番号 1 をすぐにログに記録することができません。関数スタックは次のようになります。

second()
first()

次に、2 つ目の関数が third() を呼び出して関数スタックに追加します。

third()
second()
first()

次に、3 つ目の関数が番号 3 を出力します。実行が完了すると、関数スタックから削除されます。

second()
first()

次に、second() 関数が番号 2 をログに記録してから fourth() を呼び出します。この時点で、番号 3 に次いで 2 がログに記録され、関数スタックは次のようになります。

fourth()
second()
first()

fourth() 関数が番号 4 を出力し、関数スタックから削除(ポップ)されます。次に、second() 関数の実行が完了し、関数スタックからポップされます。これで、second() とそこから呼び出された関数がすべて完了したので、デバイスは first() の残りのコードを実行し、それによって番号 1 が出力されます。

このようにして、4231 の順に番号がログに記録されます。

関数スタックのイメージを思い浮かべながら時間をかけてコードをたどると、どのコードがどのような順序で実行されるかを正確に把握できます。上記の例のゼロ除算のようなバグの場合は、それだけで強力なデバッグ技法になります。もっと複雑な問題をデバッグする場合でも、順を追ってコードを調べることにより、ログ ステートメントをどこに配置すればよいかを適切に把握できます。

5. ログを使用したバグの特定と修正

前のセクションでは、スタック トレースを調査し、特に次の行に注目しました。

Caused by: java.lang.ArithmeticException: divide by zero
        at com.example.debugging.MainActivity.division(MainActivity.kt:21)

この行から、クラッシュが 21 行目で発生したことと、それがゼロ除算に関連していることがわかります。つまり、このコードが実行される前のどこかで分母が 0 になったはずです。このような単純な例では、自力でコードをたどって調べる方法でも十分対応できますが、ログ ステートメントを使用すると、ゼロ除算が発生する前の分母の値を出力できるので、時間の節約になります。

  1. Log.v() ステートメントの前に、分母をログに記録する Log.d() 呼び出しを追加します。Log.d() を使用するのは、このログがデバッグ専用であり、詳細ログをフィルタで除外できるからです。
Log.d(TAG, "$denominator")
  1. アプリを再度実行します。今回もクラッシュしますが、分母が何度かログに記録されます。フィルタ構成を使用すると、"MainActivity" タグのあるログのみを表示できます。

d6ae5224469d3fd4.png

  1. ご覧のように、いくつかの値が出力されています。ループが数回実行され、分母が 0 になった 5 回目の反復処理でクラッシュしています。分母が 4 のときからループが 5 回反復されて分母が 1 ずつ減らされているので、これは当然の結果です。バグを修正するには、ループの反復回数を 5 から 4 に変更します。アプリを再実行すると、クラッシュしなくなるはずです。
fun division() {
    val numerator = 60
    var denominator = 4
    repeat(4) {
        Log.v(TAG, "${numerator / denominator}")
        denominator--
    }
}

6. デバッグ例: 存在しない値へのアクセス

デフォルトでは、プロジェクトの作成に使用した Blank Activity テンプレートによってアクティビティが 1 つ追加され、TextView が画面の中央に配置されます。以前学習したように、Layout Editor で ID を設定して findViewById() でビューにアクセスすると、コードからビューを参照できます。アクティビティ クラスで onCreate() を呼び出すときは、まず setContentView() を呼び出してレイアウト ファイル(activity_main.xml など)を読み込む必要があります。setContentView() を呼び出す前に findViewById() を呼び出そうとすると、ビューが存在しないため、アプリはクラッシュします。別のバグを説明するために、ビューにアクセスしてみましょう。

  1. activity_main.xml を開いて Hello, world! TextView を選択し、idhello_world に設定します。

c94be640d0e03e1d.png

  1. ActivityMain.kt に戻り、onCreate()setContentView() 呼び出しの前に、TextView を取得してそのテキストを「Hello, debugging!」に変更するコードを追加します。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val helloTextView: TextView = findViewById(R.id.hello_world)
    helloTextView.text = "Hello, debugging!"
    setContentView(R.layout.activity_main)
    division()
}
  1. アプリを再度実行し、今回も起動直後にクラッシュすることを確認します。"MainActivity" タグのないログを表示するには、前の例でフィルタを解除する必要があります。840ddd002e92ee46.png

例外は Logcat の最後の方に表示されます(表示されない場合は「RuntimeException」で検索できます)。出力は次のようになります。

Process: com.example.debugging, PID: 14896
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3449)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)
     Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null
        at com.example.debugging.MainActivity.onCreate(MainActivity.kt:14)
        at android.app.Activity.performCreate(Activity.java:8000)
        at android.app.Activity.performCreate(Activity.java:7984)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1309)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3422)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3601)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:85)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2066)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:223)
        at android.app.ActivityThread.main(ActivityThread.java:7656)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947)

前のように、上部に「Unable to start activity」と表示されています。MainActivity が起動する前にアプリがクラッシュしているため、当然です。次の行には、エラーについてもう少し詳しく記載されています。

java.lang.RuntimeException: Unable to start activity ComponentInfo{com.example.debugging/com.example.debugging.MainActivity}: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

スタック トレースのさらに下方には次の行もあり、関数呼び出しと行番号を示しています。

Caused by: java.lang.NullPointerException: findViewById(R.id.hello_world) must not be null

このエラーは正確には何を意味し、「null」値とは正確には何でしょうか。これはわざとらしい例であり、アプリがクラッシュした原因についてはすでにご存じかもしれませんが、今後見たことのないエラー メッセージに遭遇することもあるはずです。その場合、エラーを最初に確認した人はお客様ではない可能性があります。経験豊富なデベロッパーであっても、他の人が問題をどのように解決したのか確認するために、エラー メッセージを Google で検索することはよくあります。このエラーを検索すると、StackOverflow(デベロッパーがバグのあるコードや一般的なプログラミングのトピックについて質問したり答えたりできるサイト)から複数の検索結果が得られます。

質問に対して、似ているがまったく同じではない回答が示されていることも多いので、自分で回答を検索する際は以下のヒントを参考にしてください。

  1. 回答はいつ頃投稿されたものか。数年前に投稿された回答は、現在の状況では意味を失っている場合や、古くなったバージョンの言語やフレームワークに基づいている場合があります。
  2. 回答は Java または Kotlin に関係しているか。遭遇した問題が特定の言語に関係しているか、あるいは特定のフレームワークに関係しているかに注意しましょう。
  3. 「承認済み」の回答や賛成票が多い回答は優れた回答である可能性が高いですが、それ以外の回答にも有益な情報が含まれている可能性があることを忘れないでください。

1636a21ff125a74c.png

数字は賛成票(または反対票)の数を示しており、緑のチェックマークは承認済みの回答であることを示しています。

既存の質問が見つからない場合は、いつでも新しい質問を投稿できます。StackOverflow などのサイトで質問する際は、こちらのガイドラインを参考にすると良いでしょう。

それでは、エラーを検索してみましょう。

a60ba40e5247455e.png

いくつかの回答に目を通すと、エラーには複数の理由が考えられることがわかります。しかし、故意に setContentView() の前に findViewById() を呼び出していることを考えると、この質問のいくつかの回答が参考になりそうです。たとえば、2 番目に投票数の多い回答では次のように述べられています。

「おそらく setContentView を呼び出す前に findViewById を呼び出しているのでは?その場合は、setContentView を呼び出した後で findViewById を呼び出してみてください」

この回答を見てコードを確認すると、実際に findViewById() の呼び出しが早すぎる(setContentView() の前に配置されている)ことと、setContentView() の後に呼び出す必要があることがわかります。

コードを更新してエラーを修正します。

  1. findViewById() の呼び出しと、helloTextView のテキストを設定する行を、setContentView() の呼び出しの下に移動します。新しい onCreate() メソッドは次のようになります。バグが修正されたことを確認するため、次のようにログを追加することもできます。
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.d(TAG, "this is where the app crashed before")
    val helloTextView: TextView = findViewById(R.id.hello_world)
    Log.d(TAG, "this should be logged if the bug is fixed")
    helloTextView.text = "Hello, debugging!"
    logging()
    division()
}
  1. アプリを再実行します。アプリがクラッシュしなくなり、テキストが想定どおりに更新されることを確認してください。

9ff26c7deaa4a7cc.png

スクリーンショットを撮る

このコースでは、これまでに Android Emulator の多くのスクリーンショットを目にしたはずです。スクリーンショットを撮るのは比較的簡単ですが、バグを再現する手順などの情報を他のチームメンバーと共有するのに便利です。Android Emulator でスクリーンショットを撮るには、右側のツールバーにあるカメラアイコンを押します。

455336f50c5c3c7f.png

また、キーボード ショートカットの Command+S でスクリーンショットを撮ることもできます。スクリーンショットは、自動的にデスクトップ フォルダに保存されます。

実行中のアプリを録画する

スクリーンショットは多くの情報を伝えることができますが、実行中のアプリの録画を共有する方法も、他のメンバーがバグの原因を再現できるようにするために役立ちます。Android Emulator には、実行中のアプリの GIF(アニメーション画像)を簡単にキャプチャできる組み込みツールがいくつか用意されています。

  1. 右側のエミュレータ ツールで、その他アイコン 558dbea4f70514a8.png(最後のオプション)をクリックすると、追加のエミュレータ デバッグ オプションが表示されます。ウィンドウが開き、テスト目的で物理デバイスの機能をシミュレートするための追加のツールが表示されます。

46b1743301a2d12.png

  1. 左側のメニューで [Record and Playback] をクリックすると、録画開始ボタンが表示された画面が開きます。

dd8b5019702ead03.png

  1. 現在のところ、プロジェクトには静的な TextView 以外に録画する価値のあるものはありません。コードを変更して、数秒ごとにラベルを更新して除算結果を表示するようにしてみましょう。MainActivitydivision() メソッドで、Log() の呼び出しの前に Thread.sleep(3000) の呼び出しを追加します。このメソッドは次のようになります(クラッシュを避けるため、ループの反復回数を 4 回にとどめる必要があります)。
fun division() {
   val numerator = 60
   var denominator = 4
   repeat(4) {
       Thread.sleep(3000)
       Log.v(TAG, "${numerator / denominator}")
       denominator--
   }
}
  1. activity_main.xml で、TextViewiddivision_textview に設定します。

db3c1ef675872faf.png

  1. MainActivity.kt に戻り、Log.v()findViewById() および setText() の呼び出しに置き換えて、テキストを除算結果に設定します。
findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
  1. 除算の結果をアプリの UI でレンダリングしているため、UI の更新の実行方法について細心の注意を払う必要があります。まず、repeat ループを実行できる新しいスレッドを作成する必要があります。そうしないと、Thread.sleep(3000) がメインスレッドをブロックし、onCreate() が終了するまでアプリビューがレンダリングされません(repeat ループのある division() を含む)。
fun division() {
   val numerator = 60
   var denominator = 4

   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
         denominator--
      }
   }
}
  1. 試しにアプリを実行してみると、FATAL EXCEPTION が表示されます。この例外が発生するのは、ビューを作成したスレッドのみがビューを変更できるためです。runOnUiThread() を使用して UI スレッドを参照できます。division() を変更して、UI スレッド内の TextView を更新します。
private fun division() {
   val numerator = 60
   var denominator = 4
   thread(start = true) {
      repeat(4) {
         Thread.sleep(3000)
         runOnUiThread {
            findViewById<TextView>(R.id.division_textview).setText("${numerator / denominator}")
            denominator--
         }
      }
   }
}
  1. アプリを実行すると、すぐにエミュレータに切り替わります。アプリが起動したら、[Extended Controls] ウィンドウの [Start Recording] ボタンをクリックします。除算結果が 3 秒ごとに更新されます。除算結果が数回更新されたら、[Stop Recording] をクリックします。

55121bab5b5afaa6.png

  1. デフォルトでは、出力は .webm 形式で保存されます。プルダウンを使用すると、出力を GIF ファイルとしてエクスポートできます。

850713aa27145908.png

7. 完了

お疲れさまでした。このパスウェイでは、以下のことを学習しました。

  • デバッグとは、コードのバグをトラブルシューティングするプロセスです。
  • ログを使用すると、さまざまなログレベルとタグを含むテキストを出力できます。
  • スタック トレースを読むと、例外に関する情報(例外の原因となった関数や例外が発生した行番号など)が得られます。
  • デバッグを行う際は、誰かが同じ問題または似た問題に遭遇している可能性があるため、StackOverflow などのサイトを利用して、バグを調査することができます。
  • Android Emulator を使用すると、スクリーンショットとアニメーション GIF を簡単にエクスポートできます。

詳細