Flutter异常搜集和上报

简介

Flutter中的异常虽然不像Native那样会直接导致app crash,但也是不容忽视的. 比如widget构建失败,又或是某个网络请求解析失败,所以针对flutter我们也需要有一套规则来捕捉异常,下面主要是介绍异常类型, 全局异常捕捉的三种方式异常报告的几种形式, 在查看了Isolate,Future,FlutterError.onError,相关的代码实践出来的.

Flutter中常见的异常

Flutter中最最常见的就是空指针异常了,关于可选类型这块始终是Flutter这门语言的痛点之一,总之Flutter在数据结构转换这块和可选类型和移动端语言还是有很大差距的,希望官方快点优化吧。

字典转换,类型推倒,文件读取,网络请求错误,布局溢出,数组越界等,插件通信异常等等,通常用Error和Exception来描述

1.Error: 用于定义程序执行错误的对象

Error (dart.core)
    AsyncError (dart.async) 
    JsonUnsupportedObjectError (dart.convert)
        JsonCyclicError (dart.convert)
    LateInitializationErrorImpl (dart._internal)
    FlutterError (assertions.dart)
    RemoteError (dart.isolate)
    UnderflowError (quiver.async)
    MatchError (quiver.testing.equality)
    FallThroughError (dart.core)
    CastError (dart.core)
    UnsupportedError (dart.core)
        UnimplementedError (dart.core)
    ConcurrentModificationError (dart.core)
    LateInitializationError (dart.core)
        LateInitializationErrorImpl (dart._internal)
    OutOfMemoryError (dart.core)
    AbstractClassInstantiationError (dart.core)
    NoSuchMethodError (dart.core)
    TypeError (dart.core)
    UnimplementedError (dart.core)
    NullThrownError (dart.core)
    AssertionError (dart.core)
        FlutterError (assertions.dart)
    StackOverflowError (dart.core)
    CyclicInitializationError (dart.core)
    StateError (dart.core)
    ArgumentError (dart.core)
        IndexError (dart.core)
        RangeError (dart.core)
  1. Exception: 由dartVM和自定义的dart代码手动抛出
Exception (dart.core)
    DeferredLoadException (dart.async)
    TimeoutException (dart.async)
    IsolateSpawnException (dart.isolate)
    IOException (dart.io)
        HttpException (dart._http)
        WebSocketException (dart._http)
        FileSystemException (dart.io)
        ProcessException (dart.io)
        SignalException (dart.io)
        TlsException (dart.io)
        SocketException (dart.io)
        StdoutException (dart.io)
        StdinException (dart.io)
    PlatformException (message_codec.dart)
    MissingPluginException (message_codec.dart)
    TickerCanceled (ticker.dart)
    NetworkImageLoadException (image_provider.dart)
    PathException (path_exception.dart)
    UsageException (usage_exception.dart)
    SourceSpanException (span_exception.dart)
        MultiSourceSpanException (span_exception.dart)
        SourceSpanFormatException (span_exception.dart)
    ImageException (image_exception.dart)
    ParserException (petitparser.core.contexts.exception)
    XmlException (xml.utils.exceptions)
        XmlNodeTypeException (xml.utils.exceptions)
        XmlParserException (xml.utils.exceptions)
        XmlParentException (xml.utils.exceptions)
        XmlTagException (xml.utils.exceptions)
    ClosedException (closed_exception.dart)
    RemoteException (remote_exception.dart)
        _RemoteTestFailure (remote_exception.dart)
    FormatException (dart.core)
        ArchiveException (archive_exception.dart)
        ArgParserException (arg_parser_exception.dart)
        MultiSourceSpanFormatException (span_exception.dart)
        SourceSpanFormatException (span_exception.dart)
        XmlParserException (xml.utils.exceptions)
    IntegerDivisionByZeroException (dart.core)
    _Exception (dart.core)
  • ErrorException他们都代表了异常,但是从命名上来看似乎Error级别的日志更倾向于程序执行错误,不可预知的问题,如dart vm内部抛出的异常,数据严重级别较高的异常,甚至会让程序直接瘫痪; Exception更多的则是开发者自定义的异常,预知到的问题,并做相应的try catch去处理这种case.

  • 所以这就要求我们在程序开发时要有较强的安全意思,合理利用Exception定义函数可能出现的异常,并做好相应的try catch和备注

  • 对于Error类型的异常,要提前做好校验,如TypeError,确保代码的准确性,对于FutureStream这类的异步操作如果使用了await关键字,一定要使用try catch,否则它将会直接block后面的执行的代码,尤其是在程序启动阶段需要格外注意。

Flutter中的异常捕捉

通过阅读flutter framework层的源代码不难发现捕获异常主要由Isolate和Zone,还有Flutter.error,此外基于Zone封装而来的Future和Stream都可以进行异常捕捉.因为他们的依赖关系如下:

Stream -> Future -> Zone -> ParentZone -> Isolate, Stream作为最小的单位,如果错误发生在Stream类,我们可以手动hook住优先拦截,如果不做处理将会层层传递, StreamFuture主要用于业务逻辑的编写,我们可以根据业务场景选择捕获和忽略,如果是有await这类的关键字,则一定要使用异常捕捉,不然它会直接抛出一行到当前的zone,会block住后面代码的执行.

在启动时注册Isolate和currentZone,和FlutterError.onError事件能够获取到app所有的异常了,同时记录当前的异常堆栈。

此外还可以进一步将异常堆栈数据发送至后台,或者是发送邮件到开发者,当然也可以直接通过设置后门开关,将异常堆栈的信息显示的展示在widget上。

Flutter error捕捉

  • 它主要侧重于Flutter框架层的异常输出,如widget构建,图片读取,RenderObject绘制,点击事件的分发,测试框架,统计分析。相关的类如下:
dev/benchmarks/macrobenchmarks/lib/src/web/recorder.dart:
packages/flutter/lib/src/foundation/assertions.dart:
packages/flutter/lib/src/gestures/binding.dart:
packages/flutter/lib/src/gestures/pointer_router.dart:
packages/flutter/lib/src/painting/image_provider.dart:
packages/flutter/lib/src/widgets/fade_in_image.dart:
packages/flutter/lib/src/widgets/framework.dart:
packages/flutter/lib/src/widgets/image.dart:
packages/flutter/lib/src/widgets/widget_inspector.dart:
packages/flutter/test/rendering/rendering_tester.dart:
packages/flutter_test/lib/src/binding.dart:
  • 使用方式
    FlutterError.onError = (FlutterErrorDetails details) async {
      _reportError(details.exception, details.stack, errorDetails: details);
    };

Zone和Isolate的异常捕捉

  • 开辟一个新的Zone来捕捉,这种方案有一些局限性,它只能捕捉到Zone内部的异常,由于Zone的异常会逐级上抛给parent,所以我们可以利用这个特点,将程序的根Widget加入到当前的Zone,这样就能捕获到所有遗漏的异常的。
    runZonedGuarded<Future<void>>(() async {
      if (ensureInitialized) {
        WidgetsFlutterBinding.ensureInitialized();
      }
      runApp(rootWidget);
    }, (dynamic error, StackTrace stackTrace) {
      _reportError(error, stackTrace);
    });
  • Isolate为flutter的提供了独立的进程空间,程序运行也是依赖于Isolate的创建,所有的函数包括Zone.run都是在这个对应的Isolate下运行,此外系统提供了Isolate的几个钩子函数,方便我们拦截对应的回掉事件,通过注册ErrorListener就能实现全局错误的监听,
  Isolate.current.addErrorListener(new RawReceivePort((dynamic pair) async {
        var isolateError = pair as List<dynamic>;
        _reportError(
          isolateError.first.toString(),
          isolateError.last.toString(),
        );
      }).sendPort);

处理异常数据

打印的堆栈信息可以发送到指定的后台服务用于测试
还可以以邮件的形式发送,便于问题排查
另外Flutter提供了ErrorWidget的构造方法,它主要有以下2个使用场景,局限于Widget的build,在实际应用中,我们可以保存一个全局的GlobalKey用来存储context,这样就能在其他出现的异常部分也能创建错误的Widget并显示在界面上了。

packages/flutter/lib/src/widgets/layout_builder.dart:
  102          } catch (e, stack) {
  103:           built = ErrorWidget.builder(
  104              _debugReportException(

  118        } catch (e, stack) {
  119:         built = ErrorWidget.builder(
  120            _debugReportException(

packages/flutter/lib/src/widgets/sliver.dart:
  1628    FlutterError.reportError(details);
  1629:   return ErrorWidget.builder(details);

总结

  1. 任何时候都不要对于类型的非空判定,只要不是百分百不为空就必须得预处理,设置默认值或者添加可选操作符号,如 optialValue?.propertyoptialValue ?? 0

  2. 合理的定义Expection定义可能出现的异常并捕获,比如解析,类型推导,缓存读取,例如

try{
  final userInfo = await servie.getUserInfo();
} on NetExpection catch (e, string){
   ...
}
  1. 定义全局的异常捕捉
    FlutterError.onError = (FlutterErrorDetails details) async {
      ...
    };
        FlutterError.onError = (FlutterErrorDetails details) async {
      _reportError(details.exception, details.stack, errorDetails: details);
    };
    Isolate.current.addErrorListener(new RawReceivePort((dynamic pair) async {
        ...
    }).sendPort);
    runZonedGuarded<Future<void>>(() async {
      if (ensureInitialized) {
        WidgetsFlutterBinding.ensureInitialized();
      }
      runApp(rootWidget);
    }, (dynamic error, StackTrace stackTrace) {
      ...
    });

通过GlobalKey<NavigatorState>获取到当前的context,将错误信息通过widget输出到屏幕上;在实际使用的时候会比较频繁,建议设置个后门开关,选择性的弹出;继续扩展,发送邮件调用api,发送至后台;至此,一个简易的 bugReport就搜集完成了,如果不追求排版格式几十行代码就搞定了,测试过了,具体可以结合官方提供的[https://pub.dartlang.org/packages/sentry](https://pub.dartlang.org/packages/sentry)

  • Future和Zone相关的异常捕获测试

class CustomException implements Exception {
    CustomException({String message});
}

/**
- 当Future在构造时注册了OnError事件,将会拦截其他的所有onError事件
result:
start
end
onError Instance of 'CustomException' `StackTrace.current` 
*/
void testFutureOnErrorHasMostPriority(){
   
   print('start');
   runZonedGuarded((){
   final  future = Future.error(CustomException(message: 'CustomException'));
     future.then((value){
       print('then value $value');
     },onError: (e,s){   //最高优先级
       print('onError $e `StackTrace.current`');
     } ).catchError((e, s){
       print('catchError $e `StackTrace.current`');
       return null;
     }, test: (object) {   
       if (object is CustomException) {
         print('test object $object true'); //当为true时这是一个可恢复的错误,交由`catchError`继续处理
         return true;
       } else {
         print('test object $object false');//当为false时不可恢复错误,交友 `runZonedGuarded`的`onError`处理
         return false;
       }
     });
   },(e,s){
     print('runZonedGuarded onError: $e $s');
   });
   print('end');
}

/**
- 当testObjet接收到Error时可以校验,如果确定不是error,返回true,可以由`catchError`转换成替换值
start
end
test object Instance of 'CustomException' true
catchError Instance of 'CustomException' `StackTrace.current`
Exited
*/

void testFutureOnErrorDelegateToSelfTestAndCatch(){
   
   print('start');
   runZonedGuarded((){
   final  future = Future.error(CustomException());
     future.then((value){
       print('then value $value');
     }).catchError((e, s){
       print('catchError $e `StackTrace.current`');
       return null;
     }, test: (object) {   
        if (object is CustomException) {
         print('test object $object false');
         return false;
       } else {
         print('test object $object false');
         return false;
       }
 
     });
   },(e,s){
     print('runZonedGuarded onError: $e $s');
   });
   print('end');
}

/**
- Future处理不了的异常抛给`Zone`
start
end
runZonedGuarded onError: Instance of 'CustomException' 
Exited
*/

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