Dart线程与Flutter异常捕获

1,089 阅读5分钟

本篇文章有2个目的:

  • 介绍Dart单线程的运行模式
  • Flutter项目中如何收集崩溃信息和日志


单线程模型:

Dart单线程queue分类

Dart 在单线程中是以消息循环机制来运行的,其中包含两个任务队列,

一个是“微任务队列” microtask queue,

另一个叫做“事件队列” event queue。

使用Future类,可以将任务加入到Event Queue的队尾

使用scheduleMicrotask函数,将任务加入到Microtask Queue队尾

Microtask Queue存在的意义是:希望通过这个Queue来处理稍晚一些的事情,但是在下一个消息到来之前需要处理完的事情。(画重点!)

当Event Looper正在处理Microtask Queue中的Event时候,Event Queue中的Event就停止了处理了,此时App不能绘制任何图形,不能处理任何鼠标点击,不能处理文件IO等等

从图中可以发现,微任务队列的执行优先级高于事件队列




Dart线程运行过程

现在我们来介绍一下Dart线程运行过程,

如上图中所示,入口函数 main() 执行完后,

消息循环机制便启动了。

首先会按照先进先出的顺序逐个执行微任务队列中的任务,

事件任务执行完毕后程序便会退出,

但是,在事件任务执行的过程中也可以插入新的微任务和事件任务,

在这种情况下,整个线程的执行过程便是一直在循环,不会退出,

而Flutter中,主线程的执行过程正是如此,永不终止。

在Dart中,所有的外部事件任务都在事件队列中,

如IO、计时器、点击、以及绘制事件等,







而微任务通常来源于Dart内部,并且微任务非常少,

之所以如此,是因为微任务队列优先级高,

如果微任务太多,执行时间总和就越久,事件队列任务的延迟也就越久,

对于GUI应用来说最直观的表现就是比较卡,所以必须得保证微任务队列不会太长。

值得注意的是,我们可以通过Future.microtask(…)方法向微任务队列插入一个任务。

在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出,

而直接导致的结果是当前任务的后续代码就不会被执行了,也就是说一个任务中的异常是不会影响其它任务执行的。

关于单线程中Future的一些使用:

  • Future中的then并没有创建新的Event丢到Event Queue中,而只是一个普通的Function Call,在FutureTask执行完后,立即开始执行
  • 当Future在then函数先已经执行完成了,则会创建一个task,将该task的添加到microtask queue中,并且该任务将会执行通过then传入的函数
  • Future只是创建了一个Event,将Event插入到了Event Queue的队尾
  • 使用Future.value构造函数的时候,就会和第二条一样,创建Task丢到microtask Queue中执行then传入的函数
  • Future.sync构造函数执行了它传入的函数之后,也会立即创建Task丢到microtask Queue中执行(这个很明显,不加入解释)

就是说Future.then方法里面并没有创建一个新的Event,只是一个简单的方法回调,Future(该方法只是创建一个Evnet并入队)在FutureTask(这个需要去Dart的文档去看了解一下),就会占用eventloop立即执行;

一般Future会带有很多then处理函数,then都视为任务的子任务。所以会创建新的task。而这个task一般会加入到microtask中(希望通过这个Queue来处理稍晚一些的事情,但是在下一个消息到来之前需要处理完的事情)

和上面的解析这个相呼应!就是解决下一个Future进入Event之前处理好这些事情。


Flutter异常捕获及上报

捕获Flutter框架抛出的异常

在我们日常开发中,也许写代码时候会写一些比较低级的bug

Flutter 框架为我们在很多关键的方法进行了异常捕获

通常这个时候调试机器上面就会红屏,弹出错误信息,那么这个错误信息是如何弹出来的呢?

下面这一段是弹出错误视图的源码

class _MyAppState extends State<MyApp> {
@overridevoid initState() {
super.initState();
// 注册页面,每个页面的名称在native端和flutter端需要一致
FlutterBoost.singleton.registerPageBuilders({
    'embeded': (pageName, params, _)=>EmbededFirstRouteWidget(),
    'first': (pageName, params, _) => FirstRouteWidget(),
    'firstFirst': (pageName, params, _) => FirstFirstRouteWidget(),
    'second': (pageName, params, _) => SecondRouteWidget(),
    'secondStateful': (pageName, params, _) => SecondStatefulRouteWidget(),
    'tab': (pageName, params, _) => TabRouteWidget(),
    'platformView': (pageName, params, _) => PlatformRouteWidget(),
    'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),
    ///可以在native层通过 getContainerParams 来传递参数
    'flutterPage': (pageName, params, _) {
        print("flutterPage params:$params");
        return FlutterRouteWidget(params:params);
       },
    });
FlutterBoost.singleton.addBoostNavigatorObserver(TestBoostNavigatorObserver());
}
@overrideWidget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Boost example',
        builder: FlutterBoost.init(postPush: _onRoutePushed),
        home: Container(color:Colors.white));
}
void _onRoutePushed(String pageName, String uniqueId, Map params, Route route, Future _) {}}

综上所述,想捕获Flutter抛出的异常,那么我们只需要在void mian(){}内实现FlutterError提供的onError方法

即可捕获Flutter为我们包装好的一些错误收集


void main() {
    FlutterError.onError = (FlutterErrorDetails details) {
        reportError(details);
    };
    ...
}


捕获其它异常

在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常

在Dart中,异常分两类:同步异常和异步异常,同步异常可以通过try/catch捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:

即Future中的Error使用try catch的方式是捕获不到的!!!



try{    
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
} catch (e){    
    print(e)
}


那么我们就捕获不到这些错误了吗?其实不然,

Dart语法中提供了runZoned()方法

Dart中有一个runZoned(...) 方法,可以给执行对象指定一个Zone。

Zone表示一个代码执行的环境范围,为了方便理解,读者可以将Zone类比为一个代码执行沙箱,

不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,

如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,

同时Zone也可以捕获所有未处理的异常。

来看下runZoned的定义


R runZoned<R>(R body(), {    
    Map zoneValues,    
    ZoneSpecification zoneSpecification,
    Function onError,
})


其中zoneValues是私有数据,可以使用kv的方式取出,通常我们在实际使用的时候传入了runApp()实例!!!即意味着捕获的范围在整个Flutter的app内部

zoneSpecification可以定义一些代码行为,如打印日志等

onError即可捕获所有沙箱范围内抛出的错误


void main() {    
runZoned(() => runApp(MyApp()),    
    zoneSpecification: ZoneSpecification(        
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {        
    parent.print(zone, "Intercepted: $line");    
},),    
onError: (Object obj, StackTrace stack) {
        var details = makeDetails(obj, stack);
        // 自定义error收集
        reportErrorAndLog(details);
    },    
);
}

在实际项目中的完整使用


综合以上两点,在一个完整的Flutter App内部应该有自己的崩溃收集策略。

其实现原理就是基于FlutterError.onError()和runZoned()

附上简单的实现代码


void collectLog(String line){
    ... //收集日志,如存储在本地等
}
void reportErrorAndLog(FlutterErrorDetails details){
    ... //上报错误和日志逻辑,通过服务端日志接口上报日志
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
    ...// 构建错误信息,根据FlutterErrorDetail组件自己的error模型及赋予基本特征信息
}
void main() {
    FlutterError.onError = (FlutterErrorDetails details) {
        reportErrorAndLog(details);
    };
    runZoned(() => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
        print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
        collectLog(line); // 收集日志
    },
    ),
    onError: (Object obj, StackTrace stack) {
        var details = makeDetails(obj, stack);
        reportErrorAndLog(details);
    },
);
}