当前位置:   article > 正文

Flutter混编工程之异常处理

flutter try catch

749bdb90fe55d6d2be67cd6cb8e5e1a2.png

点击上方蓝字关注我,知识会给你力量

41ade7e0e1b9cc7bd275fe2f35aa9367.png

Flutter App层和Framework层的异常,通常是不会引起Crash的,但是Engine层的异常会造成Crash。而Flutter Engine部分的异常,主要是libfutter.so发生的异常,这部分的异常,在Dart层无法捕获,一般会交给类似Bugly这样的平台来收集。

我们能主动监控的,主要是Dart层的异常,这些异常虽然不会让App crash,但是统计这些异常对于提高我们的用户体验,是非常有必要的。

同步异常与异步异常

对于同步异常来说,直接使用try-catch就可以捕获异常,如果要指定捕获的异常类型,可以使用on关键字。但是,try-catch不能捕获异步异常,就像下面的代码,是无法捕获的。

  1. try {
  2.   Future.error("error");
  3. } catch (e){
  4.   print(e)
  5. }

这和在Java中,try-catch捕获Thread中的异常类似,对于异步异常来说,只能使用Future的catchError或者是onError来捕获异常,代码如下所示。

Future.delayed(Duration(seconds: 1)).then((value) => print(value), onError: (e) {});

Dart的执行队列是一个单线程模型,所以在事件循环队列中,当某个Task发生异常并没有被捕获时,程序并不会退出,只是当前的Task异常中止,也就是说一个Task发生的异常是不会影响其它Task执行的。

Widget Build异常

Widget在Build过程中如果发生异常,例如在build函数中出错(throw exception),我们会看见一个深红色的异常界面,这个就是Flutter自带的异常处理界面,我们来看下源代码中,Flutter对这类异常的处理方式。在ComponentElement的实现中,我们找到performRebuild函数,这个是函数是build时所调用的,我们在这里,可以找到相关的实现。

如下所示,在执行到build()函数如果出错时,就会被catch,从而创建一个ErrorWidget。
f7dbb0810aed2568bc32e29fb4b1dd02.png
再进入_debugReportException中一探究竟,你会发现,应用层的异常被catch之后,都是通过FlutterError.reportError来处理的。
e40b6aa3e5e6be2b207227fd5fe4eef5.png
在reportError中,会调用onError来处理,默认的处理方式是dumpErrorToConsole,它就是onError的默认实现。
90701c352935d7f57e294b8b4e86c8bf.png

在这里我们还能发现如何判断debug模式,看源码是不是很有意思。

通过上面的源码,我们就可以了解到,当Flutter应用层崩溃后,SDK的处理,简而言之,就是会构建一个错误界面,同时回调onError函数。在这里,我们可以通过修改这个静态的回调函数,来创建自己的处理方式。

674c09a52a328baf996d13dc74006f3a.png

所以,很简单,我们只需要在main()中,执行下面的代码即可。

  1. var defaultError = FlutterError.onError;
  2. FlutterError.onError = (FlutterErrorDetails details) {
  3.   defaultError?.call(details);// 根据需要是否要保留default处理
  4.   reportException(details);
  5. };

defaultError?.call(details)就是默认将异常日志打印到console的方法,如果不用,这里可以去掉。

重写错误界面

前面我们看到了,在源代码中,Flutter自定义了一个ErrorWidget作为默认的异常界面,在平时的开发中,我们可以自定义ErrorWidget.builder,实现一个更友好的错误界面,例如封装一个统一的异常提示界面。

  1. ErrorWidget.builder = (FlutterErrorDetails details) {
  2.   return MaterialApp(
  3.     theme: ThemeData(primarySwatch: Colors.red),
  4.     home: Scaffold(
  5.       appBar: AppBar(
  6.         title: const Text('出错了,请稍后再试'),
  7.       ),
  8.       body: SingleChildScrollView(
  9.         child: Padding(
  10.             padding: const EdgeInsets.all(8.0),
  11.             child: Text(details.toString()), // 后续修改为统一的错误页
  12.           )),
  13.     ),
  14.   );
  15. };

如上所示,通过修改ErrorWidget.builder,就可以将任意自定义的界面作为异常界面了。

全局未捕获异常

前面讲到的,都是属于被捕获的异常,而有一些异常,在代码中是没有被捕获的,这就类似Android的UncaughtExceptionHandler,Flutter也提供了一个全局的异常处理钩子函数,所有的未捕获异常,无论是同步异常还是异步异常,都会在这里被监听。

在Dart中,SDK提供了一个Zone的概念,一个Zone就类似一个沙箱,在Zone里面,可以拥有独立的异常处理、print函数等等功能,多个Zone之间是彼此独立的,所以,我们只需要将App运行在一个Zone里面,就可以借助它的handleUncaughtError来处理所有的未捕获异常了。下面是使用Zone的一个简单示例。

  1. void main() {
  2.   runZoned(
  3.     () => runApp(const MyApp(color: Colors.blue)),
  4.     zoneSpecification: ZoneSpecification(
  5.       handleUncaughtError: (
  6.         Zone self,
  7.         ZoneDelegate parent,
  8.         Zone zone,
  9.         Object error,
  10.         StackTrace stackTrace,
  11.       ) {
  12.         reportException(
  13.           FlutterErrorDetails(
  14.             exception: error,
  15.             stack: stackTrace,
  16.           ),
  17.         );
  18.       },
  19.     ),
  20.   );
  21. }

根据文档中的提升,可以使用runZonedGuarded来进行简化,代码如下所示。

  1. void main() {
  2.   runZonedGuarded(
  3.     () => runApp(const MyApp(color: Colors.blue)),
  4.     (Object error, StackTrace stack) {
  5.       reportException(
  6.         FlutterErrorDetails(
  7.           exception: error,
  8.           stack: stack,
  9.         ),
  10.       );
  11.     },
  12.   );
  13. }

封装

下面我们将前面的异常处理方式都合并到一起,并针对EngineGroup的多入口处理,封装一个类,代码如下所示。

  1. class SafeApp {
  2.   run(Widget app) {
  3.     ErrorWidget.builder = (FlutterErrorDetails details) {
  4.       return MaterialApp(
  5.         theme: ThemeData(primarySwatch: Colors.red),
  6.         home: Scaffold(
  7.           appBar: AppBar(
  8.             title: const Text('出错了,请稍后再试'),
  9.           ),
  10.           body: SingleChildScrollView(
  11.             child: Padding(
  12.                 padding: const EdgeInsets.all(8.0),
  13.                 child: Text(details.toString()), // 后续修改为统一的错误页
  14.               )),
  15.         ),
  16.       );
  17.     };
  18.     FlutterError.onError = (FlutterErrorDetails details) {
  19.       Zone.current.handleUncaughtError(details.exception, details.stack!);
  20.     };
  21.     runZonedGuarded(
  22.       () => runApp(const MyApp(color: Colors.blue)),
  23.       (Object error, StackTrace stack) {
  24.         reportException(
  25.           FlutterErrorDetails(
  26.             exception: error,
  27.             stack: stack,
  28.           ),
  29.         );
  30.       },
  31.     );
  32.   }
  33. }

在这里,我们构建了下面这些异常处理的方式:

  • 统一的异常处理界面

  • 将Build异常统一转发到Zone中的异常处理函数来进行处理

  • 将所有的未捕获异常记录

这样的话,我们在使用时,只需要对原始的App进行下调用即可。

void main() => SafeApp().run(const MyApp(color: Colors.blue));

这样就完成了异常处理的封装。

上报

在Flutter侧,我们只是获取了异常的相关信息,如果需要上报,那么我们需要借助Channel,桥接的Native,使用Bugly或其它平台进行上报,我们可以借助Pigeon来进行处理,还不熟悉的朋友可以参考我前面的文章。
Flutter混编工程之高速公路Pigeon
Flutter混编工程之通讯之路
通过Channel,我们可以把异常数据报给Native侧,再让Native侧走自己的上报通道,例如Bugly等。

NativeCommonApi().reportException('------Flutter_Exception------\n${details.exceptionAsString()}\n${details.stack.toString()}');

同时,Flutter提供了exceptionAsString()方法,将异常信息展示的更加友好一点,我们可以借助它来做一些格式化的操作。

3.3版本API的改进

官方的API更新如下:
https://docs.flutter.dev/testing/errors
PlatformDispatcher.onError在以前的版本中,开发者必须手动配置自定义Zone才能捕获应用程序的所有异常和错误,但是自定义Zone对Dart核心库中的一些优化是有害的,这会减慢应用程序的启动时间。「在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。」

所以,3.3之后,我们不用再设置Zone来捕获全局异常了,只用设置PlatformDispatcher.instance.onError即可。

  1. import 'package:flutter/material.dart';
  2. import 'dart:ui';
  3. Future<void> main() async {
  4.   await myErrorsHandler.initialize();
  5.   FlutterError.onError = (details) {
  6.     FlutterError.presentError(details);
  7.     myErrorsHandler.onErrorDetails(details);
  8.   };
  9.   PlatformDispatcher.instance.onError = (error, stack) {
  10.     myErrorsHandler.onError(error, stack);
  11.     return true;
  12.   };
  13.   runApp(const MyApp());
  14. }
  15. class MyApp extends StatelessWidget {
  16.   const MyApp({super.key});
  17.   @override
  18.   Widget build(BuildContext context) {
  19.     return MaterialApp(
  20.       builder: (context, widget) {
  21.         Widget error = const Text('...rendering error...');
  22.         if (widget is Scaffold || widget is Navigator) {
  23.           error = Scaffold(body: Center(child: error));
  24.         }
  25.         ErrorWidget.builder = (errorDetails) => error;
  26.         if (widget != null) return widget;
  27.         throw ('widget is null');
  28.       },
  29.     );
  30.   }
  31. }

向大家推荐下我的网站 https://www.yuque.com/xuyisheng 点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问

往期推荐

本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。

< END >

作者:徐宜生

更文不易,点个“三连”支持一下

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/455559
推荐阅读
相关标签