Flutter Zone异常处理方法及基本原理
作者:开中断
1. 认识Zone
Zone像一个沙盒,是我们代码执行的一个环境。
我们的main
函数默认就运行在Root Zone
当中。
子Zone的构造有点像Linux中的进程,它支持从当前的Zone中Fork出一个子Zone:
Zone myZone = Zone.current.fork(...)
对于Zone而言,它有两个构造函数:
- ZoneSpecification
- ZoneValues
ZoneSpecification:其实是Zone内部代码行为的一个提取,我们可以通过它来为Zone设置一些监听。
ZoneValues:Zone的变量,私有变量。
类似Linux 通过Fork创建的 myZone默认也具有源Zone的ZoneSpecification和ZoneValues。
1.1 ZoneValues
和Linux类似地,当Zone做Fork的时候,会将父Zone所持有的ZoneSpecification、ZoneValues会继承下来,可以直接使用。并且是支持追加的,secondZone在firstZone的基础之上,又追加了extra_values
属性,不会因为secondZone的ZoneValues就导致name属性被替换掉。
Zone firstZone = Zone.current .fork(specification: zoneSpecification, zoneValues: {"name": "bob"}); Zone secondZone = firstZone.fork(zoneValues: {"extra_values": 12345}); secondZone.run(() { print(secondZone["name"]); // bob print(secondZone["extra_values"]); // 12345 }
我们可以使用Zone.current
,访问当前的代码执行在哪一个Zone
当中,默认情况下,代码执行在Root Zone
当中,后续会根据需求分化出多个Zone
,也可以使用Zone.root
访问到RootZone的实例。
1.2 ZoneSpecification
和ZoneValues不同,ZoneValues支持追加不同的属性,而ZoneSpecification
只支持重写,并且RootZone
已经预设好了一系列的Zone中运行的规则,一旦我们重写了ZoneSpecification
的一些方法回调,之前的一些功能可能会消失。
这种基于配置对象
的扩展方法和基于继承
的子类的重写是不一样的,该方法具有更强的扩展性,但是在类似于特性保留的机制上就明显不如继承来的方便,一旦重写某个方法,该方法原有的特性需要重新实现一遍,否则原有的功能会消失。
如果你只重写了其中的一个方法,那么其他方法不会被覆盖,依然采用默认配置。
ZoneSpecification的构造方法中,包含非常多的参数,其中绝大多数都是以回Callback形式出现,首先来看看run
系列的方法:
RunHandler? run, RunUnaryHandler? runUnary, RunBinaryHandler? runBinary,
其实这三个方法的区别在于参数,我们看看RunHandler
、RunUnaryHandler
和RunBinaryHandler
的具体定义:
typedef RunHandler = R Function<R>( Zone self, ZoneDelegate parent, Zone zone, R Function() f); typedef RunUnaryHandler = R Function<R, T>( Zone self, ZoneDelegate parent, Zone zone, R Function(T arg) f, T arg); typedef RunBinaryHandler = R Function<R, T1, T2>(Zone self, ZoneDelegate parent, Zone zone, R Function(T1 arg1, T2 arg2) f, T1 arg1, T2 arg2);
不难发现,三者除了固定的:self
、parent
、zone
之外,区别就在于
UnaryHandler
和BinaryHandler
提供了分别提供了一个参数、两个参数的选项。这个参数的作用是提供给另外一个参数:f,类型是一个Function
,显然它是我们调用Zone.run
方法传进来的body
参数,以RunHandler
为例,我们对run
做出如下的定义:
Zone secondZone = firstZone.fork( zoneValues: {"extra_values": 12345}, specification: ZoneSpecification( run: <int>(self, parent, zone, f) { int output = f(); return output; }, ));
我们在外部调用secondZone.run(()=>...)
时,就可以在run方法的开始、结尾做一些其他的事情了:
secondZone.run(body);// 执行 run: <int>(self, parent, zone, f) { // 1. print("before"); int output = f();// 这里的f就是body,它是可执行的 print("after"); return output; // 2. },
直觉告诉我,1/2之间的代码应该是在Second Zone
中执行的,但是打印一下Zone.root
,我们发现实际上是在Root Zone
中执行的,二者的HashCode相同。
// 在body内部打印的 body internal Zone:195048515 // Root Zone:195048515 // first Zone:700091970 second Zone:707932504
大致上去跟了一下代码,发现默认的run方法的实现,被我们新编写的run
参数覆盖掉了,所以会导致本该在secondZone中执行的body结果在Root Zone
中执行。然后再run
参数的注释里,发现了这么一段话:
Since the root zone is the only zone that can modify the value of [current], custom zones intercepting run should always delegate to their parent zone. They may take actions before and after the call.
大致上的意思是:
因为Root Zone是唯一能够修改Zone.current
参数的Zone,所以自定义的Zone拦截run方法必须总是将方法交给它们的父Zone去代为处理。而run自己可以在run调用之前或者之后采取一些行动。
也就是说,我们不能直接return f();
,而要把f()
委托给parent
来执行,像这样:
secondZone.run(body);// 执行 run: <int>(self, parent, zone, f) { // 1.这里执行在Root Zone print("before"); Function output = parent.run(self, () { // 这里执行在second Zone return f(); }); print("after"); return output; // 2. },
委托之后,由Root Zone
去做统一的调度、Zone的切换。这样,我们再去打印一下执行的Zone,发现正常了,secondZone.run
方法(其实是被ZoneSpecification中的run指定的方法)的Zone仍然是Root Zone
,而我们传递过去的任务被执行在了self
之中,也就是SecondZone
当中,符合我们的预期:
current zone:692810917
body internal Zone:558922284
Root Zone:692810917
firstZone Zone:380051056
second Zone:558922284
额外地,可以牵出ZoneDelegate
是做什么的,它允许子Zone,访问父Zone的一些方法,与此同时保留自己额外的一些行为:绿框表示额外的行为,当Zone A
调用Zone B
的run时,它通常执行在调用者的Zone当中,也就是ZoneA。
1.3 通过runZoned快速创建Zone
Dart提供了runZoned方法,支持Zone的快速创建:
R runZoned<R>(R body(), {Map<Object?, Object?>? zoneValues, ZoneSpecification? zoneSpecification, @Deprecated("Use runZonedGuarded instead") Function? onError}) {
其中body、zoneValues、zoneSpecification都是老熟人了,关键在于它对于run方法的处理:
/// Runs [body] in a new zone based on [zoneValues] and [specification]. R _runZoned<R>(R body(), Map<Object?, Object?>? zoneValues, ZoneSpecification? specification) => Zone.current .fork(specification: specification, zoneValues: zoneValues) .run<R>(body);
如果我们不显式地传递一个ZoneSpecififation
进来,fork
时传进去的是null,自然不会导致Specification被我们重写,因此代码能按照Dart默认的实现方式,运行在一个新的、Fork出来的Zone当中(至少能看出不是Root Zone):
runZoned(() { print("body internal Zone:" + Zone.current.hashCode.toString()); print("Root Zone:" + Zone.root.hashCode.toString()); }); // 打印结果 body internal Zone:253994638 Root Zone:1004225004
但是如果你像之前手动fork一样,指定它的ZoneSpecification,又不把f委托给上层Zone处理,那么就会:
body internal Zone:44766141 Root Zone:44766141
2. 异步基本原理和异常捕获
默认大家已经知道什么事单线程模型,以及Future的执行机制了,Dart的单线程模型和事件循环机制。
来看看这段简单的代码:
void asyncFunction() { print('1'); Future((){ print('2'); }).then((e) { print('3'); }); print('4'); }
大家都知道,这段代码的输出的顺序是:1423,它的大致流程是:
print 1 创建一个Future,并扔到Event Queue末尾 print 4 // 从Event Queue中取出,并执行下一个消息...... 执行Future构造函数中的方法:-> print 2 print 2执行完成,即Future完成,回调它的then: -> print 3
我们为他加上await和async,并稍作改造,写成async、await的同步形式,同时删掉4
void asyncFunction() async { print('1'); await Future(() { print('2'); }); print('3'); print('4'); }
它的输出是:1234,他所做的是:
print 1; 创建一个Future@1,并扔到Event Queue末尾; // 从Event Queue中取出,并执行下一个消息...... 取出Future@1,立刻执行它构造中的方法: -> print 2; 并将之后的代码打包,重新放到Event Queue的末尾(这里一般会等待IO完成,之后就会去执行和这个回调) 执行完成之后,执行之后的代码: print 3; print 4;
今天我们不是讨论Async和Await的,就不再展开。
但是大家可以比较一下这两次调用,发现第二种和第一种相比,第二种调用的代码是会 “回来” 继续执行的,而第一种的Future创建不搭配await/async的就好比脱缰的野马,这种代码我们并不关心它的结果,自然也不要求代码在此await,执行起来就无法控制,但在Dart中我们也无法通过try/catch
捕获异常。
关键点在于:async + await是会回到异步阻塞的代码处(await处)执行的。既然回来了,那么try/catch
自然而然是能够继续监听是否有异常抛出的。
而第一种的Future,即使我们在外面包裹上了try/catch
,而Future的代码却是在未来的某个时间内,在Event Queue的末尾的某个位置解包执行的,上下文和try/catch
所在的代码并没什么关联,自然不能拦截到异常。我们可以从Stack Trace中看看这两种代码抛出异常时的执行栈:
左侧是一种方法的执行栈,throwExceptionFunction()
项相关的栈帧已经消失了,异常自然没有办法通过throwExceptionFunction()
中的try/catch
进行捕获。
问题就出在这了, 对于这种错误我们是否有办法去捕获呢?
答案仍然还是是今天的主题 : Zone。
3. HandleUncaughtErrorHandler
虽然异步代码的执行,可能会横跨多个Event,让代码前后的上下文失去联系,导致异常无法被正常捕获,但是它仍然在一个Zone之内。
就像仙剑奇侠传三中,李逍遥对景天说“邪剑仙(Exception)虽身处六界(Event)之外却是在道(Zone)之内”。
Zone提供了一些特殊的编程接口,让我们能够对当前这个Zone沙盒内的未捕获的异常进行集中处理。
它就是HandleUncaughtErrorHandler
。作为ZoneSpecification
的一个参数,它支持将Zone当中未被处理的错误统一归到这里进行处理(Dart和Java不一样,Dart的异常本身通常不会导致程序的退出),因此,常使用HandleUncaughtErrorHandler来做异常的统计、上报等等。
另外,因为Dart执行环境的单线程 + 事件队列机制本身,Dart的try/catch
对于异步代码是无法处理的,如下的代码异常会穿透(或者说根本不经过)try/catch后抛出,会在控制台中留下红色的报错。
// Zone.run(()=>throwExceptionFunctino()); void throwExceptionFunction() { try { Future.delayed(const Duration(seconds: 1)) .then((e) => throw("This is an Exception")); } catch (e) { print("an Exception has been Captured: ${e.toString()}"); } }
显然,异步的异常并没有被捕获:
Unhandled exception: This is an Exception #0 throwExceptionFunction.<anonymous closure> (file:///Users/rEd/IdeaProjects/dartProjs/zone/bin/zone.dart:140:22) #1 _rootRunUnary (dart:async/zone.dart:1434:47) #2 _CustomZone.runUnary (dart:async/zone.dart:1335:19) <asynchronous suspension>
但是我们改成这样呢?
void throwExceptionFunction() async{ try { await Future.delayed(const Duration(seconds: 1)) .then((e) => throw ("This is an Exception")); } catch (e) { print("an Exception has been Captured: ${e.toString()}"); } }
我们对异步的方法throwExceptionFunction()
加了await/async
关键字。我们会发现异常,又能被捕获了:
an Exception has been Captured: This is an Exception Process finished with exit code 0
其实上文已经提到了是异步时Dart代码上下文切换的原因,这里也不做过多的赘述了,我们像这样,将我们的App包裹在一个额外的Zone里面,并在它的HandleUncaughtErrorHandler
相关方法做如下定义:
void main() { runZoned(() => runApp(const MyExceptionApp()), zoneSpecification: ZoneSpecification( // print: (self, parent, zone, line) {}, handleUncaughtError: (Zone self, ZoneDelegate parent, Zone zone, Object error, StackTrace stackTrace) { // 同样将print代理给上层Zone,这样就可以在上层捕获到这些异常了。 parent.print(self," ### \n $stackTrace \n ### "); })); }
随便找个地方抛出个异常:
floatingActionButton: FloatingActionButton( onPressed: () => Future((){ throw ("ERROR!"); }), ),
我们可以发现,异常在此处被HandleUncaughtErrorHandler
集中捕获了。
或者我们也可以使用runZoned
自带的回调来处理,而不是去自己重写ZoneSpecification
:
// runZonedGuarded替换runZoned runZonedGuarded(() => runApp(const MyExceptionApp()), (Object error, StackTrace stack) { print('stack: $stack'); });
不过,我们去它内部看看,其实它还是HandleUncaughtErrorHandler
实现的。
注意:如果重写了ZoneSpecification的run相关的方法,可能会导致当前的Zone无法捕获到异常,就像1.中所说的那样,基于配置类的重写将原有特性覆盖掉了,导致当前代码并不一定在我们直觉认为的Zone中执行。
这需要编写者自己去解决这个问题,所以,如果没有特殊的需求,一般不给Zone传递ZoneSpecification选项,如果要传递,需要去实现它,以保证相关的功能特性可用。
以上就是Flutter Zone异常处理方法及基本原理的详细内容,更多关于Flutter Zone异常处理的资料请关注脚本之家其它相关文章!