kotlin inline 関数内で発生した例外のスタックトレース


概要

クラッシュレポートを調べていて、スタックトレースに書かれた行番号がそのソースファイルに存在しないことがありました。

検索してみると、以下のページを見つけました。

inline 関数で展開されるコードで例外が発生した場合、そのスタックトレースにはソースファイルの行数を超えた行番号が書かれるようです。

試してみる

例1. inline 関数で例外を投げてみる

以下のようなコードを作成しました。例外を投げる inline 関数 throwErrorFunc を実行し、例外をキャッチしスタックトレースをログに出力します。

MainActivity.kt
 1: package com.example.inlineexceptionapp
 2:
 3: import androidx.appcompat.app.AppCompatActivity
 4: import android.os.Bundle
 5: 
 6: class MainActivity : AppCompatActivity() {
 7:     override fun onCreate(savedInstanceState: Bundle?) {
 8:         super.onCreate(savedInstanceState)
 9:         setContentView(R.layout.activity_main)
10: 
11:         try {
12:             throwErrorFunc()
13:         }
14:         catch (e: Exception) {
15:             println(e.stackTraceToString())
16:         }
17:     }
18: 
19: 
20:     inline fun throwErrorFunc() {
21:         throw Exception("error")
22:     }
23: }

実行した際に出力されたログ

I/System.out: java.lang.Exception: error
I/System.out:     at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:24)
I/System.out:     at android.app.Activity.performCreate(Activity.java:7009)
I/System.out:     at android.app.Activity.performCreate(Activity.java:7000)
I/System.out:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
I/System.out:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731)
I/System.out:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
I/System.out:     at android.app.ActivityThread.-wrap11(Unknown Source:0)
I/System.out:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
I/System.out:     at android.os.Handler.dispatchMessage(Handler.java:106)
I/System.out:     at android.os.Looper.loop(Looper.java:164)
I/System.out:     at android.app.ActivityThread.main(ActivityThread.java:6494)
I/System.out:     at java.lang.reflect.Method.invoke(Native Method)
I/System.out:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
I/System.out:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

MainActivity.kt には 23 行しかありませんが、スタックトレースには at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:24) とあります。MainActivity.kt に存在しない行番号が出力されていることがわかります。

例2. forEach 関数

for など列挙中のコレクションに対して addremove を行い、コレクションの要素数を変更した場合、ConcurrentModificationException が発生します。

以下のコードを作成しました。

MainActivity.kt
 1: package com.example.inlineexceptionapp
 2: 
 3: import androidx.appcompat.app.AppCompatActivity
 4: import android.os.Bundle
 5: 
 6: class MainActivity : AppCompatActivity() {
 7:     override fun onCreate(savedInstanceState: Bundle?) {
 8:         super.onCreate(savedInstanceState)
 9:         setContentView(R.layout.activity_main)
10: 
11:         try {
12:             val list = mutableListOf(1, 2, 3)
13:             list.forEach {
14:                 // 何かしらの処理
15: 
16:                 // 誤って列挙中のコレクションを変更してしまう
17:                 list.add(1)
18: 
19:                 // 何かしらの処理
20:             }
21:         }
22:         catch (e: Exception) {
23:             println(e.stackTraceToString())
24:         }
25:     }
26: }

実行した際に出力されたログ

I/System.out: java.util.ConcurrentModificationException
I/System.out:     at java.util.ArrayList$Itr.next(ArrayList.java:860)
I/System.out:     at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:27)
I/System.out:     at android.app.Activity.performCreate(Activity.java:7009)
I/System.out:     at android.app.Activity.performCreate(Activity.java:7000)
I/System.out:     at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1214)
I/System.out:     at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2731)
I/System.out:     at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2856)
I/System.out:     at android.app.ActivityThread.-wrap11(Unknown Source:0)
I/System.out:     at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1589)
I/System.out:     at android.os.Handler.dispatchMessage(Handler.java:106)
I/System.out:     at android.os.Looper.loop(Looper.java:164)
I/System.out:     at android.app.ActivityThread.main(ActivityThread.java:6494)
I/System.out:     at java.lang.reflect.Method.invoke(Native Method)
I/System.out:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
I/System.out:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)

at com.example.inlineexceptionapp.MainActivity.onCreate(MainActivity.kt:27) とあり、やはり MainActivity.kt に存在しない 27 行で発生したことになっています。

なぜソースファイルの範囲外の行番号を付けるか

概要に挙げたリンクに以下のようにあります。

This means that the actual source line is an inlined code fragment from somewhere else. As class files (in the proper debug information) only support specifying a single source file kotlin had to use a workaround. Basically it adds a table in the class file with mappings between line number ranges and source files. The line numbers used for this are outside the actual range of line numbers of the file. The debugger/ide will “fix” it up for you, but exceptions don’t do that.
(Google 翻訳)
これは、実際のソース行が別の場所からのインライン化されたコードフラグメントであることを意味します。クラスファイル(適切なデバッグ情報内)は単一のソースファイルの指定のみをサポートしているため、kotlinは回避策を使用する必要がありました。基本的に、行番号範囲とソースファイル間のマッピングを含むテーブルをクラスファイルに追加します。これに使用される行番号は、ファイルの実際の行番号の範囲外です。デバッガー/ IDEはそれを「修正」しますが、例外はそれを行いません。

デバッグ目的のため、inline 展開されるコードには実際のソースファイルの範囲外の行番号を付けているようです。

付けられた行番号は、クラスファイル追加されているようです。

確認してみる

例2 で使用したコードで確認してみます。

1: Kotlin Bytecode を表示してみる

Bytecode を表示してみました。以下の手順でソースコードの Bytecode を表示することができます。
* Tools -> Kotlin -> Show Kotlin Bytecode

ソースコード 13 行にカーソルを合わせると、その行に対応する Bytecode がハイライトされます。

Bytecode 87行以降は java.util.List.dd を実行していることから、Bytecode 67-87 行が展開された forEach のコードのようです。

Bytecode 67, 72 行に LINENUMBER 27 とあり、スタックトレースにある行番号と一致します。

2: class ファイルを逆アセンブルしてみる

LineNumberTable はそのバイトコードとソース上の行番号の対応がかかれています。LocalVariableTable は、メソッド内で宣言した変数の名前などを保持したものです。 LineNumberTable と LocalVariableTable は主にデバッグ時に用いられ、実行時には不必要な属性です。

class ファイルを逆アセンブルし、LineNumberTable を確認すると良さそうです。

class ファイルは \app\build\tmp\kotlin-classes\debug\com\example\inlineexceptionapp\MainActivity.class にあります。以下のコマンドを実行して、逆アセンブルします。

$ javap -verbose -l -c MainActivity.class

出力結果です。

// 省略
  protected void onCreate(android.os.Bundle);
    descriptor: (Landroid/os/Bundle;)V
    flags: ACC_PROTECTED
    Code:
      stack=4, locals=9, args_size=2
         0: aload_0
         1: aload_1
         2: invokespecial #11                 // Method androidx/appcompat/app/AppCompatActivity.onCreate:(Landroid/os/Bundle;)V
         5: aload_0
         6: ldc           #12                 // int 2131427356
         8: invokevirtual #16                 // Method setContentView:(I)V
        11: iconst_5
        12: anewarray     #18                 // class java/lang/Integer
        15: dup
        16: iconst_0
        17: iconst_1
        18: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        21: aastore
        22: dup
        23: iconst_1
        24: iconst_2
        25: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        28: aastore
        29: dup
        30: iconst_2
        31: iconst_3
        32: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        35: aastore
        36: dup
        37: iconst_3
        38: iconst_4
        39: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        42: aastore
        43: dup
        44: iconst_4
        45: iconst_5
        46: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        49: aastore
        50: invokestatic  #28                 // Method kotlin/collections/CollectionsKt.mutableListOf:([Ljava/lang/Object;)Ljava/util/List;
        53: astore_2
        54: nop
        55: aload_2
        56: checkcast     #30                 // class java/lang/Iterable
        59: astore_3
        60: iconst_0
        61: istore        4
        63: aload_3
        64: invokeinterface #34,  1           // InterfaceMethod java/lang/Iterable.iterator:()Ljava/util/Iterator;
        69: astore        5
        71: aload         5
        73: invokeinterface #40,  1           // InterfaceMethod java/util/Iterator.hasNext:()Z
        78: ifeq          118
        81: aload         5
        83: invokeinterface #44,  1           // InterfaceMethod java/util/Iterator.next:()Ljava/lang/Object;
        88: astore        6
        90: aload         6
        92: checkcast     #46                 // class java/lang/Number
        95: invokevirtual #50                 // Method java/lang/Number.intValue:()I
        98: istore        7
       100: iconst_0
       101: istore        8
       103: aload_2
       104: iconst_1
       105: invokestatic  #22                 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
       108: invokeinterface #56,  2           // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
//省略
      LineNumberTable:
        line 8: 0
        line 9: 5
        line 11: 11
        line 12: 54
        line 13: 55
        line 27: 63
        line 27: 71
        line 17: 103
        line 20: 114
        line 28: 118
        line 22: 122
        line 23: 123
        line 24: 143
        line 25: 143

LineNumberTable より、Bytecode とソースコードを以下のように対応させているようです。

  • line 13: 55
    • Bytecode 55 行 -> ソースコード 13 行
  • line 27: 63 , line27: 71
    • Bytecode 63-103 行 -> ソースコード 27 - 行
  • line 17: 103
    • Bytecode 103 行 -> ソースコード 17 行

Bytecode 63-103 行 は Kotlin Bytecode で見たように Iterator , Iterator.hasNext が見られます。forEach で展開されるコードを "ソースコードの 27 行" としているのは間違いなさそうです。

まとめ

クラッシュレポートなど、スタックトレースでソースファイルの範囲外の行番号が出力されていた場合は、inline 関数を実行している個所を疑ってみましょう。

ソースファイルの Kotlin Bytecode を表示させてスタックトレースに書かれた行番号を検索することで、該当箇所を見つけられるかもしれません。