Flutter Zone异常处理方法及基本原理

1 认识ZoneZone像一个沙盒,是我们代码执行的一个环境。 我们的main函数默认就运行在Root Zone当中。 子Zone的构造有点像Linux中的进

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,

其实这三个方法的区别在于参数,我们看看RunHandlerRunUnaryHandlerRunBinaryHandler的具体定义:

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);

不难发现,三者除了固定的:selfparentzone之外,区别就在于

UnaryHandlerBinaryHandler提供了分别提供了一个参数、两个参数的选项。这个参数的作用是提供给另外一个参数: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(() {
 &nbsp;print("body internal Zone:" + Zone.current.hashCode.toString());
 &nbsp;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异常处理的资料请关注好代码网其它相关文章!