Flutter 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中运行的规则,一旦我们重写了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异常处理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Flutter系列重学Container示例详解

    目录 一.Container 简介 二.示例 三.源码分析 一.Container 简介 flutter 开发中最核心的是用最少的组件(层次)完成功能开发:Container 前端的盒子模型实现,类似 div 标签,掌握其他组件前,深入学习理解 Container 的使用是必要的. Container是一个组合类容器,由DecoratedBox.ConstrainedBox.Transform.Padding.Align等组件组合的一个多功能容器,所以我们只需通过一个Container组件可以实

  • 替换so文件来动态替换Flutter代码实现详解

    目录 一.Flutter代码的启动起点 1.1 initTask对象 1.2 ResourceExtractor 1.3 FlutterJNI#loadLibrary 二.ensureInitializationComplete 2.1 ShellArgs 三.实践:自定义libapp.so的加载 3.1 flutterApplicationInfo和FlutterActivity#getShellArgs() 一.Flutter代码的启动起点 我们在多数的业务场景下,使用的都是FlutterA

  • 详解Flutter中key的正确使用方式

    目录 1.什么是key 2.key的更新原理 3.key的分类 GlobalKey LocalKey 总结 1.什么是key Widget中有个可选属性key,顾名思义,它是组件的标识符,当设置了key,组件更新时会根据新老组件的key是否相等来进行更新,可以提高更新效率.但一般我们不会去设置它,除非对某些具备状态且相同的组件进行添加.移除.或者排序时,就需要使用到key,不然就会出现一些莫名奇妙的问题. 例如下面的demo: import 'dart:math'; import 'packag

  • Flutter封装组动画混合动画AnimatedGroup示例详解

    目录 一.来源 二.AnimatedGroup使用示例: 三.AnimatedGroup源码 最后 一.来源 项目中遇到混合动画的情况,每次实现都需要生命一堆属性,让代码变得杂乱,难以维护. 参考 iOS 组动画 CAAimationGroup, 随花半天时间封装一个混合动画组件 AnimatedGroup. 此组件基于极简.高扩展.高适用的封装原则,基本满足当前项目开发. 二.AnimatedGroup使用示例: // // AnimatedGroupDemo.dart // flutter_

  • Flutter通过Container实现时间轴效果

    时间轴是前端UI经常用到的效果,先看下效果图: 时间轴的特点 1.在列表中的高度不确定,高度取决于右侧 item 的高度 2.时间轴通常在第一个 item 中的样式和其他 item 中不同. 实现 一.借助 Container 中 decoration 属性,设置左侧的 border,可以实现时间轴高度随着 item 变化效果 Center( child: Container( width: 100, height: 100, decoration: BoxDecoration( // 设置 B

  • 详解flutter中常用的container layout实例

    目录 简介 Container的使用 旋转Container Container中的BoxConstraints 总结 简介 在上一篇文章中,我们列举了flutter中的所有layout类,并且详细介绍了两个非常常用的layout:Row和Column. 掌握了上面两个基本的layout还是不够的,如果需要应付日常的layout使用,我们还需要掌握多一些layout组件.今天我们会介绍一个功能强大的layout:Container layout. Container的使用 Container是一

  • flutter Container容器实现圆角边框

    本文实例为大家分享了flutter Container容器实现圆角边框的具体代码,供大家参考,具体内容如下 在这里使用 Container 容器来实现圆角矩形边框效果 1 圆角矩形边框 Container( margin: EdgeInsets.only(left: 40, top: 40), //设置 child 居中 alignment: Alignment(0, 0), height: 50, width: 300, //边框设置 decoration: new BoxDecoration

  • jstorm源码解析之bolt异常处理方法

    问题 用过storm或者jstorm的都知道,如果在bolt代码中发生了没被catch住的异常,所在worker进程会退出.本文就从源码角度分析一下具体设计,其实并不是"有异常然后进程崩了"这么简单. 实质 我们先看BasicBoltExecutor的源码: public void execute(Tuple input) { _collector.setContext(input); try { _bolt.execute(input, _collector); _collector

  • php中异常处理方法小结

    本文实例总结了php中异常处理方法.分享给大家供大家参考.具体分析如下: 当异常被触发时,通常会发生:在PHP5中添加了类似于其它语言的错误异常处理模块.在 PHP代码中所产生的异常可被 throw语句抛出并被 catch 语句捕获.需要进行异常处理的代码都必须放入 try 代码块内,以便捕获可能存在的异常.每一个 try 至少要有一个与之对应的 catch. 使用多个 catch 可以捕获不同的类所产生的异常,当 try 代码块不再抛出异常或者找不到 catch 能匹配所抛出的异常时,PHP

  • php异常处理方法实例汇总

    本文实例讲述了php异常处理方法.分享给大家供大家参考.具体如下: <?php $path = "D://in.txt"; try //检测异常 { file_open($path); } catch(Exception $e) //捕获异常 { echo $e->getMessage(); } function file_open($path) { if(!file_exists($path)) //如果文件无法找到,抛出异常对象 { throw new Exceptio

  • MySQL存储过程的异常处理方法

    本文实例讲述了MySQL存储过程的异常处理方法.分享给大家供大家参考.具体如下: mysql> mysql> delimiter $$ mysql> mysql> CREATE PROCEDURE myProc -> (p_first_name VARCHAR(30), -> p_last_name VARCHAR(30), -> p_city VARCHAR(30), -> p_description VARCHAR(30), -> OUT p_sq

  • Java一些常见的出错异常处理方法总结

    一些平时常见的错误及解决办法,我 是新手,每次遇到的错误都记录了下来. 1. 404错误 description The requested resource (/Struts2_0100_Introduction/hello.action) is not available. 先检查Manager Deployments,使之能打开(出现NullPointerException不能打开),主要是服务器在每次允许前备份,再修改了错误后不能及时更正.(如果是勾选的Backup,要改成delete)

  • 基于Java子线程中的异常处理方法(通用)

    在普通的单线程程序中,捕获异常只需要通过try ... catch ... finally ...代码块就可以了.那么,在并发情况下,比如在父线程中启动了子线程,如何在父线程中捕获来自子线程的异常,从而进行相应的处理呢? 常见错误 也许有人会觉得,很简单嘛,直接在父线程启动子线程的地方try ... catch一把就可以了,其实这是不对的. 原因分析 让我们回忆一下Runnable接口的run方法的完整签名,因为没有标识throws语句,所以方法是不会抛出checked异常的.至于Runtime

  • 基于PHP7错误处理与异常处理方法(详解)

    PHP7错误处理 PHP 7 改变了大多数错误的报告方式.不同于传统(PHP 5)的错误报告机制,现在大多数错误被作为 Error 异常抛出. 这种 Error 异常可以像 Exception 异常一样被第一个匹配的 try / catch 块所捕获.如果没有匹配的 catch 块,则调用异常处理函数(事先通过 set_exception_handler() 注册)进行处理. 如果尚未注册异常处理函数,则按照传统方式处理:被报告为一个致命错误(Fatal Error). Error 类并非继承自

  • spring boot 全局异常处理方法汇总

    这篇文章主要介绍了spring boot 全局异常处理方法汇总,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 import cn.sisyphe.framework.web.exception.DataException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.co

  • laravel框架 api自定义全局异常处理方法

    api返回实现 $result = User::find($id); if(empty($result)){ throw new ApiException('获取失败'); } else{ return json_decode($result); } api返回信息 { "msg": "", "data": "获取失败", "status": 0 } 1,添加异常类 namespace App\Except

  • Spring Cloud Hystrix异常处理方法详解

    这篇文章主要介绍了Spring Cloud Hystrix异常处理方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 在调用服务执行HsytrixCommand实现的run()方法抛出异常时,除HystrixBadRequestException之外,其他异常都会认为是Hystrix命令执行失败并触发服务降级处理逻辑. 异常处理 当Hystrix命令因为异常(除了HystrixBadRequestException异常)进入服务降级逻辑之后

随机推荐