Flutter 端异常捕获方式
Flutter中的异常分为 同步异常 和 异步异常,异常类型又分为:
-
RangeError ;dart异常
-
FlutterErrorDetails;页面渲染异常
-
MissingPluginException; 服务异常
-
DioError ;Dio请求异常
可由error.runtimeType.toString()获取异常类型;
通常情况可以使用try-catch来捕获同步异常,异步异常需要使用flutter的其它api来捕获,下面汇总了常用的异常捕获方式:
使用 try-catchf 捕获异常
与Java、OC语言类似,Dart中也提供了try-catch代码块,使用方式也比较简单:
try {
//抛出异常
throw Exception("This is a test exception");
}
catch (error,stack) {
//异常上报
}
Flutter 框架层 捕获异常
Flutter 框架为我们在很多关键的方法进行了异常捕获。这里举一个例子,当我们布局发生越界或不合规范时,Flutter就会自动弹出一个错误界面,这是因为Flutter已经在执行build方法时添加了异常捕获,最终的源码如下:
@override
void performRebuild() {
...
try {
//执行build方法
built = build();
} catch (e, stack) {
// 有异常时则弹出错误提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
可以看到,在发生异常时,Flutter默认的处理方式是弹一个ErrorWidget,但如果我们想自己捕获异常并上报到报警平台的话应该怎么做?我们进入_debugReportException()方法看看:
FlutterErrorDetails _debugReportException(
String 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方法上报的,继续跟踪:
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //调用了onError回调
}
我们发现onError是FlutterError的一个静态属性,它有一个默认的处理方法 dumpErrorToConsole,到这里就清晰了,如果我们想自己上报异常,只需要提供一个自定义的错误处理回调即可,如:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
这样我们就可以处理那些Flutter为我们捕获的异常了,接下来我们看看如何捕获其它异常。
另外:如果我们需要自定义错误页面,则需要重写 ErrorWidget.builder:
///自定义红屏异常
ErrorWidget.builder = (FlutterErrorDetails details) {
return MaterialApp(
title: 'Error Widget',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: Scaffold(
appBar: AppBar(
title: Text('Widget渲染异常!!!'),
),
body: getErrorWidget(details.toString()),
),
);
};
使用 Future API 捕获异常
Future API 提供了 catchError 方法用于捕获使用 Future 时抛出的异步异常:
Future.delayed(Duration(seconds: 1))
.then((e)=>print(e.toString()))
.catchError((){
//处理异常
}
使用 Zone 捕获未处理异常
Flutter 提供了 Zone.runZoned 方法(dart:async 包)用于捕获所有未处理的异常信息,也可以捕获、拦截或修改一些代码行为;
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
Function onError,
})
其中,zoneValues 是 Zone 的私有属性,zoneSpecification 是 Zone 的一些配置信息,可以自定义一些代码行为,onError 是 Zone 中未捕获异常的回调方法。
如果开发者提供了 onError 回调方法或者通过 ZoneSpecification.handleUncaughtError 指定了错误处理的回调方法,那么这个 Zone 将会变为一个 error zone,无论是同步环境还是异步环境,该 error zone 都会在出现未捕获的异常信息时调用:
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
var details=makeDetails(obj,stack);
reportError(details);
});
注意:
当你在debug模式运行带有异常代码块时,并不会走到onError代码块中,这是因为在debug模式下,flutter框架捕获了很多异常,并打印到控制台(有时在UI上以红色显示),没有重新抛出,所以你的代码捕获不到这些异常;解决办法是使用Release模式运行就可以了!
Flutter 异常收集最佳实践
目前异常信息收集分为:
自动收集
- 自动收集的含义是捕获全局代码未捕获的异常;
- flutter中使用runZoned的方式进行全局捕获;
手动收集
- 使用try-catch收集指定代码块的异常;
Flutter 异常捕获关键代码
红屏异常捕获
以下代码放在main.dart中,main()函数外
///创建错误widget body
Widget getErrorWidget(dynamic details) {
return Container(
color: Colors.white,
child: Center(
child: Text("出错啦!!!",style: TextStyle(color: Colors.black,fontSize: 26)),
)
);
}
以下代码放在main()函数中
///自定义红屏异常
ErrorWidget.builder = (FlutterErrorDetails details) {
ExceptionReporter.reportError(details, null);
return MaterialApp(
title: 'Error Widget',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: Scaffold(
appBar: AppBar(
title: Text('Widget渲染异常!!!'),
),
body: getErrorWidget(details.toString()),
),
);
};
其它异常捕获
以下代码放在main()函数中
bool isProduction = const bool.fromEnvironment("dart.vm.product");
FlutterError.onError = (FlutterErrorDetails details) async {
if (isProduction) {
Zone.current.handleUncaughtError(details.exception, details.stack);
}
else {
FlutterError.dumpErrorToConsole(details);
}
};
///初始化Sentry
ExceptionReporter.initSentryWithUid('1001');
runZoned(() async {
runApp(MyApp());
}, onError: (dynamic error,StackTrace stackTrace) async {
showToast(">>>"+error.toString());
//Sentry上报
ExceptionReporter.reportError(error, stackTrace);
});
exception_reporter.dart 代码文件:
import 'package:flutter_study_app/utils/platform_utils.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry/sentry.dart' as Sentry;
var _sentryClient = Sentry.SentryClient(
dsn: 'http://d5c4bd2894c54412b7139e15cd356d0f@192.168.0.33:9000/5'
);
class ExceptionReporter {
/// 初始化Sentry
/// 填写 uid
static void initSentryWithUid(String uid) {
_sentryClient.userContext = Sentry.User(id: uid);
}
static Future<Null> reportError(dynamic error, dynamic stackTrace) async {
Map<String, dynamic> deviceInfo = await PlatformUtils
.getMapFromDeviceInfo();
String appVersion = await PlatformUtils.getAppVersion();
/// 自定义需要上报的信息
Map<String, dynamic> _errMap = {
'\n 错误类型 \n': error.runtimeType.toString(),
'\n 应用版本 \n': appVersion,
'\n 设备信息 \n': deviceInfo,
'\n 错误信息 \n': error.toString()
};
// Sentry上报
//_sentryClient.captureException(exception: '$_errMap',stackTrace: stackTrace);
final SentryResponse response = await _sentryClient.capture(
event: Event(
exception: '$_errMap',
stackTrace: stackTrace,
),
);
// 上报结果处理
if (response.isSuccessful) {
print('Success! Event ID: ${response.eventId}');
} else {
print('Failed to report to Sentry.io: ${response.error}');
}
}
}
FlutterException率的计算
FlutterException率 = exception发生次数 / flutter页面PV
分子:exception发生次数(已过滤掉白名单)
Flutter内部assert、try-catch和一些异常逻辑的地方会统一调用FlutterError.onError
通过重定向FlutterError.onError到自己的方法中监测exception发生次数,并上报exception信息
分母:flutter页面PV