开发者

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

开发者 https://www.devze.com 2023-01-19 10:20 出处:网络 作者: 开中断
目录1. 认识Zone1.1 ZoneValues1.2 ZoneSpecification1.3 通过runZoned快速创建Zone2. 异步基本原理和异常捕获3. HandleUncaughtErrorHandler1. 认识Zone
目录
  • 1. 认识Zone
    • 1.1 ZoneValues
    • 1.2 ZoneSpecification
    • 1.3 通过runZoned快速创建Zone
  • 2. 异步基本原理和异常捕获
    • 3. HandleUncaughtErrorHandler

      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中运行的规则,一旦我们重写了ZonjseSpecification的一些方法回调,之前的一些功能可能会消失。

      这种基于配置对象的扩展方法和基于继承的子类的重写是不一样的,该方法具有更强的扩展性,但是在类似于特性保留的机制上就明显不如继承来的方便,一旦重写某个方法,该方法原有的特性需要重新实现一遍,否则原有的功能会消失。

      如果你只重写了其中的一个方法,那么其他方法不会被覆盖,依然采用默认配置。

      ZoneSpecification的构造方法中,包含非常多的参数,其中绝大多数都是以回Callback形式出现,首先来看看run系列的方法:

      RunHandler? run,
      RunUnaryHandler? runUnary,
      RunBinaryHandler? runBinary,
      

      其实这三个方法的区别在于参数,我们看看RunHandlerRunUnaryHandlerRunBinary开发者_C教程Handler的具体定义:

      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。

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

      1.3 通过runZoned快速创建Zone

      Dart提供了runZoned方法,支持Zone的快速创建:

      R runZoned<R>(R bo编程客栈dy(),
          {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的单线程模型php和事件循环机制。

      来看看这段简单的代码:

      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 asyncFunwww.devze.comction() 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中看看这两种代码抛出异常时的执行栈:

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

      左侧是一种方法的执行栈,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()),
            zoneS编程pecification: 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异常处理的资料请关注我们其它相关文章!

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号