概要
クラッシュレポートを調べていて、スタックトレースに書かれた行番号がそのソースファイルに存在しないことがありました。
検索してみると、以下のページを見つけました。
inline
関数で展開されるコードで例外が発生した場合、そのスタックトレースにはソースファイルの行数を超えた行番号が書かれるようです。
試してみる
例1. inline 関数で例外を投げてみる
以下のようなコードを作成しました。例外を投げる inline
関数 throwErrorFunc
を実行し、例外をキャッチしスタックトレースをログに出力します。
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
など列挙中のコレクションに対して add
や remove
を行い、コレクションの要素数を変更した場合、ConcurrentModificationException
が発生します。
以下のコードを作成しました。
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 を表示させてスタックトレースに書かれた行番号を検索することで、該当箇所を見つけられるかもしれません。