Flutter異常捕捉原理和異常上報

Flutter線程模型/事件機制
在介紹Flutter異常捕捉原理之前邢笙,先說明一下Dart的模型严蓖。方便我們了解Dart代碼的執(zhí)行流程和獲取一個合適的異常捕捉切入點。

我們知道在Java中,如果程序運行發(fā)生異常并且沒有做捕獲處理拧额,程序會直接終止運行發(fā)生Crash宿亡。但這種情況在Dart中會有所不同常遂。Dart不同于Java的多線程模型,Dart和JavaScript類似屬于單線程模型挽荠。Dart的單線程模型是以消息循環(huán)機制來運行的克胳,其中包含兩個任務隊列,一個是“微任務隊列”microtask queue圈匆,另外一個叫“事件隊列” event queue漠另。其中微任務隊列優(yōu)先級高于事件隊列。

下圖是對Dart運行原理和事件機制的一個簡單說明:

image.png

如圖跃赚,main()函數(Dart程序的入口函數)執(zhí)行完后笆搓,消息循環(huán)機制就會啟動。main中的代碼將最先執(zhí)行纬傲,然后再執(zhí)行微任務隊列中的任務(按FIFO先進先出順序)满败,再次是事件隊列中的任務。執(zhí)行順序:Main > MicroTask > EventQueue叹括。在事件任務執(zhí)行過程中又可以插入新的微任務和事件任務算墨,所有任務執(zhí)行完畢程序就會退出。

有意思的是在事件循環(huán)中领猾,當某個任務發(fā)生異常并沒有被捕獲時米同,程序并不會退出骇扇。而直接導致的結果是當前任務的后續(xù)代碼不會被執(zhí)行。也就是說一個任務中的異常是不會影響其它任務執(zhí)行的面粮。

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紅屏異常(布局發(fā)生越界或者不符合規(guī)范的寫法就會導致紅屏)柴底,就是因為Flutter在框架中已經幫我們預埋了異常的捕獲處理邏輯婿脸。這個我們可以通過Flutter的源碼來了解其實現細節(jié)。

Flutter中萬物皆Widget柄驻,無論是從StatefulWidget還是StatelessWidget中都能發(fā)現Widget在創(chuàng)建時都創(chuàng)建了對應的Element的狐树。這里以StatefulWidget源碼為例,發(fā)現其中會創(chuàng)建StatefulElement鸿脓。

abstract class StatefulWidget extends Widget {

     /// Initializes [key] for subclasses.

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

     @override

     StatefulElement createElement() => StatefulElement(this);

   ...

   }

   進入StatefulElement抑钟,發(fā)現StatefulElement繼承于ComponentElement

   class StatefulElement extends ComponentElement {

   再次追蹤到ComponentElement中,會在其**performRebuild()**方法中發(fā)現熟悉的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));

           },

         ),

       );

     }

在這里我們發(fā)現發(fā)生異常時在塔,flutter框架對異常進行了捕捉,并且創(chuàng)建了一個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);這一句贺待。進入后發(fā)現:

  /// 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);
     }
   }

繼續(xù)跟蹤這里的onError就會發(fā)現它是FlutterError的一個靜態(tài)屬性,有個名為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就可以指定代碼的執(zhí)行環(huán)境,捕獲得院、攔截或者修改代碼行為傻铣。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環(huán)境下的異常不需要捕捉上報两踏,我們任然讓它走舊有的異常處理邏輯败京。

異常信息的上報

在處理異常信息上報之前我們先來看看捕捉到的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日志來看梦染,我們可以將其簡單的分為兩部分:異常標題(異常類型和對應異常詳情說明)赡麦;異常堆棧信息。將這些信息上報到不同的異常監(jiān)控平臺需要做不同的處理

  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異常堆棧部分按行切割后脆荷,每一行日志還需要按規(guī)則進行拆解。
    以下為拆解規(guī)則:

#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閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件观堂,死亡現場離奇詭異,居然都是意外死亡呀忧,警方通過查閱死者的電腦和手機师痕,發(fā)現死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來而账,“玉大人胰坟,你說我怎么就攤上這事∨⒎” “怎么了笔横?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵竞滓,是天一觀的道長。 經常有香客問我吹缔,道長虽界,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任涛菠,我火速辦了婚禮莉御,結果婚禮上,老公的妹妹穿的比我還像新娘俗冻。我一直安慰自己礁叔,他們只是感情好,可當我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布迄薄。 她就那樣靜靜地躺著琅关,像睡著了一般。 火紅的嫁衣襯著肌膚如雪讥蔽。 梳的紋絲不亂的頭發(fā)上涣易,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天,我揣著相機與錄音冶伞,去河邊找鬼新症。 笑死,一個胖子當著我的面吹牛响禽,可吹牛的內容都是我干的徒爹。 我是一名探鬼主播,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼芋类,長吁一口氣:“原來是場噩夢啊……” “哼隆嗅!你這毒婦竟也來了?” 一聲冷哼從身側響起侯繁,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤胖喳,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贮竟,有當地人在樹林里發(fā)現了一具尸體丽焊,經...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年坝锰,在試婚紗的時候發(fā)現自己被綠了粹懒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片重付。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡顷级,死狀恐怖,靈堂內的尸體忽然破棺而出确垫,到底是詐尸還是另有隱情弓颈,我是刑警寧澤帽芽,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站翔冀,受9級特大地震影響导街,放射性物質發(fā)生泄漏。R本人自食惡果不足惜纤子,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一搬瑰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧控硼,春花似錦泽论、人聲如沸星掰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽隔缀。三九已至幔妨,卻和暖如春鹦赎,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背误堡。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工古话, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锁施。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓煞额,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沾谜。 傳聞我的和親對象是個殘疾皇子膊毁,可洞房花燭夜當晚...
    茶點故事閱讀 45,630評論 2 359