Flutter異常搜集和上報

簡介

Flutter中的異常雖然不像Native那樣會直接導致app crash,但也是不容忽視的. 比如widget構建失敗,又或是某個網(wǎng)絡請求解析失敗,所以針對flutter我們也需要有一套規(guī)則來捕捉異常,下面主要是介紹異常類型, 全局異常捕捉的三種方式異常報告的幾種形式, 在查看了Isolate,Future,FlutterError.onError,相關的代碼實踐出來的.

Flutter中常見的異常

Flutter中最最常見的就是空指針異常了,關于可選類型這塊始終是Flutter這門語言的痛點之一,總之Flutter在數(shù)據(jù)結構轉(zhuǎn)換這塊和可選類型和移動端語言還是有很大差距的,希望官方快點優(yōu)化吧。

字典轉(zhuǎn)換,類型推倒,文件讀取,網(wǎng)絡請求錯誤,布局溢出,數(shù)組越界等,插件通信異常等等,通常用Error和Exception來描述

1.Error: 用于定義程序執(zhí)行錯誤的對象

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級別的日志更傾向于程序執(zhí)行錯誤,不可預知的問題,如dart vm內(nèi)部拋出的異常,數(shù)據(jù)嚴重級別較高的異常,甚至會讓程序直接癱瘓; Exception更多的則是開發(fā)者自定義的異常,預知到的問題,并做相應的try catch去處理這種case.

  • 所以這就要求我們在程序開發(fā)時要有較強的安全意思,合理利用Exception定義函數(shù)可能出現(xiàn)的異常,并做好相應的try catch和備注

  • 對于Error類型的異常,要提前做好校驗,如TypeError,確保代碼的準確性,對于FutureStream這類的異步操作如果使用了await關鍵字,一定要使用try catch,否則它將會直接block后面的執(zhí)行的代碼,尤其是在程序啟動階段需要格外注意。

Flutter中的異常捕捉

通過閱讀flutter framework層的源代碼不難發(fā)現(xiàn)捕獲異常主要由Isolate和Zone,還有Flutter.error,此外基于Zone封裝而來的Future和Stream都可以進行異常捕捉.因為他們的依賴關系如下:

Stream -> Future -> Zone -> ParentZone -> Isolate, Stream作為最小的單位,如果錯誤發(fā)生在Stream類,我們可以手動hook住優(yōu)先攔截,如果不做處理將會層層傳遞, StreamFuture主要用于業(yè)務邏輯的編寫,我們可以根據(jù)業(yè)務場景選擇捕獲和忽略,如果是有await這類的關鍵字,則一定要使用異常捕捉,不然它會直接拋出一行到當前的zone,會block住后面代碼的執(zhí)行.

在啟動時注冊Isolate和currentZone,和FlutterError.onError事件能夠獲取到app所有的異常了,同時記錄當前的異常堆棧。

此外還可以進一步將異常堆棧數(shù)據(jù)發(fā)送至后臺,或者是發(fā)送郵件到開發(fā)者,當然也可以直接通過設置后門開關,將異常堆棧的信息顯示的展示在widget上。

Flutter error捕捉

  • 它主要側重于Flutter框架層的異常輸出,如widget構建,圖片讀取,RenderObject繪制,點擊事件的分發(fā),測試框架,統(tǒng)計分析。相關的類如下:
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內(nèi)部的異常,由于Zone的異常會逐級上拋給parent,所以我們可以利用這個特點,將程序的根Widget加入到當前的Zone,這樣就能捕獲到所有遺漏的異常的。
    runZonedGuarded<Future<void>>(() async {
      if (ensureInitialized) {
        WidgetsFlutterBinding.ensureInitialized();
      }
      runApp(rootWidget);
    }, (dynamic error, StackTrace stackTrace) {
      _reportError(error, stackTrace);
    });
  • Isolate為flutter的提供了獨立的進程空間,程序運行也是依賴于Isolate的創(chuàng)建,所有的函數(shù)包括Zone.run都是在這個對應的Isolate下運行,此外系統(tǒng)提供了Isolate的幾個鉤子函數(shù),方便我們攔截對應的回掉事件,通過注冊ErrorListener就能實現(xiàn)全局錯誤的監(jiān)聽,
  Isolate.current.addErrorListener(new RawReceivePort((dynamic pair) async {
        var isolateError = pair as List<dynamic>;
        _reportError(
          isolateError.first.toString(),
          isolateError.last.toString(),
        );
      }).sendPort);

處理異常數(shù)據(jù)

打印的堆棧信息可以發(fā)送到指定的后臺服務用于測試
還可以以郵件的形式發(fā)送,便于問題排查
另外Flutter提供了ErrorWidget的構造方法,它主要有以下2個使用場景,局限于Widget的build,在實際應用中,我們可以保存一個全局的GlobalKey用來存儲context,這樣就能在其他出現(xiàn)的異常部分也能創(chuàng)建錯誤的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定義可能出現(xiàn)的異常并捕獲,比如解析,類型推導,緩存讀取,例如

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輸出到屏幕上;在實際使用的時候會比較頻繁,建議設置個后門開關,選擇性的彈出;繼續(xù)擴展,發(fā)送郵件調(diào)用api,發(fā)送至后臺;至此,一個簡易的 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){   //最高優(yōu)先級
       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`繼續(xù)處理
         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`轉(zhuǎn)換成替換值
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');
}
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容