- 为什么写这篇文章
- Flutter异常分类
- Flutter异常的捕获方式和处理
- 针对特定业务的异常捕获如何处理?
- 为什么flutter触发dart异常的时候不会崩溃
- 在程序中内部用沙盒保护时,为什么部分异常捕捉不到?
为什么写这篇文章
flutter异常处理整体来说还是比较简单的,主要就是zone(沙盒),try/catch,Future的异步异常捕获,写这篇文章的原因是在开发flutter 动态化的时候,需要针对动态化的widget做特殊处理,比如单个widget降级、页面降级且不能影响其他业务等等。 所以梳理下关于flutter exception的知识,留个记录。
flutter异常分类
- Native异常
- plugin异常,比如Android端java,Ios端objectc触发的异常,这些异常都会导致程序闪退,和native的其他异常处理方式是一样的,这些不在本文的讨论范围之内
- Dart异常
- Zone,概念等同于沙盒,是一段代码执行的环境,不同的沙盒之间是相互隔离的,Zone可以同时捕获同步异常和异步异常,主要捕获flutter framework和应用代码未捕获的异常,这里说下同步异常和异步异常的区别
- 同步异常:正常代码执行时发生的异常,可以被try/catch捕获,也可以被Zone捕获
- 异步异常:try/catch是捕获不了的,但可以被Future的catchError捕获,Zone也可以捕获
- Zone,概念等同于沙盒,是一段代码执行的环境,不同的沙盒之间是相互隔离的,Zone可以同时捕获同步异常和异步异常,主要捕获flutter framework和应用代码未捕获的异常,这里说下同步异常和异步异常的区别
-
flutter framework异常,是指framework在运行中触发的异常,这部分异常已经被framework捕获
-
widget build异常,其实也是属于flutter framework异常,由于发生在build阶段,framework在处理的时候为我们返回了ErrorWidget,我们在开发时接触最多的就是这类异常,回忆下红屏+黄色字体的报错吧~
看几个例子,分别是Zone,同步异常,异步异常
// 沙盒
runZonedGuarded<Future<Null>>(() async {
runApp(MyApp());
}, (error, stackTrace) async {
//这里可以捕捉同步异常,也可以捕捉异步异常
print("expectinTest zone error ${stackTrace.toString()}");
});
String c;
Future.value(1).then((value) {
// 异步异常,c是null
print("app async error ${c.length}");
}).catchError((error){
// 输出异常,这里是捕捉异步异常
print(error);
});
// 同步异常
try {
print("expectinTest app error ${c.length}");
} catch (e) {
print(e);
}
// 注意,这种情况try/catch是捕获不到的
try {
Future.value(1).then((value) {
// 异步异常,c是null
print("app async error ${c.length}");
});
} catch (e) {
print(e);
}
flutter异常的捕获方式和处理
主要看以下三点
- flutter framework 是怎么捕获异常的
- widget build异常是如何用ErrorWidget处理成的
- Zone 是如何捕获异常的
flutter framework 是怎么捕获异常的,widget build异常是如何用ErrorWidget处理成的
说到这个,就不得不说flutter的三棵树,widget-element-render,日常开发中我们接触到最多的是widget,而在widget中,StatefulWidget和StateLessWidget是我们最熟悉的,他们对应的element分别是StatefulElement和StatelessElement,而他们的共同祖先是ComponentElement,注意ComponentElement的performRebuild方法(文章里的源码只保留部分逻辑,其他不重要的都会删掉)
// 对于StatefulWidget和StateLessWidget,他们执行的build方法都是这里发起的
@override
void performRebuild() {
Widget built;
try {
// 这里执行的就是StatefulWidget和StateLessWidget的build方法
built = build();
// 这个debug保留着,之后会有一个比较有意思的小实验
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
_debugDoingBuild = false;
// 如果发生异常,就返回ErrorWidget
built = ErrorWidget.builder(
// 在_debugReportException方法里调用了FlutterError.reportError(details);
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
} finally {
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
}
}
// _debugReportException方法里调用了FlutterError.reportError上报异常
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;
}
针对以上源代码,我们发现了几个关键点,分别是build(),包裹在build()外的try/catch,ErrorWidget,FlutterError.reportError
- build(),日常编码中继承StatefulWidget和StateLessWidget的build方法就是这里发起调用的,很明显在调用build方法时,framework做了try/catch保护,这就是flutter framework的异常,当然保护远不止这里,但已经包含大多数错误了
- ErrorWidget,可以看到build方法发生异常时,系统会默认返回ErrorWidget,这个ErrorWidget就是我们日常开发中看到的红屏
- FlutterError.reportError,flutter framework异常都会通过这个方法上报,系统内的默认实现是把错误输出到控制台
flutter framework异常处理
FlutterError.reportError
看源码会发现,其默认实现就是个静态函数,最终会走到onError方法
/// _debugReportException调用了reportError方法
static void reportError(FlutterErrorDetails details) {
assert(details != null);
assert(details.exception != null);
if (onError != null)
onError(details);
}
//onError 默认实现是dumpErrorToConsole,dumpErrorToConsole就是把错误输出到控制台
static FlutterExceptionHandler onError = dumpErrorToConsole;
看完源码会发现,他们都是静态的,而且不是private的,那就好办了,自己实现个 FlutterExceptionHandler就行了,不过有一点要注意的是,赋值后应该要保留原有的onError,自己处理完异常后,也要把异常向上抛,类似于这样
final defaultOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
// 自己先处理异常
// 把异常向上透传,是否透传取决于业务
defaultOnError(details);
};
ErrorWidget
这个其实是LeafRenderObjectWidget,这里顺便提下,Widget也分多种,例如常见的布局Widget,如StatelessWidget,StatefullWidget,带有布局-渲染属性的RenderObjectWidget,而LeafRenderObjectWidget就是RenderObjectWidget的子类,相似与ErrorWidget还有RawImage,这是图片的实际渲染类。 ErrorWidget的作用就是把错误堆栈渲染到屏幕上,所以系统的默认实现就是在build这个关键点上做了try/catch的保护,并在catch的保护中返回ErrorWidget,那我们实际开发中怎么处理呢?
// 看源码会发现,实际调用的是ErrorWidget.builder
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
// ErrorWidgetBuilder builder 也是个static function
static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;
//_defaultErrorWidgetBuilder源码我不贴了,实现是构建返回了ErrorWidget
既然ErrorWidgetBuilder builder是个static function,那就好办了,自己实现个覆盖他就行了
var defaultErrorBuilder = ErrorWidget.builder;
ErrorWidget.builder = (FlutterErrorDetails details) {
Widget errorBuilder;
if (debug){
// debug 建议走系统的,方便排查问题
errorBuilder = defaultErrorBuilder(details);
} else{
// 线上环境 走业务兜底,避免影响线上用户
errorBuilder = buildErrorWidget(details);
}
return errorBuilder;
};
Zone 是如何捕获异常的
Zone能捕获代码中未处理的同步异常和异步异常,一般我们的写法都是把runApp包在里面,但有没有想过为什么flutter会推荐这样写,包裹一段其他的代码能不能行?
runZonedGuarded<Future<Null>>(() {
runApp(MyApp());
}, (error, stackTrace) {
print("expectinTest zone error ${stackTrace.toString()}");
});
接着看runZonedGuarded源码
@Since("2.8")
R runZonedGuarded<R>(R body(), void onError(Object error, StackTrace stack),
{Map zoneValues, ZoneSpecification zoneSpecification}) {
// 省略部分代码
try {
// 可以看到在实际运行的时候,用try/catch包了一层
return _runZoned<R>(body, zoneValues, zoneSpecification);
} catch (error, stackTrace) {
// 如果触发异常,执行onError回调,这里就是同步异常
onError(error, stackTrace);
}
return null;
}
通过源码可以看到在真实执行runZone时,还是用try/catch包了一层,并在触发同步异常后,执行onError回调,这是runZonedGuarded能捕获同步异常的原因。那异步异常是怎么捕获的呢?还得看Future源码。
// 这是上面的例子,运行后这个异常可以被Zone捕获到
Future.value(1).then((value) {
// 异步异常,c是null
print("app async error ${c.length}");
});
// 异步异常的回调代码在future_impl里
static void _propagateToListeners(_Future source, _FutureListener listeners) {
while (true) {
assert(source._isComplete);
bool hasError = source._hasError;
if (listeners == null) {
// 看这里
if (hasError) {
AsyncError asyncError = source._error;
// 如果有错误,调用_Future的zone的handleUncaughtError方法,最终会回调到onError方法
source._zone
.handleUncaughtError(asyncError.error, asyncError.stackTrace);
}
return;
}
// 省略部分代码
}
}
// 那_Future的zone是怎么来的? 其实是初始化的时候就赋值了
_Future() : _zone = Zone.current;
//也可以指定zone
_Future.zoneValue(T value, this._zone) {
_setValue(value);
}
思考题:Zone保护一段其他代码,如果这段代码里有一段异步代码,并且这部分异步代码抛出错误,Zone的异常捕获还能生效吗?这个问题放在问题6解答。 至此,问题1-3都回答完了。
特定业务的异常如何处理?
最近在做flutter动态化相关的需求,业务上有这样一个场景,需要hook某个特定Widget的build异常,让其不要影响到主业务的降级和错误上报。
解决方式是自定义ErrorWidgetBuilder builder,在build的回调中通过判断错误的堆栈,context等信息,如果需要hook,就返回自定义ErrorWidget,如果不需要,抛给上层处理
Future<void> _setErrorWidgetBuilder() async {
var defaultErrorBuilder = ErrorWidget.builder;
ErrorWidget.builder = (FlutterErrorDetails details) {
if (buildErrorWidget == null) {
return defaultErrorBuilder(details);
}
// 如果不是dynamic异常,直接抛给默认的处理
if (!checkIfSpecialException(details)) {
print("expectinTest normal build error");
return defaultErrorBuilder(details);
}
Widget errorBuilder;
errorBuilder = buildErrorWidget();// 构建自己错误的异常
return errorBuilder;
};
}
Set<String> _hookClassNameSet = Set<String>..add("a")..add("b");
bool checkIfHookException(FlutterErrorDetails details) {
if (isEmpty(_hookClassNameSet)) return false;
if (details == null) {
return false;
}
// 判断上下文是否包含特殊类名
if (details.context != null) {
bool isHookError = false;
_hookClassNameSet.forEach((element) {
if (details?.context?.toString()?.contains(element) ?? false) {
isHookError = true;
}
});
if (isHookError) return true;
}
// 把堆栈处理成List,循环验证是否包含特殊类名
Iterable<String> lines =
details?.stack?.toString()?.trimRight()?.split('\n') ?? [];
if (isEmpty(lines)) {
return false;
}
final List<StackFrame> parsedFrames =
StackFrame.fromStackString(lines.join('\n'));
for (int index = 0; index < parsedFrames.length; index += 1) {
final StackFrame frame = parsedFrames[index];
final String className = '${frame.className}';
if (_hookClassNameSet.contains(className)) {
return true;
}
}
return false;
}
为什么flutter触发异常的时候不会崩溃?
这个和flutter的消息循环机制有关,任务分两种,一个是微任务microtask,一个是事件event,他们有自己的队列,每个任务是相互独立的,一旦某个任务触发异常,也就是导致这个任务后续代码无法执行,并不会影响其他任务执行
在程序中内部用沙盒保护时,为什么部分异常捕捉不到?
这问题比较有意思,假设有以下场景,针对某个widget的build函数单独起一个Zone,但在build里运行Future并抛错时,发现这个Zone无法捕获这个异常错误。原因还是要看runZone的这段源码
String c;
runZonedGuarded<Future<Null>>(() async {
Future.value(1).then((value) {
// 异步异常
print("app async error ${c.length}");
});
}, (error, stackTrace) async {
print("expectinTest zone error ${stackTrace.toString()}");
});
try {
return _runZoned<R>(body, zoneValues, zoneSpecification);
} catch (error, stackTrace) {
onError(error, stackTrace);
}
/// Runs [body] in a new zone based on [zoneValues] and [specification].
R _runZoned<R>(R body(), Map zoneValues, ZoneSpecification specification) =>
Zone.current
.fork(specification: specification, zoneValues: zoneValues)
.run<R>(body)
可以看到保护期只是在 _runZoned运行期间,也就是第一个参数body的执行范围内,而我们在针对特殊widget保护build的时候,build马上就执行完了,意味着保护就结束了,所以异步的异常也是同理,当前的自定义zone已经的保护期已经结束了,所以捕获不到。 这也是为什么我们看到的例子 runZone都是运行在main函数这里的原因。因为runApp()其实开启了Flutter主线程的运行,如果用户不退出APP,主线程会一直运行