前言

在给 Flutter 利用做异样监控的时候,一开始我是回绝滴,如果不思考 Flutter Engine 和 native 侧的监控,用我另一篇文章中不得不晓得的 Flutter 异样捕捉知识点 提到的办法根本能够搞定所有 Dart 侧异样,要害代码也不多,简单不到哪里去。如下(有不分明原理的能够看下原文,这里就不赘叙了):

void main() {  FlutterError.onError = (FlutterErrorDetails details) {    Zone.current.handleUncaughtError(details.exception, details.stack);//Tag1    //或customerReport(details);  };  //Tag2  Isolate.current.addErrorListener(      RawReceivePort((dynamic pair) async {        final isolateError = pair as List<dynamic>;        customerReport(details);      }).sendPort,    );  runZoned(    () => runApp(MyApp()),    zoneSpecification: ZoneSpecification(      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {            report(line)      },    ),    onError: (Object obj, StackTrace stack) {      //Tag3      customerReport(e, stack);    }  );}

为什么会找到 Catcher,有三个起因:

  1. 纯正是带着好奇的心态想理解下这么简略的性能人家还能玩出花色来。
  2. 官网举荐 的 Sentry 最初还是会通过 MethodChannel 形式给到对端原生来报这种天生太依赖对端的行为我不太认同我想找一个纯 Dart 实现的库进步异样监控的可移植性。
  3. Catcher 简略读起来能够进步自信心。

Catcher 简介

我的了解 Catcher 有如下特色:

  1. 针对 Flutter 侧异样收集的一个纯 Dart 库,人造反对各种平台包含对 Web 侧的反对。
  2. 反对异样 UI 自定义显示及扩大,默认反对对话框,终端,或者页面模式等。
  3. 反对自定义异样的上报策略,默认反对异样到文件上传到网络,Sentry 等。
  4. 流程清晰简略。

中文介绍详见[[译] 应用 Catcher 解决 Flutter 谬误 - 掘金](https://juejin.cn/post/684490...),这里说下根本应用。

main() {  /// STEP 1. Create catcher configuration.  /// Debug configuration with dialog report mode and console handler. It will show dialog and once user accepts it, error will be shown   /// in console.  CatcherOptions debugOptions =      CatcherOptions(DialogReportMode(), [ConsoleHandler()]);  /// Release configuration. Same as above, but once user accepts dialog, user will be prompted to send email with crash to support.  CatcherOptions releaseOptions = CatcherOptions(DialogReportMode(), [    EmailManualHandler(["support@email.com"])  ]);  /// STEP 2. Pass your root widget (MyApp) along with Catcher configuration:  Catcher(rootWidget: MyApp(), debugConfig: debugOptions, releaseConfig: releaseOptions);}
  1. 通过 CatcherOptions 创立两个配置,一个 debug,一个 release。
  2. 将配置设置到 Catcher 对象中即可实现异样上报和监控。

成果展现图:

如果设置了 ConsoleHandler , 日志输入如下:

I/flutter ( 7457): [2019-02-09 12:40:21.527271 | ConsoleHandler | INFO] ============================== CATCHER LOG ==============================I/flutter ( 7457): [2019-02-09 12:40:21.527742 | ConsoleHandler | INFO] Crash occured on 2019-02-09 12:40:20.424286I/flutter ( 7457): [2019-02-09 12:40:21.527827 | ConsoleHandler | INFO]I/flutter ( 7457): [2019-02-09 12:40:21.527908 | ConsoleHandler | INFO] ------- DEVICE INFO -------I/flutter ( 7457): [2019-02-09 12:40:21.528233 | ConsoleHandler | INFO] id: PSR1.180720.061I/flutter ( 7457): [2019-02-09 12:40:21.528337 | ConsoleHandler | INFO] androidId: 726e4abc58dde277I/flutter ( 7457): [2019-02-09 12:40:21.528431 | ConsoleHandler | INFO] board: goldfish_x86I/flutter ( 7457): [2019-02-09 12:40:21.528512 | ConsoleHandler | INFO] bootloader: unknownI/flutter ( 7457): [2019-02-09 12:40:21.528595 | ConsoleHandler | INFO] brand: googleI/flutter ( 7457): [2019-02-09 12:40:21.528694 | ConsoleHandler | INFO] device: generic_x86I/flutter ( 7457): [2019-02-09 12:40:21.528774 | ConsoleHandler | INFO] display: sdk_gphone_x86-userdebug 9 PSR1.180720.061 5075414 dev-keysI/flutter ( 7457): [2019-02-09 12:40:21.528855 | ConsoleHandler | INFO] fingerprint: google/sdk_gphone_x86/generic_x86:9/PSR1.180720.061/5075414:userdebug/dev-keysI/flutter ( 7457): [2019-02-09 12:40:21.528939 | ConsoleHandler | INFO] hardware: ranchuI/flutter ( 7457): [2019-02-09 12:40:21.529023 | ConsoleHandler | INFO] host: vped9.mtv.corp.google.comI/flutter ( 7457): [2019-02-09 12:40:21.529813 | ConsoleHandler | INFO] isPsychicalDevice: falseI/flutter ( 7457): [2019-02-09 12:40:21.530178 | ConsoleHandler | INFO] manufacturer: GoogleI/flutter ( 7457): [2019-02-09 12:40:21.530345 | ConsoleHandler | INFO] model: Android SDK built for x86I/flutter ( 7457): [2019-02-09 12:40:21.530443 | ConsoleHandler | INFO] product: sdk_gphone_x86I/flutter ( 7457): [2019-02-09 12:40:21.530610 | ConsoleHandler | INFO] tags: dev-keysI/flutter ( 7457): [2019-02-09 12:40:21.530713 | ConsoleHandler | INFO] type: userdebugI/flutter ( 7457): [2019-02-09 12:40:21.530825 | ConsoleHandler | INFO] versionBaseOs:I/flutter ( 7457): [2019-02-09 12:40:21.530922 | ConsoleHandler | INFO] versionCodename: RELI/flutter ( 7457): [2019-02-09 12:40:21.531074 | ConsoleHandler | INFO] versionIncremental: 5075414I/flutter ( 7457): [2019-02-09 12:40:21.531573 | ConsoleHandler | INFO] versionPreviewSdk: 0I/flutter ( 7457): [2019-02-09 12:40:21.531659 | ConsoleHandler | INFO] versionRelase: 9I/flutter ( 7457): [2019-02-09 12:40:21.531740 | ConsoleHandler | INFO] versionSdk: 28I/flutter ( 7457): [2019-02-09 12:40:21.531870 | ConsoleHandler | INFO] versionSecurityPatch: 2018-08-05I/flutter ( 7457): [2019-02-09 12:40:21.532002 | ConsoleHandler | INFO]I/flutter ( 7457): [2019-02-09 12:40:21.532078 | ConsoleHandler | INFO] ------- APP INFO -------I/flutter ( 7457): [2019-02-09 12:40:21.532167 | ConsoleHandler | INFO] version: 1.0I/flutter ( 7457): [2019-02-09 12:40:21.532250 | ConsoleHandler | INFO] appName: catcher_exampleI/flutter ( 7457): [2019-02-09 12:40:21.532345 | ConsoleHandler | INFO] buildNumber: 1I/flutter ( 7457): [2019-02-09 12:40:21.532426 | ConsoleHandler | INFO] packageName: com.jhomlala.catcherexampleI/flutter ( 7457): [2019-02-09 12:40:21.532667 | ConsoleHandler | INFO]I/flutter ( 7457): [2019-02-09 12:40:21.532944 | ConsoleHandler | INFO] ---------- ERROR ----------I/flutter ( 7457): [2019-02-09 12:40:21.533096 | ConsoleHandler | INFO] Test exceptionI/flutter ( 7457): [2019-02-09 12:40:21.533179 | ConsoleHandler | INFO]I/flutter ( 7457): [2019-02-09 12:40:21.533257 | ConsoleHandler | INFO] ------- STACK TRACE -------I/flutter ( 7457): [2019-02-09 12:40:21.533695 | ConsoleHandler | INFO] #0      ChildWidget.generateError (package:catcher_example/file_example.dart:62:5)I/flutter ( 7457): [2019-02-09 12:40:21.533799 | ConsoleHandler | INFO] <asynchronous suspension>I/flutter ( 7457): [2019-02-09 12:40:21.533879 | ConsoleHandler | INFO] #1      ChildWidget.build.<anonymous closure> (package:catcher_example/file_example.dart:53:61)I/flutter ( 7457): [2019-02-09 12:40:21.534149 | ConsoleHandler | INFO] #2      _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:507:14)I/flutter ( 7457): [2019-02-09 12:40:21.534230 | ConsoleHandler | INFO] #3      _InkResponseState.build.<anonymous closure> (package:flutter/src/material/ink_well.dart:562:30)I/flutter ( 7457): [2019-02-09 12:40:21.534321 | ConsoleHandler | INFO] #4      GestureRecognizer.invokeCallback (package:flutter/src/gestures/recognizer.dart:102:24)I/flutter ( 7457): [2019-02-09 12:40:21.534419 | ConsoleHandler | INFO] #5      TapGestureRecognizer._checkUp (package:flutter/src/gestures/tap.dart:242:9)I/flutter ( 7457): [2019-02-09 12:40:21.534524 | ConsoleHandler | INFO] #6      TapGestureRecognizer.handlePrimaryPointer (package:flutter/src/gestures/tap.dart:175:7)I/flutter ( 7457): [2019-02-09 12:40:21.534608 | ConsoleHandler | INFO] #7      PrimaryPointerGestureRecognizer.handleEvent (package:flutter/src/gestures/recognizer.dart:315:9)I/flutter ( 7457): [2019-02-09 12:40:21.534686 | ConsoleHandler | INFO] #8      PointerRouter._dispatch (package:flutter/src/gestures/pointer_router.dart:73:12)I/flutter ( 7457): [2019-02-09 12:40:21.534765 | ConsoleHandler | INFO] #9      PointerRouter.route (package:flutter/src/gestures/pointer_router.dart:101:11)I/flutter ( 7457): [2019-02-09 12:40:21.534843 | ConsoleHandler | INFO] #10     _WidgetsFlutterBinding&BindingBase&GestureBinding.handleEvent (package:flutter/src/gestures/binding.dart:180:19)I/flutter ( 7457): [2019-02-09 12:40:21.534973 | ConsoleHandler | INFO] #11     _WidgetsFlutterBinding&BindingBase&GestureBinding.dispatchEvent (package:flutter/src/gestures/binding.dart:158:22)I/flutter ( 7457): [2019-02-09 12:40:21.535052 | ConsoleHandler | INFO] #12     _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerEvent (package:flutter/src/gestures/binding.dart:138:7)I/flutter ( 7457): [2019-02-09 12:40:21.535136 | ConsoleHandler | INFO] #13     _WidgetsFlutterBinding&BindingBase&GestureBinding._flushPointerEventQueue (package:flutter/src/gestures/binding.dart:101:7)I/flutter ( 7457): [2019-02-09 12:40:21.535216 | ConsoleHandler | INFO] #14     _WidgetsFlutterBinding&BindingBase&GestureBinding._handlePointerDataPacket (package:flutter/src/gestures/binding.dart:85:7)I/flutter ( 7457): [2019-02-09 12:40:21.535600 | ConsoleHandler | INFO] #15     _rootRunUnary (dart:async/zone.dart:1136:13)I/flutter ( 7457): [2019-02-09 12:40:21.535753 | ConsoleHandler | INFO] #16     _CustomZone.runUnary (dart:async/zone.dart:1029:19)I/flutter ( 7457): [2019-02-09 12:40:21.536008 | ConsoleHandler | INFO] #17     _CustomZone.runUnaryGuarded (dart:async/zone.dart:931:7)I/flutter ( 7457): [2019-02-09 12:40:21.536138 | ConsoleHandler | INFO] #18     _invoke1 (dart:ui/hooks.dart:170:10)I/flutter ( 7457): [2019-02-09 12:40:21.536271 | ConsoleHandler | INFO] #19     _dispatchPointerDataPacket (dart:ui/hooks.dart:122:5)I/flutter ( 7457): [2019-02-09 12:40:21.536375 | ConsoleHandler | INFO]I/flutter ( 7457): [2019-02-09 12:40:21.536539 | ConsoleHandler | INFO] ======================================================================

Catcher 设计思路

Catcher 流程图。

如上整个流程:

  1. 利用运行过程中产生了 Error,这些 Error 被 Catcher 捕捉到结构成新的对象 Report。
  2. Report 被发送给了 Reporter,Reporter 会决定对 Report 的解决策略:勾销还是承受。
  3. 如果承受 Report,那么 Report 会交给 handers 持续解决直至实现。

1. Catcher 异样捕捉机会与 Report 结构

这里能够盲猜下,如上步骤 1 其实相当于前言中的集体根底版本代码,负责收集 Error 过程。看下 Catcher 收集 Error 的代码三个关键点别离如下,根本跟咱们代码解决是一样的。

runZonedGuarded

Isolate._current_.addErrorListener

FlutterError._onError_

Report 结构

void _reportError(    dynamic error,    dynamic stackTrace, {    FlutterErrorDetails? errorDetails,  }) async {    //.....    final Report report = Report(      error,      stackTrace,      //额定增加字段如下:      DateTime.now(),      _deviceParameters,      _applicationParameters,      _currentConfig.customParameters,      errorDetails,      _getPlatformType(),      screenshot,    );

2. Reporter 接管和决策 Report

从下面步骤中咱们晓得,关怀的 error 和 stackTrace 被包装到了 Report 中,咱们次要关注 Report 流向即可跟踪主流程。这里说下为啥不间接解决 error 和 stackTrace 搞个包装类 Report。因为将异样放弃到本地或者服务器后盾中咱们免不了要增加额定数据不便定位问题,比方机型信息,利用信息和平台等信息,能更加无效的还原 error 呈现的场景。

看源码能够发现找不到一个叫做 Reporter 的对象,那么这个对象为啥要接管和决策 Report 呢?它想干嘛?Reporter 对象其实是 ReportMode 对象及其子类,ReportMode 是具备显示和决策 Report 对象的能力,接管 Report 就是为了显示,决策就是能够勾销持续解决 Report 或者持续解决它。说白了就是一个给用户可查看异样的视图接口。

//这个类次要作用//1. 出现异样堆栈不同UI给用户操作:比方是以对话框,还是以页面,还是以告诉栏,还是以终端日志//2. 其余设置都是为显示1中UI服务的,比方以后UI是什么语言显示,以后UI呈现是否须要上下文等。abstract class ReportMode {  late ReportModeAction _reportModeAction;  LocalizationOptions? _localizationOptions;  // ignore: use_setters_to_change_properties  /// Set report mode action.  void setReportModeAction(ReportModeAction reportModeAction) {    _reportModeAction = reportModeAction;  }  /// Code which should be triggered if new error has been caught and core  /// creates report about this.  ///该办法下就会实现对应的UI,如弹框就会在这里弹出来。  void requestAction(Report report, BuildContext? context);  /// On user has accepted report  ///这个会被上述UI中相似”接管”的按钮对立调用  void onActionConfirmed(Report report) {    _reportModeAction.onActionConfirmed(report);  }  /// On user has rejected report  ///这个会被上述UI中相似”勾销”的按钮对立调用  void onActionRejected(Report report) {    _reportModeAction.onActionRejected(report);  }  /// Check if given report mode requires context to run  ///以后模式下UI是否须要上下文反对。即Context  bool isContextRequired() {    return false;  }  ///...}

ReportMode 子类

从下面不难看出,为什么 Catcher 能够反对异样多种 UI 显示成果都是 ReportMode 的功绩,你能够扩大它让它实现你想要的款式。这里波及一个惯例是设计思维,形象。 因为需要是出现不一样的 UI,有对话框款式,有告诉栏款式,还有页面款式,这几个款式外面雷同的就是接管同样的 Report 数据,公共的接管和回绝按钮。于是雷同货色能够被抽到父类中,于是有了 requestAction,onActionConfirmed 和 onActionRejected 的行为。

意识下面 ReportMode 要害的 UI 接口,持续主流程:

void _reportError(    dynamic error,    dynamic stackTrace, {    FlutterErrorDetails? errorDetails,  }) async {    //...    final Report report = Report(      error,      stackTrace,      //....     );    //...    if (reportMode.isContextRequired()) {      if (_isContextValid()) {        reportMode.requestAction(report, _getContext());      } else {        _logger.warning(          "Couldn't use report mode because you didn't provide navigator key. Add navigator key to use this report mode.",        );      }    } else {      reportMode.requestAction(report, null);    }  }

下面 Report 结构完之后流向了 Reporter(也就是 ReportMode), 这里留神下 isContextRequired()和\_isContextValid(), 这两个办法的作用:你在 UI 显示的时候是不是须要上下文呢,buildContext,比方 dialog 形式显示的时候,page 显示的时候,有能力显示进去。然而如果你不打算显示在 UI 上,只是显示在终端上,你就不须要 context 了,这就是 ReportMode 设计这两个办法的作用。

那么问题来了,这个 Context 到底如何设置的呢? 答案是通过 Catcher 中可选参数navigatorKey 其中流程比较简单能够自行查看源码。

如果用户设置了 DialogReportMode 之后,出现进去的就是下面成果,用户点击 Cancel 就没后文了,点击 Accept 就会持续把以后 Report 流传下去。

来看看下一个接力对象。

3. ReportHandler:默默接受下所有的人

@override  void onActionConfirmed(Report report) {    ///...    for (final ReportHandler handler in _currentConfig.handlers) {      _handleReport(report, handler);    }  }  void _handleReport(Report report, ReportHandler reportHandler) {    reportHandler        .handle(report, _getContext())        .catchError((dynamic handlerError) {      _logger.warning(        "Error occurred in ${reportHandler.toString()}: ${handlerError.toString()}",      );    }).then((result) {    }).timeout(    );  }

点击了步骤 2 中的接管,最初会到 Catcher 的 onActionConfirmed, 这里 Report 会被 CatcherOptions 中提供的 handlers 列表中每个元素顺次解决。Catcher 会日志中打印出相干的处理结果和超时等。

/// Handlers that should be used  final List<ReportHandler> handlers;/// Builds catcher options instance  CatcherOptions(    this.reportMode,    this.handlers, //...);

这里重点说下 ReportHandler 的设计跟哪个无关? 没错,就是你随心所欲的上报策略,你能够报给后盾,也能够只是显示在控制台,也能够存储到文件。

/// 次要作用是用来解决report的,比方这个report是放弃到文件还是上传到服务器,还是显示在终端。abstract class ReportHandler {  ///Logger instance  late CatcherLogger logger;  /// Method called when report has been accepted by user  ///上报处理结果,比方上传到服务器或者放弃到文件,胜利会返回true,失败返回false  Future<bool> handle(Report error, BuildContext? context);  /// Get list of supported platforms  List<PlatformType> getSupportedPlatforms();  ///Location settings  LocalizationOptions? _localizationOptions;  /// Get currently used localization options  LocalizationOptions get localizationOptions =>      _localizationOptions ?? LocalizationOptions.buildDefaultEnglishOptions();  // ignore: use_setters_to_change_properties  /// Set localization options (translations) to this report mode  void setLocalizationOptions(LocalizationOptions? localizationOptions) {    _localizationOptions = localizationOptions;  }  /// Check if given report mode requires context to run  bool isContextRequired() {    return false;  }  /// Check whether report mode should auto confirm without user confirmation.  bool shouldHandleWhenRejected() {    return false;  }}

ReportHander 子类

很容易看到,咱们能够反对上报 Report 到哪里,你甚至能够通过 SentryHandler 报到 Sentry 后盾,通过 HttpHandler 报到本人家后盾。从 ReportHandler 定义晓得,其实这些上报策略的关键点就在 Future<bool> handle(Report error, BuildContext? context) 的不同实现。无非就是对 Report error 参数的一个转换过程不同而已,你想报到 Sentry 就间接把咱们的 error 转换成 Sentry Sdk 反对的实体类格局,你想把 Error 报到本人后盾就转换成本人后盾反对格局用 http 来 post。

总结

读完 Catcher 理解其中外围原理,能够答复前言中几个问题了,Catcher 代码实现的确简略,掰着手指你都晓得 Catcher,Reportmode,ReportHander CatcherOption 其余类都能够干掉丝毫不影响整个框架失常运行。对 reportmode 和 reporthandler 的开闭准则设计上堪称无敌。

如果从工作量上来说的话前言外面的集体根底版本只能算实现了监控的 1/3 ,还有 2/3 的工作没做,只能算刚刚开始而已,所以有时候真的是你眼中的完满在大佬背后只是井底视线。。。

设计模式

继承和多态:Reportmode 和它的子类们,reportHandler 和它的子类们 都是通过多态来让程序更有弹性。

遇到的问题

上传到 Sentry 后发现堆栈不打印业务相干的行数。解决办法如下:

https://github.com/jhomlala/catcher/pull/225

长处

  1. 整个流程连贯清晰,reportMode 和 reportHandler,CacherOptions 三个要害对象合乎开闭准则,扩展性强。
  2. CatcherOptions 中的字段设计精密,思考到了不同需要场景,比方反对指定异样的 Handler 解决,反对疏忽某些指定异样,反对减少异样日志增加额定信息,反对屏蔽掉设施信息中敏感字段,感觉作者思考得好细。
  3. 反对异样存储到文件和上传到网络,反对传输到其余出名 flutter 后盾,如 Sentry 等。

毛病

  1. 异样解决和上传过程在 main 线程中,对解决和上报操作都做了工夫距离限度进行去重和抛弃解决。是否能够将其放到子线程中。
  2. 超时解决的 report 未序列化到数据库中,以备后续上传,上传都是一次性的。
  3. Report 包装过程太固定无奈自定义,比方我须要自定义设施信息的获取过程这样就须要批改源码了。
  4. 没有思考 Flutter engine 和 Native 异样的扩大解决状况,尽管他们不属于 Flutter Error 的范畴。
欢送搜寻公众号:【码里特地有禅】 外面整顿收集了最具体的Flutter进阶与优化指南。关注我,获取我的最新文章~

参考链接

Report errors to a service | Flutter

jhomlala/catcher: Flutter error catching & handling plugin. Handles and reports exceptions in your app!

[[译] 应用 Catcher 解决 Flutter 谬误 - 掘金](https://juejin.cn/post/684490...)

本文由mdnice多平台公布