Flutter 異常捕獲

Flutter 異常

Flutter 異常指的是,F(xiàn)lutter 程序中 Dart 代碼運(yùn)行時(shí)意外發(fā)生的錯(cuò)誤事件。我們可以通過與 Swift 類似的 try-catch 機(jī)制來捕獲它赋除。但與 Swift 不同的是纵顾,Dart 程序不強(qiáng)制要求我們必須處理異常蔽午。

這是因?yàn)椋?code>Dart 采用事件循環(huán)的機(jī)制來運(yùn)行任務(wù),所以各個(gè)任務(wù)的運(yùn)行狀態(tài)是互相獨(dú)立的。也就是說,即便某個(gè)任務(wù)出現(xiàn)了異常我們沒有捕獲它浮禾,Dart 程序也不會(huì)退出交胚,只會(huì)導(dǎo)致當(dāng)前任務(wù)后續(xù)的代碼不會(huì)被執(zhí)行,用戶仍可以繼續(xù)使用其他功能盈电。

Dart 異常蝴簇,根據(jù)來源又可以細(xì)分為 App 異常Framework 異常。Flutter 為這兩種異常提供了不同的捕獲方式匆帚。

APP異常捕獲

App 異常熬词,就是應(yīng)用代碼的異常,通常由未處理應(yīng)用層其他模塊所拋出的異常引起吸重。根據(jù)異常代碼的執(zhí)行時(shí)序互拾,App 異常可以分為兩類嚎幸,即同步異常異步異常同步異常可以通過 try-catch 機(jī)制捕獲颜矿,異步異常則需要采用Future 提供的catchError語句捕獲。

這兩種異常的捕獲方式嫉晶,如下代碼所示:

// 使用 try-catch 捕獲同步異常
try {
    throw SYReportException('發(fā)生一個(gè)dart 同步異常');
}
catch(e) {
  print(e);
}
 
// 使用 catchError 捕獲異步異常
Future.delayed(Duration(seconds: 1)).then((e) {
  if (sendFlag) {
    print('異步異常發(fā)生之前 >>>>>>>>>>>');
    throw SYReportException('發(fā)生一個(gè)dart 異步異常');
  }
  print('異步異常后執(zhí)行的代碼 <<<<<<<<<<<');
}).catchError((error){
print('error); 
});
    
// 注意骑疆,以下代碼無法捕獲異步異常

try {
    Future.delayed(Duration(seconds: 1)).then((e) {
      if (sendFlag) {
        print('異步異常發(fā)生之前 >>>>>>>>>>>');
        throw SYReportException('發(fā)生一個(gè)dart 異步異常');
      }
      print('異步異常后執(zhí)行的代碼 <<<<<<<<<<<');
    });
} catch (e) {
    print("這是不會(huì)執(zhí)行的. ");
}

需要注意的是,這兩種方式是不能混用的替废」棵可以看到,在上面的代碼中椎镣,我們是無法使用 try-catch 去捕獲一個(gè)異步調(diào)用所拋出的異常的诈火。

同步的 try-catch異步的 catchError,為我們提供了直接捕獲特定異常的能力状答,而如果我們想集中管理代碼中的所有異常柄瑰,F(xiàn)lutter 也提供了 Zone.runZoned方法。

我們可以給代碼執(zhí)行對(duì)象指定一個(gè) Zone剪况,在 Dart 中,Zone表示一個(gè)代碼執(zhí)行的環(huán)境范圍蒲跨,其概念類似沙盒译断,不同沙盒之間是互相隔離的。如果我們想要觀察沙盒中代碼執(zhí)行出現(xiàn)的異常或悲,沙盒提供了 onError 回調(diào)函數(shù)孙咪,攔截那些在代碼執(zhí)行對(duì)象中的未捕獲異常。

在下面的代碼中巡语,我們將可能拋出異常的語句放置在了 Zone 里翎蹈。可以看到男公,在沒有使用try-catchcatchError 的情況下荤堪,無論是同步異常還是異步異常,都可以通過Zone 直接捕獲到:

runZoned(() {
  // 同步拋出異常
  throw SYReportException('發(fā)生一個(gè)dart 同步異常');
}, onError: (dynamic e, StackTrace stack) {
  print('zone捕獲到了同步異常');
});
 
runZoned(() {
  // 異步拋出異常
  Future.delayed(Duration(seconds: 1))
      .then((e) => throw SYReportException('發(fā)生一個(gè)dart 異步異常'));
}, onError: (dynamic e, StackTrace stack) {
  print('zone捕獲到了異步異常');
});

因此,如果我們想要集中捕獲 Flutter 應(yīng)用中的未處理異常澄阳,可以把 main 函數(shù)中的 runApp 語句也放置在 Zone 中拥知。這樣在檢測(cè)到代碼中運(yùn)行異常時(shí),我們就能根據(jù)獲取到的異常上下文信息碎赢,進(jìn)行統(tǒng)一處理了:

runZonedGuarded(() {
    runApp(MyApp());
}, (error, stackTrace) {
    // 這個(gè)閉包中發(fā)生的Exception是捕獲不到的 @山竹
    SYExceptionReportChannel.reportException(error, stackTrace);
}, zoneSpecification: ZoneSpecification(
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
  // 記錄所有的打印日志
  parent.print(zone, "line是啥:$line");
},
));

接下來低剔,我們?cè)倏纯?Framework 異常應(yīng)該如何捕獲吧。

Framework 異常的捕獲

Framework 異常肮塞,就是 Flutter 框架引發(fā)的異常襟齿,通常是由應(yīng)用代碼觸發(fā)了 Flutter 框架底層的異常判斷引起的。比如枕赵,當(dāng)布局不合規(guī)范時(shí)猜欺,F(xiàn)lutter 就會(huì)自動(dòng)彈出一個(gè)觸目驚心的紅色錯(cuò)誤界面,如下所示:

image

這其實(shí)是因?yàn)椋?code>Flutter 框架在調(diào)用 build 方法構(gòu)建頁(yè)面時(shí)進(jìn)行了 try-catch 的處理烁设,并提供了一個(gè) ErrorWidget替梨,用于在出現(xiàn)異常時(shí)進(jìn)行信息提示:

圖片.png

這個(gè)頁(yè)面反饋的信息比較豐富,適合開發(fā)期定位問題装黑。但如果讓用戶看到這樣一個(gè)頁(yè)面副瀑,就很糟糕了。因此恋谭,我們通常會(huì)重寫 ErrorWidget.builder 方法糠睡,將這樣的錯(cuò)誤提示頁(yè)面替換成一個(gè)更加友好的頁(yè)面。

下面的代碼演示了自定義錯(cuò)誤頁(yè)面的具體方法疚颊。在這個(gè)例子中狈孔,我們自定義了錯(cuò)誤頁(yè)面,顯示導(dǎo)航欄和可滾動(dòng)的錯(cuò)誤信息:

// 重寫 ErrorWidget 的builder材义,顯示地優(yōu)雅一些
ErrorWidget.builder = (FlutterErrorDetails details) {
  print('錯(cuò)誤widget詳細(xì)的錯(cuò)誤信息為:' + details.toString());

  return MaterialApp(
    title: 'Error Widget',
    theme: ThemeData(
      primarySwatch: Colors.red,
    ),
    home: Scaffold(
      appBar: AppBar(
        title: Text('Widget渲染異常>椤!其掂!'),
      ),
      body: _createBody(details),
    ),
  );
};

運(yùn)行效果如下所示:


圖片.png

比起之前觸目驚心的紅色錯(cuò)誤頁(yè)面油挥,自定義的看起來優(yōu)雅一些,當(dāng)然也可以找UI幫忙設(shè)計(jì)更友好的界面款熬。需要注意的是深寥,ErrorWidget.builder方法提供了一個(gè)參數(shù) details用于表示當(dāng)前的錯(cuò)誤上下文,為避免用戶直接看到錯(cuò)誤信息贤牛,這里我們并沒有將它展示到界面上惋鹅。但是,我們不能丟棄掉這樣的異常信息殉簸,需要提供統(tǒng)一的異常處理機(jī)制闰集,用于后續(xù)分析異常原因沽讹。

為了 集中處理框架異常 ,F(xiàn)lutter 提供了FlutterError 類返十,這個(gè)類的onError屬性會(huì)在接收到框架異常時(shí)執(zhí)行相應(yīng)的回調(diào)妥泉。因此,要實(shí)現(xiàn)自定義捕獲邏輯洞坑,我們只要為它提供一個(gè)自定義的錯(cuò)誤處理回調(diào)即可盲链。

在下面的代碼中,我們使用 Zone 提供的 handleUncaughtError語句迟杂,將 Flutter 框架的異常統(tǒng)一轉(zhuǎn)發(fā)到當(dāng)前的 Zone 中刽沾,這樣我們就可以統(tǒng)一使用 Zone 去處理應(yīng)用內(nèi)的所有異常了:

// framework異常捕獲,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
FlutterError.onError = (FlutterErrorDetails details) async {
    Zone.current.handleUncaughtError(details.exception, details.stack);
};

異常上報(bào)

到目前為止排拷,我們已經(jīng)捕獲到了應(yīng)用中所有的未處理異常侧漓。但如果只是把這些異常在控制臺(tái)中打印出來還是沒辦法解決問題,我們還需要把它們上報(bào)到開發(fā)者能看到的地方监氢,用于后續(xù)分析定位并解決問題布蔗。

三方,我們一般都是用bugly浪腐。如果公司有自研的bug系統(tǒng)纵揍,那就更好了。

這些異常上報(bào)议街,我們將使用MethodChannel推送給Native泽谨,由Native上報(bào)到bugly或自研的異常系統(tǒng)。

這里只展示Dart的代碼實(shí)現(xiàn)特漩,至于Native怎么實(shí)現(xiàn)Channel吧雹,自行Google即可
Dart實(shí)現(xiàn)

代碼如下:
/// flutter exception channel
class SYExceptionReportChannel {
  static const MethodChannel _channel =
      const MethodChannel('sy_exception_channel');

  // 上報(bào)異常
  static reportException(dynamic error, dynamic stack) {
    print('捕獲的異常類型 >>> : ${error.runtimeType}');
    print('捕獲的異常信息 >>> : $error');
    print('捕獲的異常堆棧 >>> : $stack');

    Map reportMap = {
      'type': "${error.runtimeType}",
      'title': error.toString(),
      'description': stack.toString()
    };

    // 得使用這個(gè)
    print('這是通過convert轉(zhuǎn)的json');
    print(jsonEncode(reportMap));

    _channel.invokeListMethod('reportException', reportMap);
  }
}

我們捕獲到的異常后,由channel推送給Native涂身,包含三個(gè)信息:

  • 異常的類型信息
  • 異常的簡(jiǎn)要說明信息(即error的toString的值)
  • 異常的堆棧信息
優(yōu)化雄卷、封裝及問題點(diǎn)

綜合上述的闡述,我們將代碼做一些封裝和優(yōu)化蛤售。

優(yōu)化:異常捕獲后龙亲,在debug和release的模式下是不一樣的處理,debug模式悍抑,直接打印到控制臺(tái)是最直觀的,release模式下杜耙,無法感知哪里出了問題搜骡,所以我們需要上報(bào),然后分析問題佑女。

區(qū)分當(dāng)前是debug還是release记靡,有一個(gè)比較巧妙的方式谈竿,代碼及注釋如下:

// 比較巧妙的一種方式判定是否是debug模式
static bool get isInDebugMode {
    bool inDebugMode = false;
    // 如果debug模式下會(huì)觸發(fā)賦值,只有在debug模式下才會(huì)執(zhí)行assert
    assert(inDebugMode = true);
    return inDebugMode;
}

基于上述的思路摸吠,我們將未捕獲的異常轉(zhuǎn)發(fā)到zone做一個(gè)判斷:

// framework異常捕獲空凸,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
    FlutterError.onError = (FlutterErrorDetails details) async {
      // debug模式
      if (ExceptionReportUtil.isInDebugMode) {
        // 打印到控制臺(tái)
        FlutterError.dumpErrorToConsole(details);

        // release模式
      } else {
        // 轉(zhuǎn)發(fā)到zone
        Zone.current.handleUncaughtError(details.exception, details.stack);
      }
    };

封裝:main函數(shù)中的代碼,自然是越簡(jiǎn)練越好寸痢,但將未捕獲的異常轉(zhuǎn)發(fā)到zone及錯(cuò)誤Widget重寫必須放在main中呀洲,所以抽取一個(gè)工具類
ExceptionReportUtil

/// 工具類
class ExceptionReportUtil {
  // 比較巧妙的一種方式判定是否是debug模式
  static bool get isInDebugMode {
    bool inDebugMode = false;
    // 如果debug模式下會(huì)觸發(fā)賦值,只有在debug模式下才會(huì)執(zhí)行assert
    assert(inDebugMode = true);
    return inDebugMode;
  }

  // 初始化異常捕獲配置
  static void initExceptionCatchConfig() {
    // framework異常捕獲啼止,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
    FlutterError.onError = (FlutterErrorDetails details) async {
      // debug模式
      if (ExceptionReportUtil.isInDebugMode) {
        // 打印到控制臺(tái)
        FlutterError.dumpErrorToConsole(details);

        // release模式
      } else {
        // 轉(zhuǎn)發(fā)到zone
        Zone.current.handleUncaughtError(details.exception, details.stack);
      }
    };

    // 重寫 ErrorWidget 的builder道逗,顯示地優(yōu)雅一些
    ErrorWidget.builder = (FlutterErrorDetails details) {
      print('錯(cuò)誤widget詳細(xì)的錯(cuò)誤信息為:' + details.toString());

      return MaterialApp(
        title: 'Error Widget',
        theme: ThemeData(
          primarySwatch: Colors.red,
        ),
        home: Scaffold(
          appBar: AppBar(
            title: Text('Widget渲染異常!O追场滓窍!'),
          ),
          body: _createBody(details),
        ),
      );
    };
  }

  // 創(chuàng)建錯(cuò)誤widget body
  static Widget _createBody(dynamic details) {
    // 正確代碼
    return Container(
      color: Colors.white,
      child: SingleChildScrollView(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Text(
            details.toString(),
            style: TextStyle(color: Colors.red),
          ),
        ),
      ),
    );
  }
}

問題點(diǎn): 在runZonedGuarded函數(shù)的閉包中接收未捕獲的異常,然后上報(bào)巩那,如果執(zhí)行該閉包中的代碼發(fā)生異常吏夯,是無法捕獲的:

代碼及注釋如下:
main(List<String> args) {
  // 初始化Exception 捕獲配置
  ExceptionReportUtil.initExceptionCatchConfig();

  runZonedGuarded(() {
    runApp(MyApp());
  }, (error, stackTrace) {
    // 這個(gè)閉包中發(fā)生的Exception是捕獲不到的 @山竹
    SYExceptionReportChannel.reportException(error, stackTrace);
  }, zoneSpecification: ZoneSpecification(
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
      // 記錄所有的打印日志
      parent.print(zone, "line是啥:$line");
    },
  ));
}

我們通過SYExceptionReportChannel.reportException(error, stackTrace)將錯(cuò)誤上報(bào)給Native,但在Native如果沒有實(shí)現(xiàn)channel的鏈接即横,那么必然會(huì)報(bào)MissingPluginException噪生,這個(gè)異常是不在當(dāng)前的zone中的,所以無法捕獲令境。

KBMore

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末杠园,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子舔庶,更是在濱河造成了極大的恐慌抛蚁,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件惕橙,死亡現(xiàn)場(chǎng)離奇詭異瞧甩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)弥鹦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門肚逸,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人彬坏,你說我怎么就攤上這事朦促。” “怎么了栓始?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵务冕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我幻赚,道長(zhǎng)禀忆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任离熏,我火速辦了婚禮,結(jié)果婚禮上滋戳,老公的妹妹穿的比我還像新娘。我一直安慰自己喊括,他們只是感情好胧瓜,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著府喳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪钝满。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天申窘,我揣著相機(jī)與錄音弯蚜,去河邊找鬼。 笑死剃法,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的贷洲。 我是一名探鬼主播,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼优构,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了钦椭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤侥锦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后德挣,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡署照,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年吗浩,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了建芙。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片懂扼。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阀湿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情陷嘴,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布灾挨,位于F島的核電站,受9級(jí)特大地震影響劳澄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜秒拔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望砂缩。 院中可真熱鬧,春花似錦梯轻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽单绑。三九已至,卻和暖如春曹宴,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背笛坦。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工区转, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留苔巨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓侄泽,卻偏偏與公主長(zhǎng)得像蜻韭,于是被迫代替她去往敵國(guó)和親悼尾。 傳聞我的和親對(duì)象是個(gè)殘疾皇子肖方,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容

  • 無論我們的應(yīng)用寫得多么完美、測(cè)試得多么全面析桥,總是無法完全避免線上的異常問題。 這些異常活翩,可能是因?yàn)椴怀浞值臋C(jī)型適配...
    半心_忬閱讀 4,096評(píng)論 3 11
  • 本文內(nèi)容非原創(chuàng), 僅用于整理記錄原文鏈接??: flutter 崩潰收集 Dart線程模型及異常捕獲 Flutter...
    _白羊閱讀 8,231評(píng)論 4 5
  • Flutter異常捕獲Dart中可以通過try/catch/finally來捕獲代碼塊異常烹骨,這個(gè)和其它變成語言類似...
    秋分落葉閱讀 7,491評(píng)論 2 12
  • 今天開始看gsy_github_app_flutter入口文件就來了個(gè)runZoned,代碼如下 ErrorWid...
    waiwaaa閱讀 2,866評(píng)論 0 0
  • 在程序開發(fā)中材泄,有個(gè)非常重要的思想,《發(fā)現(xiàn)問題拉宗,解決問題》異常捕獲顯然是發(fā)現(xiàn)問題,解決問題的必要手段之一旦事,接下來我們...
    YorkLe閱讀 316評(píng)論 0 0