Flutter异常捕捉原理和异常上报

Flutter线程模型/事件机制
在介绍Flutter异常捕捉原理之前,先说明一下Dart的模型。方便我们了解Dart代码的执行流程和获取一个合适的异常捕捉切入点。

我们知道在Java中,如果程序运行发生异常并且没有做捕获处理,程序会直接终止运行发生Crash。但这种情况在Dart中会有所不同。Dart不同于Java的多线程模型,Dart和JavaScript类似属于单线程模型。Dart的单线程模型是以消息循环机制来运行的,其中包含两个任务队列,一个是“微任务队列”microtask queue,另外一个叫“事件队列” event queue。其中微任务队列优先级高于事件队列。

下图是对Dart运行原理和事件机制的一个简单说明:

image.png

如图,main()函数(Dart程序的入口函数)执行完后,消息循环机制就会启动。main中的代码将最先执行,然后再执行微任务队列中的任务(按FIFO先进先出顺序),再次是事件队列中的任务。执行顺序:Main > MicroTask > EventQueue。在事件任务执行过程中又可以插入新的微任务和事件任务,所有任务执行完毕程序就会退出。

有意思的是在事件循环中,当某个任务发生异常并没有被捕获时,程序并不会退出。而直接导致的结果是当前任务的后续代码不会被执行。也就是说一个任务中的异常是不会影响其它任务执行的。

Flutter 异常分类

  1. Flutter dart代码异常(包含app 代码异常,和framework部分异常,和未处理的异步异常)

  2. Flutter Engine 异常

Flutter Dart 代码异常

1. Dart 代码异常捕捉

Dart中也有try/catch/finally的存在,用于捕捉代码同步异常。

try {

  result = await _methodChannel

      .invokeMethod(METHOD_ADD_WALLET_START, {"cardNo": cardNo});

} catch(ignored) {
}

return result;

2. Framework异常捕捉
Flutter的框架本身也做了很多异常捕捉措施,例如著名的Flutter红屏异常(布局发生越界或者不符合规范的写法就会导致红屏),就是因为Flutter在框架中已经帮我们预埋了异常的捕获处理逻辑。这个我们可以通过Flutter的源码来了解其实现细节。

Flutter中万物皆Widget,无论是从StatefulWidget还是StatelessWidget中都能发现Widget在创建时都创建了对应的Element的。这里以StatefulWidget源码为例,发现其中会创建StatefulElement。

abstract class StatefulWidget extends Widget {

     /// Initializes [key] for subclasses.

     const StatefulWidget({ Key key }) : super(key: key);

     @override

     StatefulElement createElement() => StatefulElement(this);

   ...

   }

   进入StatefulElement,发现StatefulElement继承于ComponentElement

   class StatefulElement extends ComponentElement {

   再次追踪到ComponentElement中,会在其**performRebuild()**方法中发现熟悉的try/catch结构。

   void performRebuild() {

   ...

     Widget built;

     try {

       built = build();

       debugWidgetBuilderValue(widget, built);

     } catch (e, stack) {

       built = ErrorWidget.builder(

         _debugReportException(

           ErrorDescription('building $this'),

           e,

           stack,

           informationCollector: () sync* {

             yield DiagnosticsDebugCreator(DebugCreator(this));

           },

         ),

       );

     }

在这里我们发现发生异常时,flutter框架对异常进行了捕捉,并且创建了一个ErrorWidget来做进一步的处理。Flutter的红屏也就是从这里产生的。这里就是我们实现Flutter框架异常捕捉的切入点。 查看_debugReportException的源码来确定异常捕捉的具体实现方式。_debugReportException的源码如下:

  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.reportError(details);这一句。进入后发现:

  /// Calls [onError] with the given details, unless it is null.
     static void reportError(FlutterErrorDetails details) {
       assert(details != null);
       assert(details.exception != null);
       if (onError != null)
         onError(details);
     }
   }

继续跟踪这里的onError就会发现它是FlutterError的一个静态属性,有个名为dumpErrorToConsole的默认处理方法。

static FlutterExceptionHandler onError = dumpErrorToConsole;

所以我们只需要实现一个自定义的FlutterError.onError来处理异常就能实现Flutter框架异常的捕捉和上报。最终实现如下:

void main() {
     FlutterError.onError = (FlutterErrorDetails details) {
       reportError(details);
     };

    ...

}

3. dart的异步异常捕捉

刚才介绍的两种异常捕捉方式并不足以应对所有的flutter异常。例如下面的这种异常:

  try {    
     Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
   } catch (e) {    
     print(e)
  }

Dart有个Zone的概念,可以简单的理解为沙箱。不同的Zone相处独立,互不影响。借助于Zone就可以指定代码的执行环境,捕获、拦截或者修改代码行为。Flutter中有一个Zone.runZoned方法。

image.png

在runZoned方法的源码中我们又看到了熟悉的onError。在这里我们注入自定义的onError回调方法,用于捕获方法1、方法2中未能捕获处理的异常。 最终的实现如下:

runZoned(
     () => MyApp(),
     onError: (dynamic ex, StackTrace stack) {
       reportError(ex, stack);
     },
 );

4. 最终实现

///flutter 应用入口

   void main() {
   //  Flutter framework 异常捕获
     FlutterError.onError = (FlutterErrorDetails details) {
       bool isDebugMode = false;
       assert(() {
         isDebugMode = true;
         return true;
       }());
       if (isDebugMode) {
         FlutterError.dumpErrorToConsole(details);
       } else {
         //profile,release两个模式下下捕捉异常信息
         reportFrameworkError(details);
       }
    };

     //其他类型异常

     runZoned(
       () => runAutoSizeApp(MyApp(), width: 375, height: 667),
       onError: (dynamic ex, StackTrace stack) {
         reportError(ex, stack);
       },
     );
   }

注:针对debug环境下的异常不需要捕捉上报,我们任然让它走旧有的异常处理逻辑。

异常信息的上报

在处理异常信息上报之前我们先来看看捕捉到的Flutter日志格式。

MissingPluginException(No implementation found for method getCache on channel com.zhongan.beeline/ZACache)
#0      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319:7)
<asynchronous suspension>
#1      CachePlugin.getCache (package:ZABank/plugin/CachePlugin.dart:34:37)
#2      new HomePageViewModel (package:ZABank/home/bloc/HomePageViewModel.dart:158:17)
#3      HomePageViewModel.create (package:ZABank/home/bloc/HomePageViewModel.dart:138:23)
#4      _NewHomeTabState.build.<anonymous closure> (package:ZABank/home/NewHomeTab.dart:36:47)
#5      BuilderStateDelegate.initDelegate (package:provider/src/delegate_widget.dart:249:14)
#6      _DelegateWidgetState._initDelegate (package:provider/src/delegate_widget.dart:118:21)
#7      _DelegateWidgetState.initState (package:provider/src/delegate_widget.dart:110:5)
#8      StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4355:58)
#9      ComponentElement.mount (package:flutter/src/widgets/framework.dart:4201:5)
#10     Element.inflateWidget (package:flutter/src/widgets/framework.dart:3194:14)
#11     MultiChildRender

从上面的flutter crash日志来看,我们可以将其简单的分为两部分:异常标题(异常类型和对应异常详情说明);异常堆栈信息。将这些信息上报到不同的异常监控平台需要做不同的处理

  1. 以Android端为例,如果上报平台为bugly。我们只需要将上面的异常日志分为异常标题和异常堆栈,通过methodchannel传递到native层,然后通过bugly接口直接上报即可。以下为bugly上报接口调用示例:
if (!CrashModule.hasInitialized()) return;

    CrashReport.postException(4, excpetionType, excpetionMessage, stack, null);
  1. 如果我们的上报目标平台为firebase,那需要做更进一步的处理。因为firebase平台目前给出的接口没有bugly的灵活,上报时接口只认Java的Exception对象。因此为了将flutter异常上报到firebase平台我们需要对其进行解析转换,将其格式转换为Java的异常类型然后再上报到firebase。

    flutter异常的标题没什么好说的,可以直接作为Java Exception的detailMessage字段。

    flutter异常堆栈部分按行切割后,每一行日志还需要按规则进行拆解。
    以下为拆解规则:

#1   CachePlugin.getCache  (package:ZABank/plugin/CachePlugin.dart:34:37)
丢弃 class method file line 丢弃
#1 CachePlugin getCache package:ZABank/plugin/CachePlugin.dart 34 37

这些字段分别对应了Java中StackTraceElement类的几个字段。

 *public final class StackTraceElement implements [java.io](http://java.io).Serializable {*

 *// Normally initialized by VM (public constructor added in 1.5)*

 *private String declaringClass;*

 *private String methodName;*

 *private String fileName;*

 *private int    lineNumber;*

最终我们就能在firebase中看见上报的flutter异常。

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342