最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Flutter Dart 异常不讲武德

    正文概述 掘金(wanderingguy)   2020-12-15   749

    以上内容调侃归调侃,很多异常处理的细节是不是有点过于真实了?我们应当时时对异常处理保持敬畏。

    前言

    Flutter Dart 异常与传统原生平台异常很不一样,原生平台的任务采用多线程调度,当一个线程出现未捕获的异常时,会导致整个进程退出。而在 Dart 中是单线程的,任务采用事件循环调度,Dart 异常并不会导致应用程序崩溃,取而代之的是当前事件后续的代码不会被执行了。

    这样带来的好处是一些无关紧要的异常不会闪退,用户还可以继续使用核心功能。

    坏处是这些异常可能没有明显的提示和异常表现,导致问题容易被隐藏,如果此时恰好是核心流程上且链路较长的异常,可能导致问题排查极难下手。

    本文将从异常的捕获、处理、提示、上报和稳定性指标等角度,系统的介绍异常处理的正确做法,帮助我们提升 APP 的稳定性,希望对你有所帮助。

    局部异常捕获

    同步异常

    对于同步异常,我们只需要在可能出现异常的代码块包一层 try-catch 即可。

    try {
      String abc;
      print("abc's length ${abc.length}");
    } catch (error, stacktrace) {
      //todo catch all error
    }
    

    catch 最多提供两个可选参数:

    • 第一个参数 error 类型为 Object,也就是异常是可以抛出任意对象。
    • 第二个参数 stacktrace,表示异常堆栈。

    如果想捕获特定类型的异常可以,使用on关键字。

    try {
      String abc;
      print("abc's length ${abc.length}");
    }  on NoSuchMethodError catch (error, stacktrace) {
      //todo catch NoSuchMethodError
    }
    

    在处理异常中,可以抛出新的异常,使用throw关键字。

    try {
      String abc;
      print("abc's length ${abc.length}");
    }  on NoSuchMethodError catch (error, stacktrace) {
      //exception handler
      ...
      throw 'abc';
    }
    

    常见的同步异常包括,空指针异常(NoSuchMethodError)、类型转换异常(type xxx is not a subtype of type xxx)、格式转换异常(FormatException)等。

    异步异常

    使用 catchError 捕获异步异常,第一个参数为 Function error 类型,入参至多两个 分别为error 和 stackstace,均可选。

    Future.delayed(Duration(seconds: 1), () {
      throw '123';
    }).then((value) {
      print('value $value');
      return value;
    }).catchError((error, stack) {
      print('error $error stack $stack');
    });
    

    第二个参数为 {bool test(Object error)},是一个判断表达式,当此表达式返回值为 true 时,表示需要执行 catch 逻辑,如果返回 false,则不执行 catch 逻辑,即会成为未捕获的异常,默认不传时 认为是true。

    这里的作用是可以精细化的处理异常,可以理解为同步异常中强化版的 on 关键字,例如:

    Future.delayed(Duration(seconds: 1), () {
      throw 123;
    }).then((value) {
      print('value $value');
      return value;
    }).catchError(() {
      //todo exception handler
    }, test: (error) => error is int);
    

    由于异步任务在默认另一个任务队列中,所以这部分异常不会影响 UI 渲染流程,不会在页面上有展示红屏,只会在控制台中输出。

    全局异常捕获

    全局异常可以分为全局同步异常和全局异步异常。

    对于同步异常,大部分出现在 UI 渲染流程中,我们称之为全局 UI 异常。

    对于异步异常,上面讲到可以通过 catchError 手动捕获,那如果没有手动 catch,可不可以在全局集中捕获这些异常呢?还有上面讲到的非 UI 绘制流程中的同步异常,又能不能捕获到呢?

    答案是肯定的,我们依次来看:

    全局 UI 异常

    在 Flutter 的世界一切皆 Widget,视图、业务逻辑由 UI 渲染驱动,你在开发 Flutter 页面时,时不时看见的红屏,实际上就是 Flutter Framework 对 UI 布局绘制中产生的异常捕获后展示的提示页面。

    # framework.dart / ComponentElement
    void performRebuild() {
        ...
        Widget built;
        try {
          built = build();
          ...
        } catch (e, stack) {
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $this'),
              e, stack,
              ...
            ),
          );
        } finally {
          ...
        }
        try {
          _child = updateChild(_child, built, slot);
        } catch (e, stack) {
          built = ErrorWidget.builder(
            _debugReportException(
              ErrorDescription('building $this'),
              e, stack,
              ...
              },
            ),
          );
          _child = updateChild(null, built, slot);
        }
      }
    

    performRebuild 在 widget 重绘时调用,是 UI 渲染流程的必经之路,包括 State 生命周期,可以看到其内部对 build 方法和 updateChild 更新方法都做了 try-catch 处理。

    ErrorWidget.builder 返回的 widget 将作为错误信息,展示在前台,默认也就是我们看到的红底黄字的错误页。

    _debugReportException 方法内部会将错误封装成一个 _debugReportException 对象返回,并调用 FlutterError 对象的静态函数 onError ,默认是将错误信息、堆栈等信息打印在控制台。

    # framework.dart
    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
    static FlutterExceptionHandler onError = dumpErrorToConsole;
    
    static void reportError(FlutterErrorDetails details) {
    ...
    if (onError != null)
      onError(details);
    }
    

    因此,我们可以在 main 函数入口,为 FlutterError 的 onError 重新赋值做自定义处理。

    void main() {
        FlutterError.onError = (FlutterErrorDetails details) {
            //todo 自定义的异常处理
            print('catch a exception');
            FlutterError.dumpErrorToConsole(details);
      }; 
      runApp(MyApp());
    }
    

    你如果实际测试一下会发现,这样做并不能捕获异常,这是因为 Flutter 的官方 issue#47447,在 release 模式下才能生效,或者使用船新的 Flutter 版本。

    全局未捕获异常处理

    Flutter 为我们提供 Zone 的概念,相当于沙盒,将我们的 runApp 调用包裹在一个 runZoned 下,可以全局捕获未 catch 的异常,包括异步异常,类似 Android 中的Thread.UncaughtExceptionHandler

    # main.dart
    runZoned(() {
        runApp(MyApp());
    }, onError: _handleError);
    
    void _handleError(Object obj, StackTrace stack) {
      // todo global uncaught exception handle
      print('zone _handleError $obj stack $stack');
    }
    

    至此,我们就完成了 APP 内全部的 Dart 异常捕获的工作。

    关于 Zone

    Dart 中的 Zone 表示指定代码执行的环境,主线程也是通过 _runMainZoned 方法启动一个新的沙盒环境。通过 runZoned 函数会基于当前 Zone Fork 一个新的沙盒环境,在这个沙盒环境中,我们可以做集中的异常处理,或者改变一些默认的系统行为。

    # Zone.dart
    R runZoned<R>(R body(),
        {Map zoneValues,
        ZoneSpecification zoneSpecification, 
        Function onError})
    
    • zoneValues:指定 Zone 的私有数据,默认情况下父 Zone 节点设置的 zoneValues,在子 Zone 中同样可以访问到。我们可以通过 .parent 方法获得父 Zone 的引用Zone.current.parent。此外,这里设置 zoneValues 以后,在当前 zone 的任何位置,都可以通过Zone.current[#key]访问到值。
    • zoneSpecification:可以设置一些系统行为的拦截,比如全局事件回调、全局异常回调、Timer 创建拦截、print 打印拦截(可以做日志收集或美化打印)等等,详细参考 ZoneSpecification.class
    • onError: 即上文讲到的全局未捕获的异常,都会这里收到回调。对于已捕获的异常,我们也可以通过Zone.current.handleUncaughtError(exception, stack)方法发送到这个 onError 中集中处理。

    比如上文提到的全局 UI 异常,是被 Flutter Framework 捕获了。那么我们可以通过下面的代码,收集到这部分异常。

    FlutterError.onError = (FlutterErrorDetails details) async {
        Zone.current.handleUncaughtError(details.exception, details.stack);
    };
    

    需要指出,Flutter 中的 Future 就是对 Zone 的封装。

    异常提示和上报收集

    异常捕获到了,仅仅完成了万里长征第一步,可以看到 UI 渲染的异常会有明显的视图提示,但是对于异步异常对研发和测试是无感知的,这不像是原生开发,出现异常 APP 会直接闪退。

    这导致很容易忽略这部分异常,为了尽早的将问题暴露出来,我们必须要捕获到异常时给测试和开发 强提示

    自定义异常提示视图

    上文讲到,当 UI 渲染出现异常时,会展示一个红屏黄字的异常视图,这是 Flutter 的默认行为,我们可以在 main 函数中,自定义一个错误提醒页面,下面是一个例子:

    # main.dart
    ErrorWidget.builder = (FlutterErrorDetails detail) {
    return Container(
        color: Colors.white,
        child: Column(children: <Widget>[
          Text(
            'error\n--------\n ${detail.exception}',
            style: TextStyle(fontSize: 12, color: Colors.green),
          ),
          SizedBox(height: 12),
          Text(
            'stacktrace\n--------\n ${detail.stack}',
            style: TextStyle(fontSize: 12, color:Colors.green),
          ),
        ]));
    };
    

    效果如图,是不是看起来比红屏让人淡定多了呢?

    Flutter Dart 异常不讲武德

    全局提示弹窗

    对于其他异常,默认是不会在页面上提示的,上文已经可以捕获到了这部分异常,我们可以将这些异常以弹窗的形式提醒开发者和测试人员。

    具体可以这样做:

    runZoned(() {
        runApp(MyApp());
    }, onError: (exception, stackTrace) {
        _handleError(exception, stackTrace);
    });
    
    /// 异常处理
    void _handleError(Object error, StackTrace stack, Map zoneValues) {
        debugPrint(error);//打印异常信息
        _showExceptionAlertDialog(error, stack);//提示弹窗
        _uploadException(error, stack);//上报异常
    }
    

    弹窗代码如下:

    void _showExceptionAlertDialog(Object error, StackTrace stack) {
      //①
      assert(() {
        GlobalKey key = globalKey;
        if (key == null || key.currentContext == null) {
          return true;
        }
        //②
        SchedulerBinding.instance.addPostFrameCallback(_) {
          showDialog(
            context: key.currentContext,//③
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text('$error'),
                content: SingleChildScrollView(
                    child: Text("$stack"),
                ),
                actions: ...,
              );
            },
          );
        });
        return true;
      }());
    }
    

    最终效果是这样的:

    Flutter Dart 异常不讲武德

    这里有三个地方需要注意:

    1. 使用 assert 断言,只会在 debug 模式下执行,因此 release 版本不会弹出异常对话框。
    2. 由于异常可能在任何时刻出现,而视图有可能正在渲染 ,所以需要注册一个渲染结束的回调,在本帧渲染之后执行。对于渲染阶段的异常会在这一帧结束后立刻弹窗,对于异步异常如果在当前帧结束后才出现,则会在下帧渲染结束后弹出。
    3. 弹窗需要指定一个 context,用于确定所属的导航栈,这里使用全局的 GlobalKey 就可以,而这个 GlobalKey 通常可以用 MaterialApp 中 home 指向的 Widget 声明的 GlobalKey。这么做的目的是,home 一定不会退出,可以保证这个 context 一定存在。

    为了提高移植性,我将异常的回调提取出来,并提供一些开关,供业务层灵活使用,详细参考 Github flutter_exception_handler。

    对于异常的上报,如果没有自研的异常收集平台,可以使用 bugly 或者开源的 Sentry 服务。

    至此,我们已经完成了异常的捕获和上报,有了这些异常数据,接下来我们应该如何评价项目的稳定性呢?

    稳定性指标

    对于原生开发而言,稳定性可以用崩溃率来衡量,崩溃率达到万分之一就是优秀的标准。

    但是对于 Flutter 而言,崩溃只会出现在 Flutter 引擎或者一些 channel 中,Dart 侧的异常都不会导致应用程序的崩溃,大部分业务的异常都在 Dart 侧,所以传统的崩溃率统计一定是大幅下降的(不排除会出现一些奇奇怪的 Flutter Engine Crash,所以还是建议升级到最新的 Flutter 版本)。

    与之相反,如果统计 Dart 异常率,这个指标有可能高的离谱,原因是一次启动可能导致多次异常,如果在一个定时任务中出现异常,那么这个指标就更高了。所以为了更合理的衡量 Flutter 侧的稳定性,我们可以统计 页面异常率

    页面异常率 = 异常数 / 打开的页面数

    对于打开的页面数,我们可以通过为 MaterialApp 注册 navigatorObservers,来监听全局的页面路由数据。在异常上报时同时附带当前路由信息,即可实现页面异常率的统计。

    我们可以更精细化的统计某个页面的异常率,理论上这个值同样可能超过 100%,但相对于崩溃率的统计方式,已经大幅趋向合理。

    对于重 Flutter 的业务场景,页面异常率应当同样以万分之一作为优秀标准。

    常见的异常处理 Tips

    这里提供几个工作中常见的异常情况,供你参考并在实际开发中多加留意。

    1. 多使用 ? 和操作符 ?? 来进行验空操作。
    2. 基本类型的操作注意验空,在 Dart 语言中一切皆为对象,常见的 int,bool 都是对象,且默认值是null,如果你稍不注意写出 if(isComplete)bool isExceed = count < 10 这样的代码,就可能出问题,正确的做法可以是这样,if(isComplete ?? false)bool isExceed = (count ?? 0) < 10
    3. setState() 保证在 mounted 条件下。
    4. 使用完的 ScrollController 注意调用 dispose 销毁,已经销毁的不要再次使用。
    5. 转数字的场景,可以使用 int.tryParse,而不是直接 int.parse,前者内部会处理异常情况。
    6. Text 指定的文本不能为 null,需做好验空处理。
    7. PlatformChannel 注意提前注册,并做好 method 实现。
    8. 做好核心流程和分支流程的隔离,比如首页同时请求三个网络接口,但核心接口是第一个,那么应当对第一个和后两个做单独的异常处理,而不能整体一起 catch。
    9. 尽量升级最新的 Flutter 版本,当真正拿到线上的异常数据时,你会发现很多异常都是 Flutter 官方 issue。
    10. 对于重度依赖 Webview 的场景,请尽快升级到 1.20 及以后版本,使用 HybridComposition 模式渲染 Webview,避免键盘、Resize、多指操作等奇葩问题。
    11. 对于在业务代码中已经手动捕获的异常,应该主动上报到独立的异常类型中,以和未捕获的异常区分。

    相关文章

    • Report errors to a service
    • Dart Zones

    欢迎关注公众号 wanderingTech ,获取更多深度好文,溜了~~~

    Flutter Dart 异常不讲武德


    起源地下载网 » Flutter Dart 异常不讲武德

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元