利用异常机制获取堆栈轨迹元素

0x0001 什么是堆栈轨迹?

堆栈轨迹(stack trace) 是 一个方法调用过程的列表,它包含程序在执行过程中调用的特定位置。当 Java 程序正常终止,而没有捕获异常时,就可以通过这个列表显示出来。

0x0002 获得堆栈轨迹元素

方法一:

可以通过调用 Throwable 的 printStackTrace 方法,访问堆栈轨迹的文本描述信息

1
2
3
4
Throwable throwable = new Throwable();
StringWriter out = new StringWriter();
throwable.printStackTrace(new PrintWriter(out));
String desc = out.toString();

方法二:

相较于方法一,这是一种更灵活的方法:getStackTrace 方法,它会 获得 StackTraceElement 对象的数组,可以通过分析数组,可以获得相应的信息。

可以使用如下示例进行调用:

1
2
3
4
5
Throwable throwable = new Throwable();
StackTraceElement[] elements = throwable.getStackTrace();
for(StackTraceElement element:elements){
....
}

通过 StackTraceElement 可以获得文件名、当前执行代码的行号、类名、方法名等消息,StackTraceElement 的源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public final class StackTraceElement implements Serializable {
public StackTraceElement(String declaringClass, String methodName, String fileName, int lineNumber) {
throw new RuntimeException("Stub!");
}
// 获得文件名
public String getFileName() {
throw new RuntimeException("Stub!");
}
// 获得行号
public int getLineNumber() {
throw new RuntimeException("Stub!");
}
// 获得所在类的类名
public String getClassName() {
throw new RuntimeException("Stub!");
}
// 获得所在方法的方法名
public String getMethodName() {
throw new RuntimeException("Stub!");
}
// // 获得所在方法是否是 native 方法
public boolean isNativeMethod() {
throw new RuntimeException("Stub!");
}
...
}

方法三:

通过 Thread.getAllStackTrace 方法,可以获得 所有线程的堆栈轨迹,具体调用方式:

1
2
3
4
5
Map<Tread,StackTraceElement> map = Thread.getAllStackTrace();
for(Thread thread:map.keySet()){
StrackTraceElement[] elements = map.get(thread);
...
}

0x0003 该机制的典型应用

以此机制可以打印调用方法的堆栈信息,一个比较常见的例子,在 Android 中通过 Log 日志调用方法的详细堆栈信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
object LogUtil {
private val isDebug = BuildConfig.DEBUG

private fun generateTag(): String {
val caller = Throwable().stackTrace[2]
var tag = "%s.%s(L:%d)"
var callerClazzName = caller.className
callerClazzName = callerClazzName.substring(callerClazzName.lastIndexOf(".") + 1)
tag = String.format(
Locale.CHINA, tag, callerClazzName, caller.methodName,
caller.lineNumber
)
val customTagPrefix = "h_log"
tag = if (TextUtils.isEmpty(customTagPrefix)) tag else "$customTagPrefix:$tag"
return tag
}

fun d(content: Any?) {
if (!isDebug || content == null) {
return
}
val tag = generateTag()
Log.d(tag, content.toString())
}
...
}

至于 Throwable().stackTrace[2] 中为什么取索引 2,这是因为该方法单独定义在一个类中,根据代码所在的位置不同,取得索引也是不同的。