Flutter线程模型/事件机制
在介绍Flutter异常捕捉原理之前,先说明一下Dart的模型。方便我们了解Dart代码的执行流程和获取一个合适的异常捕捉切入点。
我们知道在Java中,如果程序运行发生异常并且没有做捕获处理,程序会直接终止运行发生Crash。但这种情况在Dart中会有所不同。Dart不同于Java的多线程模型,Dart和JavaScript类似属于单线程模型。Dart的单线程模型是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列”microtask queue,另外一个叫“事件队列” event queue。其中微任务队列优先级高于事件队列。
下图是对Dart运行原理和事件机制的一个简单说明:
如图,main()函数(Dart程序的入口函数)执行完后,消息循环机制就会启动。main中的代码将最先执行,然后再执行微任务队列中的任务(按FIFO先进先出顺序),再次是事件队列中的任务。执行顺序:Main > MicroTask > EventQueue。在事件任务执行过程中又可以插入新的微任务和事件任务,所有任务执行完毕程序就会退出。
有意思的是在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出。而直接导致的结果是当前任务的后续代码不会被执行。也就是说一个任务中的异常是不会影响其它任务执行的。
Flutter 异常分类
Flutter dart代码异常(包含app 代码异常,和framework部分异常,和未处理的异步异常)
Flutter Engine 异常
Flutter Dart 代码异常
1. Dart 代码异常捕捉
Dart中也有try/catch/finally的存在,用于捕捉代码同步异常。
try {
result = await _methodChannel
.invokeMethod(METHOD_ADD_WALLET_START, {"cardNo": cardNo});
} catch(ignored) {
}
return result;
2. Framework异常捕捉
Flutter的框架本身也做了很多异常捕捉措施,例如著名的Flutter红屏异常(布局发生越界或者不符合规范的写法就会导致红屏),就是因为Flutter在框架中已经帮我们预埋了异常的捕获处理逻辑。这个我们可以通过Flutter的源码来了解其实现细节。
Flutter中万物皆Widget,无论是从StatefulWidget还是StatelessWidget中都能发现Widget在创建时都创建了对应的Element的。这里以StatefulWidget源码为例,发现其中会创建StatefulElement。
abstract class StatefulWidget extends Widget {
/// Initializes [key] for subclasses.
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
...
}
进入StatefulElement,发现StatefulElement继承于ComponentElement
class StatefulElement extends ComponentElement {
再次追踪到ComponentElement中,会在其**performRebuild()**方法中发现熟悉的try/catch结构。
void performRebuild() {
...
Widget built;
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
在这里我们发现发生异常时,flutter框架对异常进行了捕捉,并且创建了一个ErrorWidget来做进一步的处理。Flutter的红屏也就是从这里产生的。这里就是我们实现Flutter框架异常捕捉的切入点。 查看_debugReportException的源码来确定异常捕捉的具体实现方式。_debugReportException的源码如下:
FlutterErrorDetails _debugReportException(
DiagnosticsNode context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector,
}) {
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
FlutterError.reportError(details);
return details;
}
核心的是FlutterError.reportError(details);这一句。进入后发现:
/// Calls [onError] with the given details, unless it is null.
static void reportError(FlutterErrorDetails details) {
assert(details != null);
assert(details.exception != null);
if (onError != null)
onError(details);
}
}
继续跟踪这里的onError就会发现它是FlutterError的一个静态属性,有个名为dumpErrorToConsole的默认处理方法。
static FlutterExceptionHandler onError = dumpErrorToConsole;
所以我们只需要实现一个自定义的FlutterError.onError来处理异常就能实现Flutter框架异常的捕捉和上报。最终实现如下:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
3. dart的异步异常捕捉
刚才介绍的两种异常捕捉方式并不足以应对所有的flutter异常。例如下面的这种异常:
try {
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
} catch (e) {
print(e)
}
Dart有个Zone的概念,可以简单的理解为沙箱。不同的Zone相处独立,互不影响。借助于Zone就可以指定代码的执行环境,捕获、拦截或者修改代码行为。Flutter中有一个Zone.runZoned方法。
在runZoned方法的源码中我们又看到了熟悉的onError。在这里我们注入自定义的onError回调方法,用于捕获方法1、方法2中未能捕获处理的异常。 最终的实现如下:
runZoned(
() => MyApp(),
onError: (dynamic ex, StackTrace stack) {
reportError(ex, stack);
},
);
4. 最终实现
///flutter 应用入口
void main() {
// Flutter framework 异常捕获
FlutterError.onError = (FlutterErrorDetails details) {
bool isDebugMode = false;
assert(() {
isDebugMode = true;
return true;
}());
if (isDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
//profile,release两个模式下下捕捉异常信息
reportFrameworkError(details);
}
};
//其他类型异常
runZoned(
() => runAutoSizeApp(MyApp(), width: 375, height: 667),
onError: (dynamic ex, StackTrace stack) {
reportError(ex, stack);
},
);
}
注:针对debug环境下的异常不需要捕捉上报,我们任然让它走旧有的异常处理逻辑。
异常信息的上报
在处理异常信息上报之前我们先来看看捕捉到的Flutter日志格式。
MissingPluginException(No implementation found for method getCache on channel com.zhongan.beeline/ZACache)
#0 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319:7)
<asynchronous suspension>
#1 CachePlugin.getCache (package:ZABank/plugin/CachePlugin.dart:34:37)
#2 new HomePageViewModel (package:ZABank/home/bloc/HomePageViewModel.dart:158:17)
#3 HomePageViewModel.create (package:ZABank/home/bloc/HomePageViewModel.dart:138:23)
#4 _NewHomeTabState.build.<anonymous closure> (package:ZABank/home/NewHomeTab.dart:36:47)
#5 BuilderStateDelegate.initDelegate (package:provider/src/delegate_widget.dart:249:14)
#6 _DelegateWidgetState._initDelegate (package:provider/src/delegate_widget.dart:118:21)
#7 _DelegateWidgetState.initState (package:provider/src/delegate_widget.dart:110:5)
#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4355:58)
#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4201:5)
#10 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3194:14)
#11 MultiChildRender
从上面的flutter crash日志来看,我们可以将其简单的分为两部分:异常标题(异常类型和对应异常详情说明);异常堆栈信息。将这些信息上报到不同的异常监控平台需要做不同的处理
- 以Android端为例,如果上报平台为bugly。我们只需要将上面的异常日志分为异常标题和异常堆栈,通过methodchannel传递到native层,然后通过bugly接口直接上报即可。以下为bugly上报接口调用示例:
if (!CrashModule.hasInitialized()) return;
CrashReport.postException(4, excpetionType, excpetionMessage, stack, null);
-
如果我们的上报目标平台为firebase,那需要做更进一步的处理。因为firebase平台目前给出的接口没有bugly的灵活,上报时接口只认Java的Exception对象。因此为了将flutter异常上报到firebase平台我们需要对其进行解析转换,将其格式转换为Java的异常类型然后再上报到firebase。
flutter异常的标题没什么好说的,可以直接作为Java Exception的detailMessage字段。
flutter异常堆栈部分按行切割后,每一行日志还需要按规则进行拆解。
以下为拆解规则:
#1 CachePlugin.getCache (package:ZABank/plugin/CachePlugin.dart:34:37)
丢弃 | class | method | file | line | 丢弃 |
---|---|---|---|---|---|
#1 | CachePlugin | getCache | package:ZABank/plugin/CachePlugin.dart | 34 | 37 |
这些字段分别对应了Java中StackTraceElement类的几个字段。
*public final class StackTraceElement implements [java.io](http://java.io).Serializable {*
*// Normally initialized by VM (public constructor added in 1.5)*
*private String declaringClass;*
*private String methodName;*
*private String fileName;*
*private int lineNumber;*
最终我们就能在firebase中看见上报的flutter异常。