如果我们可以在调用函数时拿到当前调用堆栈,就可以取到一系列想要的数据。

+

一、思考

iOS 开发的同学都知道,我们在平时开发过程中,经常会用到非常实用的自定义打印功能,方便我们快速定位是在哪个控制器,哪一行代码。

OCSwift 中都可以很轻松实现,因为系统本来就提供了用于日志输出的预处理宏,只要我们拿来拼接就可以了,这里以 Swift 的打印为例:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
enum LogType: String {
case error = "❤️ ERROR"
case warning = "💛 WARNING"
case info = "💙 INFO"
case debug = "💚 DEBUG"
}

let log = Logger.shared

final class Logger {
static let shared = Logger()
private init() { }

static let logDateFormatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS"
return f
}()
}

extension Logger {
func error<T>(
_ message : T,
file : StaticString = #file,
function : StaticString = #function,
line : UInt = #line
) {
LXFLog(message, type: .error, file : file, function: function, line: line)
}

func warning<T>(
_ message : T,
file : StaticString = #file,
function : StaticString = #function,
line : UInt = #line
) {
LXFLog(message, type: .warning, file : file, function: function, line: line)
}

func info<T>(
_ message : T,
file : StaticString = #file,
function : StaticString = #function,
line : UInt = #line
) {
LXFLog(message, type: .info, file : file, function: function, line: line)
}

func debug<T>(
_ message : T,
file : StaticString = #file,
function : StaticString = #function,
line : UInt = #line
) {
LXFLog(message, type: .debug, file : file, function: function, line: line)
}
}

// MARK:- 自定义打印方法
// target -> Build Settings 搜索 Other Swift Flags
// 设置Debug 添加 -D DEBUG
func LXFLog<T>(
_ message : T,
type: LogType,
file : StaticString = #file,
function : StaticString = #function,
line : UInt = #line
) {
#if DEBUG
let time = Logger.logDateFormatter.string(from: Date())
let fileName = (file.description as NSString).lastPathComponent
print("\(time) \(type.rawValue) \(fileName):(\(line))-\(message)")
#endif
}

使用及打印结果:

1
2
3
4
5
6
7
8
9
10
11
log.debug("models count -- \(models.count)")
log.warning("models count -- \(models.count)")
log.info("models count -- \(models.count)")
log.error("models count -- \(models.count)")

/*
💚 DEBUG XXXViewController.swift:(79)-models count -- 10
💛 WARNING XXXViewController.swift:(80)-models count -- 10
💙 INFO XXXViewController.swift:(81)-models count -- 10
❤️ ERROR XXXViewController.swift:(82)-models count -- 10
*/

OCSwift 的预处理宏对应表

OC Swift
__FILE__ #file 打印当前文件路径
__LINE__ #line 打印当前行号,整数
__FUNCTION__ #function 打印当前函数或方法

但是在 Dart 中并没有提供这些功能,但是这个功能对我们来说确实又非常需要,那有什么办法实现它呢?


🤔

我们回想在开发过程中,是不是发现只要一不小心抛异常,就可以看到类似如下的打印内容,而且还能清楚的知道异常是在哪个文件和哪一行的代码造成的。

所以如果我们可以在调用函数时拿到当前调用堆栈,就可以取到一系列想要的数据。

二、实践

dart:core 中提供了 堆栈跟踪(StackTrace),可以通过 StackTrace.current 取到当前的堆栈信息,打印如下图所示,会发现这不好拿到我们想要的信息。

这里我用到了官方开发的一个包 stack_trace,它可以将堆栈信息变得更多人性化,并方便我们查看堆栈信息和获取想要的数据。

ps: stack_traceFlutter 环境下直接导包即可使用,而在纯 Dart 下需要将其添加为依赖于pubspec.yaml中。

1
2
dependencies:
stack_trace: ^1.9.3

那下面我们来试试 stack_trace 的威力吧

1
2
3
4
5
6
7
8
import 'package:stack_trace/stack_trace.dart';

// 将 StackTrace 对象转换成 Chain 对象
// 当然,这里也可以直接用 Chain.current();
// final chain = Chain.current();
final chain = Chain.forTrace(StackTrace.current);

print(chain);

打印内容:

1
2
3
4
5
6
7
8
9
10
11
12
flutter: package:flutter_test1/main.dart 79:17       _MyHomePageState.test_print
package:flutter_test1/main.dart 38:5 _MyHomePageState._incrementCounter
package:flutter/src/material/ink_well.dart 779:19 _InkResponseState._handleTap
package:flutter/src/material/ink_well.dart 862:36 _InkResponseState.build.<fn>
package:flutter/src/gestures/recognizer.dart 182:24 GestureRecognizer.invokeCallback
package:flutter/src/gestures/tap.dart 504:11 TapGestureRecognizer.handleTapUp
package:flutter/src/gestures/tap.dart 282:5 BaseTapGestureRecognizer._checkUp
package:flutter/src/gestures/tap.dart 254:7 BaseTapGestureRecognizer.acceptGesture
package:flutter/src/gestures/arena.dart 156:27 GestureArenaManager.sweep
package:flutter/src/gestures/binding.dart 222:20 GestureBinding.handleEvent
package:flutter/src/gestures/binding.dart 198:22 GestureBinding.dispatchEvent
package:flutter/src/gestures/binding.dart 156:7 GestureBinding._handle<…>

工具代码雏形:

1
2
3
4
5
6
7
8
9
10
11
12
13
import 'package:stack_trace/stack_trace.dart';

// 将 StackTrace 对象转换成 Chain 对象
// 当然,这里也可以直接用 Chain.current();
final chain = Chain.forTrace(StackTrace.current);
// 拿出其中一条信息
final frames = chain.toTrace().frames;
final frame = frames[1];
// 打印
print("所在文件:${frame.uri} 所在行 ${frame.line} 所在列 ${frame.column}");

// 打印结果
// flutter: 所在文件:package:flutterlog/main.dart 所在行 55 所在列 23

Frame 类的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// A single stack frame. Each frame points to a precise location in Dart code.
class Frame {
/// The URI of the file in which the code is located.
///
/// This URI will usually have the scheme `dart`, `file`, `http`, or `https`.
final Uri uri;

/// The line number on which the code location is located.
///
/// This can be null, indicating that the line number is unknown or
/// unimportant.
final int line;

/// The name of the member in which the code location occurs.
///
/// Anonymous closures are represented as `<fn>` in this member string.
final String member;
...
}
  • uri : 获取代码所在文件的路径
  • line : 获取代码所在行
  • member : 获取所在方法

打印看一下 :

1
2
3
4
5
6
7
8
// uri
print("${frame.uri.toString()}"); // package:flutter_test1/main.dart

// member
print("${frame.member}"); // _MyHomePageState.scheduleAsync.<fn>

// line
print("${frame.line}"); // 97

三、呈上代码

下面我做了一点封装,直接拿走即可使用,完整的代码和示例请到GitHub上【查看】

代码:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// log.dart

enum FLogMode {
debug, // 💚 DEBUG
warning, // 💛 WARNING
info, // 💙 INFO
error, // ❤️ ERROR
}

void FLog(dynamic msg, { FLogMode mode = FLogMode.debug }) {
if (kReleaseMode) { // release模式不打印
return;
}
var chain = Chain.current(); // Chain.forTrace(StackTrace.current);
// 将 core 和 flutter 包的堆栈合起来(即相关数据只剩其中一条)
chain = chain.foldFrames((frame) => frame.isCore || frame.package == "flutter");
// 取出所有信息帧
final frames = chain.toTrace().frames;
// 找到当前函数的信息帧
final idx = frames.indexWhere((element) => element.member == "FLog");
if (idx == -1 || idx+1 >= frames.length) {
return;
}
// 调用当前函数的函数信息帧
final frame = frames[idx+1];

var modeStr = "";
switch(mode) {
case FLogMode.debug:
modeStr = "💚 DEBUG";
break;
case FLogMode.warning:
modeStr = "💛 WARNING";
break;
case FLogMode.info:
modeStr = "💙 INFO";
break;
case FLogMode.error:
modeStr = "❤️ ERROR";
break;
}

print("$modeStr ${frame.uri.toString().split("/").last}(${frame.line}) - $msg ");
}

四、使用

1
2
3
4
5
6
7
8
// 直接使用FLog
FLog("flutter_log demo");

// mode:打印模式(默认值为debug)
FLog("flutter_log demo", mode: FLogMode.debug);
FLog("flutter_log demo", mode: FLogMode.warning);
FLog("flutter_log demo", mode: FLogMode.info);
FLog("flutter_log demo", mode: FLogMode.error);

打印效果如下所示: