Dart单线程模型和Flutter异常捕获

前言

Dart单线程模型

基本流程就是,在执行完main()后,消息循环机制便启动了。微任务队列的优先级比较高,等到微任务队列的事件执行完就去执行事件队列,事件队列执行完就会退出程序。微任务队列的目的就是在当前事件处理完之后下个事件开始之前可以执行一些其他事务。

但是,在任务执行的过程中也可以插入新的微任务和事件任务,就会一直循环执行下去,不会退出。

在这里插入图片描述

  • event queue: 包含了所有的外部事件:I/O,鼠标点击,绘制,定时器,Dart isolate 中的消息等等。自于 Dart 和系统的事件
  • microtask queue:事件处理代码有时需要在当前 event 之后,且在下一个 event 之前做一些任务。仅仅包含了来自于 Dart 内部的事件。

虽然你可以事先知道 task 执行的顺序,但是,你无法知道 event loop 什么时候从队列中取出任务。Dart 的事件处理系统是基于一个单线程循环模型,而不是基于时间系统。举个栗子,当你创建一个延时任务,事件在一个你指定的时间入队。然而,在它前面的事件没有被处理完,它无法被处理。

加入队列

  1. 使用Future 类,创建一个事件到 事件队列event queue尾部。
  2. 使用scheduleMicrotask() 或Future.microtask方法,添加一个事件到 microtask queue 的尾部。

尽可能的使用 Future,即,使用 event queue。从而减轻 microtask queue 对 event queue 的影响。
 如果一个任务(task),必须在所有 event queue 中事件被处理之前完成,通常你应该立即执行它,如果你不能立即执行它,就需要使用 scheduleMicrotask(),将事件添加到 microtask queue 中。目前在使用中并没有发现有什么必须要立刻执行的。

Future.delayed

new Future.delayed(const Duration(seconds:1), () {
  // ...code goes here...
});

上例子中本意是希望 1 秒钟之后添加任务到队列,但是任务可能并不会马上自行,除非当前的 main isolate 是 idle 状态,而且 microtask 队列是空的,而当前任务在 event queue 的最前面。

查看delayed的源码注释,当时间小于等于0,那么在下一次事件循环迭代,所有的微任务全部执行完毕,就会立刻执行这个事件。

总结下关于 Future 这些有趣的结论:

  1. then()后面的方法会在 Future 完成之后立即执行。(这个方法并不会进入 event queue,而仅仅是直接被调用)
  2. 如果 Future 已经执行完毕了,此时再调用 then() ,then() 中的方法会被封装成一个 microtask,进入 microtask queue 中(还不是很理解)
  3. Future 和 Future.delayed() 并不是立即构造完成,计时结束再添加这个事件到 event queue 中
  4. Future.value() 的构造出来的任务任务是一个 microtask ,这点和 第2条 很像
  5. Future.sync() 同样也构造出来了一个 microtask,同时,它的函数参数立即执行,这点跟 #2 类似

验证

Question1

void main() {
	eventQueueQuestion1();
}
void eventQueueQuestion1() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 2'));

  Future.delayed(Duration(seconds: 1), () => print('future #1 (delayed)'));
  Future(() => print('future #2 of 3'));
  Future(() => print('future #3 of 3'));

  scheduleMicrotask(() => print('microtask #2 of 2'));

  Future.value("我是value").then((value) => print('future.value #1 of 4 $value'));

  print('main #2 of 2');

}

结果

main #1 of 2
main #2 of 2
microtask #1 of 2
microtask #2 of 2
future.value #1 of 4 我是value
future #2 of 3
future #3 of 3
future #1 (delayed)

示例中代码的执行有三个批次:

  1. 在 main() 方法中的代码
  2. 在 microtask queue 中的任务(scheduleMicrotask())
  3. 在 event queue 中的任务(new Future() or new Future.delay())

Question2

void eventQueueQuestion2() {
  print('main #1 of 2');
  scheduleMicrotask(() => print('microtask #1 of 3'));

  Future.delayed(Duration(seconds: 1), () => print('future #1 (delayed)'));

  Future(() => print('future #2 of 4'))
      .then((_) => print('future #2a'))
      .then((_) {
    print('future #2b');
    scheduleMicrotask(() => print('microtask #0 (from future #2b)'));
  }).then((_) => print('future #2c'));

  scheduleMicrotask(() => print('microtask #2 of 3'));

  Future(() => print('future #3 of 4'))
      .then((_) => Future(() => print('future #3a (a new future)')))
      .then((_) => print('future #3b'));

  Future(() => print('future #4 of 4'));

  scheduleMicrotask(() => print('microtask #3 of 3'));

  print('main #2 of 2');
}

结果

main #1 of 2
main #2 of 2
microtask #1 of 3
microtask #2 of 3
microtask #3 of 3
future #2 of 4
future #2a
future #2b
future #2c
microtask #0 (from future #2b)
future #3 of 4
future #4 of 4
future #3a (a new future)
future #3b
future #1 (delayed)

在这里插入图片描述

  • 在 future 3 的 then() 回调中,调用了 new Future(),这将创建一个新的任务(#3a),然后添加到 event queue 的末端
  • 上述所有的 then() 回调,都是在 Future 执行完成后立即执行。因此,future 2,2a,2b,2c 一口气执行完了。相似的,3a,3b 也是一样。
  • 如果改变 3a 的代码,将 then(() => new Future(…)) 改为 then(() {new Future(…); }),那么,”future #3b” 会出现的更早些,会替代到 #3a 的位置。原因是回调返回了一个 Future,这个 Future 会和 then() 返回的 Future 链(chain) 在一起,同时执行完。

总结

主要概念:

  1. Dart app 中的 event loop 执行的任务来自于两个队列: event queue 和 microtask queue
  2. 两个队列接收的事件包括:Dart 事件(future、timer、isolate message 等)和系统事件(用户操作,IO 等)
  3. 目前,microtask queue 中主要处理来自 Dart 的事件
  4. event loop 会先去执行完 microtask queue 中的所有任务,再去处理 event queue 中的任务
  5. 一旦两个队列都空了,app 可以退出了(是否退出取决于宿主,比如浏览器)
  6. main() 方法和 所有 microtask queue ,event queue 中的任务均运行在 Dart 的 isolate 中。

当你在安排任务的时候,需要遵循如下原则:

  1. 尽可能的使用 event queue(即,使用 Future 和 Future.delayed())
  2. 使用 Future 的 then() 和 whenComplete() 来处理任务顺序
  3. 为了避免 event loop 阻塞,尽量避免使用 microtask queue
  4. 为了使得 app 尽可能快的响应,避免在 queue 中运行计算密集型任务
  5. 在另外的 isolate 或者 worker 中运行计算密集型任务

异常捕获

每个任务都是独立的,当某个任务发生异常,并不会导致整个程序崩溃,只是从当前任务退出,后续的代码不再运行,也就不会影响其他任务的执行。

我们需要捕获哪些异常:

  1. 同步错误
  2. 异步错误
  3. Framework异常

捕获同步错误——try/catch

同步的错误,直接使用try/catch捕获错误。

/// 抛出同步错误
  /// 同步异常可以通过try/catch捕获
  void _onClickThrowSyncError() {
    try {
      print("发生一个dart 同步异常");
      throw StateError('发生一个dart 同步异常');
    } catch (e) {
      print("打印错误 $e");
    }
}

运行结果,说明异常被捕获到。

发生一个dart 同步异常
打印错误 Bad state: 发生一个dart 同步异常 // Bad state:是StateError内部打印的

捕获异步错误——Zone

对于一个async的函数、Future实现的函数中抛出的错误,直接使用try/catch是捕获不到的,try/catch用在同步方法中捕获异常。
可以使用Future中提供的catchError方法进行,比如

Future.delayed(Duration(seconds: 1)).then((e) {
      print('异步异常发生之前 >>>>>>>>>>>');
      throw StateError('发生一个dart 异步异常');
    }).catchError((e) {
      print('异步错误 $e'); 
    });

运行结果,可以看到在catchError中捕获了错误

异步异常发生之前 >>>>>>>>>>>
异步错误 Bad state: 发生一个dart 异步异常 // Bad state:是StateError内部打印的

但try/catch无法捕获这种异常错误

try {
      Future.delayed(Duration(seconds: 1)).then((e) {
        print('异步异常发生之前 >>>>>>>>>>>');
        throw StateError('发生一个dart 异步异常');
      });
    } catch (e) {
      print("这是不会执行的. ");
    }

运行结果,可以看到catch中的代码并没有执行。

异步异常发生之前 >>>>>>>>>>>

这个时候就需要借助另一个工具,叫zone。

关于zoned

简单说,就是一个独立的运行空间,不同的空间之间是隔离的。这个空间可以捕获、拦截或修改一些代码行为等。

使用Zoned

runZonedGuarded(() {
      Future.delayed(Duration(seconds: 1)).then((e) {
        print('异步异常发生之前 >>>>>>>>>>>');
        throw StateError('发生一个dart 异步异常');
      });
    }, (Object error, StackTrace stack) {
      print('zone捕获到了同步异常:========');
      print('$error $stack');
    });

运行结果,可以看到在zoned包裹的空间,能正常捕获到异步错误。

异步异常发生之前 >>>>>>>>>>>
zone捕获到了同步异常:========
/// error 和 stack 太长了忽略

由此,为了捕获整个app的异步错误。在整个runApp外层包裹一个Zoned空间,这样在程序运行就能捕获到所有的异步错误,相当于是一个全局Zoned。

runZonedGuarded(
    () {
      runApp(const MyApp());
    },
    (Object error, StackTrace stackTrace) {
      // print("runZonedGuarded onError 打印错误 $error");
      print("runZonedGuarded onError打印错误\n $error $stackTrace");
    },
  );
try {
      Future.delayed(Duration(seconds: 1)).then((e) {
        print('异步异常发生之前 >>>>>>>>>>>');
        throw StateError('发生一个dart 异步异常');
      });
    } catch (e) {
      print("这是不会执行的. ");
    }

运行结果,可以看到在app页面中发生的异步错误,被全局的Zoned捕获。

异步异常发生之前 >>>>>>>>>>>
runZonedGuarded onError打印错误 
Bad state: 发生一个dart 异步异常

捕获Framework异常——FlutterError.onError

Framework 异常,就是 Flutter 框架引发的异常,大部分是Flutter底层出现错误。最常见的就是使用布局不当,会弹出一个全屏红色错误。
在这里插入图片描述
查看源码…/flutter/lib/src/widgets/framework.dart,其中在重新执行Build的方法中,对错误进行了catch操作,捕获了错误,然后通过ErrorWidget.builder展示出来,也就是上面看到的红色背景(debug是红色并且显示错误内容,release是灰色背景无法展示具体内容,也不会显示错误信息)

  @override
  void performRebuild() {
    ...
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
      _child = updateChild(null, built, slot);
    }
    ...
  }

我们就可以重写ErrorWidget.builder,自己定义布局。

ErrorWidget.builder = (FlutterErrorDetails details) {
      return Container(
        color: Colors.white,
        child: SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text(
              details.toString(),
              style: TextStyle(
                  fontSize: 10,
                  color: Colors.blueAccent),
            ),
          ),
        ),
      );
    }

在build中抛出一个错误,强制将int转换成string类型

@override
  Widget build(BuildContext context) {
    if(errorSwitch){
      String name = "123";
      name = 12 as String;
    }
    ...
  }

在这里插入图片描述
注意:
在details.toString,在release会返回空,但对象中的exception和stack有数据,所以需要取对象中的exception和stack来展示。

但这只是在界面上显示错误,如果要捕获错误信息并上报,为了捕获这种Framework抛出的错误,可以重写官方提供的FlutterError.onError的方法,相关说明可以看官方关于FlutterError.onError的介绍
直接在全局定义,重写FlutterError.onError的方法:

FlutterError.onError = (FlutterErrorDetails details) async {
      print("进入FlutterError.onError");

      /// debug模式
      if (ExceptionReportUtil.isInDebugMode) {
        print("FlutterError.onError framework出错,打印到控制台");
        /// 打印到控制台
        FlutterError.dumpErrorToConsole(details);

        /// release模式
      } else {
        print("FlutterError.onError中转发到zone");
        /// 转发到zone
        Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.current);
      }
    };

在这里插入图片描述
这样就能看到,在Framework抛出错误的时候,会回调到FlutterError.onError中。
为什么要在Release环境中,将错误转发到Zone,是为了统一错误入口。

/// 转发到zone
        Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.current);

总结

  1. 捕获同步错误——try/catch
  2. 捕获异步错误——Zone
  3. 捕获Framework异常——FlutterError.onError
    以上所有错误都转发到Zone,统一错误入口,进行上报。
void main() {
  /// 初始化Exception 捕获配置

  runZonedGuarded(
    () {
      /// 记得这里也要调用一次,才能正确回调FlutterError.onError
      WidgetsFlutterBinding.ensureInitialized();

      FlutterError.onError = (FlutterErrorDetails details) async {
        print("进入FlutterError.onError");

        /// debug模式
        if (ExceptionReportUtil.isInDebugMode) {
          print("FlutterError.onError framework出错,打印到控制台");
          /// 打印到控制台
          FlutterError.dumpErrorToConsole(details);
          Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.current);

          /// release模式
        } else {
          print("FlutterError.onError中转发到zone");
          /// 转发到zone
          Zone.current.handleUncaughtError(details.exception, details.stack ?? StackTrace.current);
        }
      };

      ErrorWidget.builder = (FlutterErrorDetails details) {
        return Container(
          color: Colors.white,
          child: SingleChildScrollView(
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Text(
                details.toString(),
                style: TextStyle(
                    fontSize: 10,
                    color: Colors.blueAccent),
              ),
            ),
          ),
        );
      };

      runApp(const MyApp());
    },
    (Object error, StackTrace stackTrace) {
      // print("runZonedGuarded onError 打印错误 $error");
      print("runZonedGuarded onError打印错误\n $error $stackTrace");
      /// 在这里进行统一错误处理!!!!
    },
  );
}

遇到问题

为什么触发了一个framework错误,FlutterError.onError没有执行?

  1. flutter官方issues有提到,debug模式下会被idea的日志覆盖,release不会有这种情况。
  2. 要在WidgetsFlutterBinding.ensureInitialized();或runApp(const MyApp());之后重写FlutterError.onError,才能接收Framework错误。

原因:WidgetsFlutterBinding的ensureInitialized()简单说就是framework框架层和 Flutter engine引擎绑定在一起。本来这一步是在runApp(const MyApp());执行。由于提前就要用到Flutter功能,所以需要提前调用ensureInitialized()方法。

main函数是Dart程序的入口,runApp才算是flutter真正的入口。

FlutterError.onError = (FlutterErrorDetails details) 中的details.toString,在release会返回空.

在details.toString,在release会返回空,但对象中的exception和stack有数据,所以需要取对象中的exception和stack来展示。

补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值