Flutter异常
众所周知,软件项目的交付是一个复杂的过程,任何原因都有可能导致交付的失败。很多时候经常遇到的一个现象是,应用在开发测试时没有任何异常,但一旦上线就问题频出。出现这些异常,可能是因为不充分的机型适配或者用户糟糕的网络状况造成的,也可能是Flutter框架自身缺陷造成的,甚至是操作系统底层的问题。
而处理此类异常的最佳方式是捕获用户的异常信息,将异常现场保存起来并上传至服务器,然后通过分析异常上下文来定位引起异常的原因,并最终解决此类问题。
所谓Flutter异常,指的是Flutter程序中Dart代码运行时发生的错误。与Java和OC等多线程模型的编程语言不同,Dart是一门单线程的编程语言,采用事件循环机制来运行任务,所以各个任务的运行状态是互相独立的。也即是说,当程序运行过程中出现异常时,并不需要像Java那样使用try-catch机制来捕获异常,因为即便某个任务出现了异常,Dart程序也不会退出,只会导致当前任务后续的代码不会被执行,而其它功能仍然可以继续使用。
在Flutter开发中,根据异常来源的不同,可以将异常分为Framework异常和Dart异常。Flutter对这两种异常提供了不同的捕获方式,Framework异常是由Flutter框架引发的异常,通常是由于错误的应用代码造成Flutter框架底层的异常判断引起的,当出现Framework异常时,Flutter会自动弹出一个的红色错误界面。而对于Dart异常,则可以使用try-catch机制和catchError语句进行处理。
除此之外,Flutter还提供了集中处理框架异常的方案。集中处理框架异常需要使用Flutter提供的FlutterError类,此类的onError属性会在接收到框架异常时执行相应的回调。因此,要实现自定义捕获异常逻辑,只需要为它提供一个自定义的错误处理回调函数即可。
异常捕获
在Flutter开发中,根据异常来源的不同,可以将异常分为Framework异常和Dart异常。所谓Dart异常,指的是应用代码引起的异常。根据异常代码的执行时序,Dart异常可以分为同步异常和异步异常两类。对于同步异常,可以使用try-catch机制来进行捕获,而异步异常的捕获则比较麻烦,需要使用Future提供的catchError语句来进行捕获,如下所示。
//使用try-catch捕获同步异常
try {
throw StateError('This is a Dart exception');
}catch(e) {
print(e);
}
//使用catchError捕获异步异常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'))
.catchError((e)=>print(e));
复制代码
需要说明的是,对于异步调用所抛出的异常是无法使用try-catch语句进行捕获的,因此下面的写法就是错误的。
//以下代码无法捕获异步异常
try {
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future'))
}catch(e) {
print("This line will never be executed");
}
复制代码
因此,对于Dart中出现的异常,同步异常使用的是try-catch,异步异常则使用的是catchError。如果想集中管理代码中的所有异常,那么可以Flutter提供的Zone.runZoned()方法。在Dart语言中,Zone表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果想要处理沙盒中代码执行出现的异常,可以使用沙盒提供的onError回调函数来拦截那些在代码执行过程中未捕获的异常,如下所示。
//同步抛出异常
runZoned(() {
throw StateError('This is a Dart exception.');
}, onError: (dynamic e, StackTrace stack) {
print('Sync error caught by zone');
});
//异步抛出异常
runZoned(() {
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'));
}, onError: (dynamic e, StackTrace stack) {
print('Async error aught by zone');
});
复制代码
可以看到,在没有使用try-catch、catchError语句的情况下,无论是同步异常还是异步异常,都可以使用Zone直接捕获到。 同时,如果需要集中捕获Flutter应用中未处理的异常,那么可以把main函数中的runApp语句也放置在Zone中,这样就可以在检测到代码运行异常时对捕获的异常信息进行统一处理,如下所示。
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
//异常处理
});
复制代码
除了Dart异常外,Flutter应用开发中另一个比较常见的异常是Framework异常。Framework异常指的是Flutter框架引起的异常,通常是由于执行错误的应用代码造成Flutter框架底层异常判断引起的,当出现Framework异常时,系统会自动弹出一个的红色错误界面,如下图所示。
通常,此页面反馈的错误信息对于开发环境的问题定位还是很有帮助的,但如果让线上用户也看到这样的错误页面,体验上就不是很友好比较了。对于Framework异常,最通用的处理方式就是重写ErrorWidget.builder()方法,然后将默认的错误提示页面替换成一个更加友好的自定义提示页面,如下所示。
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
//自定义错误提示页面
return Scaffold(
body: Center(
child: Text("Custom Error Widget"),
)
);
};
复制代码
应用示例
通常,只有当代码运行出现错误时,系统才会给出异常错误提示。为了说明Flutter捕获异常的工作流程,首先来看一个越界访问的示例。首先,新建一个Flutter项目,然后修改main.dart文件的代码,如下所示。
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
List<String> numList = ['1', '2'];
print(numList[5]);
return Container();
}
}
复制代码
上面的代码模拟的是一个越界访问的异常场景。当运行上面的代码时,控制台会给出如下的错误信息。
RangeError (index): Invalid value: Not in range 0..2, inclusive: 5
复制代码
对于程序中出现的异常,通常只需要在Flutter应用程序的入口main.dart文件中,使用Flutter提供的FlutterError类集中处理即可,如下所示。
Future<Null> main() async {
FlutterError.onError = (FlutterErrorDetails details) async {
Zone.current.handleUncaughtError(details.exception, details.stack);
};
runZoned<Future<void>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
await _reportError(error, stackTrace);
});
}
Future<Null> _reportError(dynamic error, dynamic stackTrace) async {
print('catch error='+error);
}
复制代码
同时,对于开发环境和线上环境还需要区别对待。因为,对于开发环境遇到的错误,一般是可以立即定位并修复问题的,而对于线上问题才需要对日志进行上报。因此,对于错误日志上报,需要对开发环境和线上环境进行区分对待,如下所示。
Future<Null> main() async {
FlutterError.onError = (FlutterErrorDetails details) async {
if (isDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
… //省略其他代码
}
bool get isDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
复制代码
异常上报
目前为止,我们已经对应用中出现的所有未处理异常进行了捕获,不过这些异常还只能被保存在移动设备中,如果想要将这些异常上报到服务器还需要做很多的工作。 目前,支持Flutter异常的日志上报的方案有Sentry、Crashlytics等。其中,Sentry是收费的且免费天数只有13天左右,不过它提供的Flutter插件可以帮助开发者快速接入日志上报功能。Crashlytics是Flutter官方支持的日志上报方案,开源且免费,缺点是没有公开的Flutter插件,而flutter_crashlytics插件接入起来也比较麻烦。
Sentry方案
Sentry是一个商业级的日志管理系统,支持自动上报和手动上报两种方方。在Flutter开发中,由于Sentry提供了Flutter插件,因此如果有日志上报的需求,Sentry是一个不错的选择。 使用Sentry之前,需要先在官方网站注册开发者账号。如果还没有Sentry账号,可以先注册一个,然后再创建一个App工程。等待工程创建完成之后,系统会自动生成一个DSN,可以依次点击【Project】→【Settings 】→【Client Keys】来打开DSN,如下图所示。
dependencies:
sentry: ">=3.0.0 <4.0.0"
复制代码
然后,使用flutter packages get命令将插件拉取到本地。使用Sentry之前,需要先创建一个SentryClient对象,如下所示。
const dsn='';
final SentryClient _sentry = new SentryClient(dsn: dsn);
复制代码
为了方便对错误日志进行上传,可以提供一个日志的上报方法,然后在需要进行日志上报的地方调用日志上报方法即可,如下所示。
Future<void> _reportError(dynamic error, dynamic stackTrace) async {
_sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
}
runZoned<Future<void>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) {
_reportError(error, stackTrace); //上传异常日志
});
复制代码
同时,开发环境遇到的异常通常是不需要上报的,因为可以立即定位并修复问题,线上遇到的问题才需要进行上报,因此在进行异常上报时还需要区分开发环境和线上环境。
const dsn='https://[email protected]/5189144';
final SentryClient _sentry = new SentryClient(dsn: dsn);
Future<Null> main() async {
FlutterError.onError = (FlutterErrorDetails details) async {
if (isInDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
await _reportError(error, stackTrace);
});
}
Future<Null> _reportError(dynamic error, dynamic stackTrace) async {
if (isInDebugMode) {
print(stackTrace);
return;
}
final SentryResponse response = await _sentry.captureException(
exception: error,
stackTrace: stackTrace,
);
//上报结果处理
if (response.isSuccessful) {
print('Success! Event ID: ${response.eventId}');
} else {
print('Failed to report to Sentry.io: ${response.error}');
}
}
bool get isInDebugMode {
bool inDebugMode = false;
assert(inDebugMode = true);
return inDebugMode;
}
复制代码
在真机上运行Flutter应用,如果出现错误,就可以在Sentry服务器端看到对应的错误日志,如下图所示。
Bugly方案
目前,Bugly还没有提供Flutter插件,那么,我们针对混合工程,可以采用下面的方案。接入Bugly时,只需要完成一些前置应用信息关联绑定和 SDK 初始化工作,就可以使用 Dart 层封装好的数据上报接口去上报异常了。可以看到,对于一个应用而言,接入数据上报服务的过程,总体上可以分为两个步骤:
- 初始化 Bugly SDK;
- 使用数据上报接口。
这两步对应着在 Dart 层需要封装的 2 个原生接口调用,即 setup 和 postException,它们都是在方法通道上调用原生代码宿主提供的方法。考虑到数据上报是整个应用共享的能力,因此我们将数据上报类 FlutterCrashPlugin 的接口都封装成了单例,如下所示。