文章目录
- 写在前面
- 内容
- 先从异常的例子讲起
- Zone
- main
- 参数
- zoneValues
- zoneSpecification
- 修改 print 行为
- 修改 run 的行为
- 修改注册回调
- onError
写在前面
在《Flutter 实战》这本书里的 Flutter异常捕获 一节,讲到了如何对异步异常进行捕获,里面就提到了用 Zone 来做处理。
Zone表示一个代码执行的环境范围,为了方便理解,读者可以将Zone类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。
这一篇主要就是对 Zone 进行一些用法和参数上的了解。
内容
先从异常的例子讲起
在 Flutter 里,同步代码里异常的捕获使用try-catch
进行。我们用await
的方式来处理 Future
,同样也是同步代码,所以也可以被try-catch
捕获:
main() async {
try {
throw "My Error";
} catch (e) {
print(e);
}
try {
await Future.error("My Future Error");
} catch (e) {
print(e);
}
}
打印结果:
My Error
My Future Error
对于不是这种同步写法的异步错误,那么我们就需要通过Zone
来处理。Zone
给代码提供了一个环境,并能捕获到里面的相关信息。以《Flutter 实战》一书里给的例子,它通过 Dart 里的runZoned()
方法,让当前 Zone fork 一个新的 Zone 出来,把代码放在里面执行。
R runZoned<R>(R body(),
{Map<Object?, Object?>? zoneValues,
ZoneSpecification? zoneSpecification,
@Deprecated("Use runZonedGuarded instead") Function? onError}) {
checkNotNullable(body, "body");
...
return _runZoned<R>(body, zoneValues, zoneSpecification);
}
R _runZoned<R>(R body(), Map<Object?, Object?>? zoneValues,
ZoneSpecification? specification) =>
Zone.current
.fork(specification: specification, zoneValues: zoneValues)
.run<R>(body);
可以看出runZoned()
其实是对Zone.current.fork().run()
的一个封装。
所以通过给我们的整个 App 套上一个 Zone,就可以捕获所有的异常了。
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
var details=makeDetails(obj,stack);
reportError(details);
});
Zone
main
当我们运行main()
函数的时候,其实就已经运行在一个 root 的 Zone 里:
main() {
Zone.root.run(() => print("hello"));
print(Zone.root == Zone.current);
}
打印结果:
hello
true
在main()
里调用Zone.root.run()
方法跟直接在main()
里没区别,而且由于已经默认运行起来,所以我们也不能对Zone
进行一些定制修改,这也是为什么我们要新建一个Zone
来处理。
参数
Zone 里有几个参数:
- zoneValues
- zoneSpecification
- onError
zoneValues
这是 Zone 的私有数据,可以通过实例 zone[key] 获取,并且可以被自己 fork 出来的子 Zone 继承。
Zone parentZone = Zone.current.fork(zoneValues: {"parentValue": 1});
Zone childZone = parentZone.fork();
// childZone 可以通过父的 key 获得相应的 value
childZone.run(() => {print(childZone["parentValue"])});
zoneSpecification
Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出行为等。
abstract class ZoneSpecification {
...
const factory ZoneSpecification(
{HandleUncaughtErrorHandler? handleUncaughtError,
RunHandler? run,
RunUnaryHandler? runUnary,
RunBinaryHandler? runBinary,
RegisterCallbackHandler? registerCallback,
RegisterUnaryCallbackHandler? registerUnaryCallback,
RegisterBinaryCallbackHandler? registerBinaryCallback,
ErrorCallbackHandler? errorCallback,
ScheduleMicrotaskHandler? scheduleMicrotask,
CreateTimerHandler? createTimer,
CreatePeriodicTimerHandler? createPeriodicTimer,
PrintHandler? print,
ForkHandler? fork}) = _ZoneSpecification;
...
}
修改 print 行为
比方说在一个 Zone 里,我们想修改它的 print 行为,就可以这样做:
main() {
Zone parentZone = Zone.current.fork(specification: new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, "Intercepted: $line");
// 还可以在这里把打印写入文件,发送给服务器
}));
parentZone.run(() => print("hello"));
print("hello");
}
打印结果:
Intercepted: hello
hello
可以看到当我们parentZone
运行起来后,print 的行为就已经被我们修改了。
由于最后面的 print 不在同一个 Zone 里,所以这个 print 不会发生改变。
修改 run 的行为
假如我们想在进入run()
的时候做一些事情,就可以:
main() {
Zone parentZone = Zone.current.fork(
specification: new ZoneSpecification(
run: <R>(self, parent, zone, f) {
print("Enter run");
return parent.run(zone, f);
}));
parentZone.run(() => print("hello"));
}
打印结果:
Enter run
hello
修改注册回调
main() {
Zone.root;
Zone parentZone = Zone.current.fork(specification:
new ZoneSpecification(registerCallback: <R>(self, parent, zone, f) {
// 调用我们实际注册的回调函数,第一次这里进来的 f 是 start(),第二次进来则是 end()
f();
return f;
}));
parentZone.run(() {
Zone.current.registerCallback(() => start());
print("hello");
Zone.current.registerCallback(() => end());
});
}
void start() {
print("start");
}
void end() {
print("end");
}
打印结果:
start
hello
end
onError
虽然runZoned()
方法有onError
的回调,但官方新版本里推荐是用runZonedGuarded()
来代替:
runZonedGuarded(() {
throw "null";
}, (Object error, StackTrace stack) {
print("error = ${error.toString()}");
});
打印结果
error = null
也就是说在Zone
里那些没有被我们捕获的异常,都会走到onError
回调里。
那么如果这个Zone
的specification
里实现了handleUncaughtError
或者是实现了onError
回调,那么这个 Zone
就变成了一个error-zone
。
那么error-zone
里发生的异常是不会跨越边界的,例如:
var future = new Future.value(499);
var future2 = future.then((_) {throw "future error";});
runZonedGuarded(() {
var future3 = future2.catchError((e) {
print(e.toString()); // 不会打印
});
}, (Object error, StackTrace stack) {
print(" error = ${error.toString()}"); // 不会打印
});
future
函数在error-zone
的外面定义,并定义了执行完毕后会抛出异常,当在error-zone
里调用的时候,此时这个异常就无法被error-zone
捕获了,因为已经超出了它的边界,解决的做法就是在定义future
函数那里再套一个Zone
,这样这个错误就会被外面的error-zone
捕获了:
var future = new Future.value(499);
runZonedGuarded(() {
var future2 = future.then((_) {
throw "future error";
});
runZonedGuarded(() {
var future3 = future2.catchError((e) {
print(e.toString());
});
}, (Object error, StackTrace stack) {
print(" error = ${error.toString()}");
});
}, (Object error, StackTrace stack) {
print("out error = ${error.toString()}");
});
输出结果:
out error = future error