Flutter實(shí)戰(zhàn) Flutter異常捕獲

2021-03-06 17:12 更新

在介紹 Flutter 異常捕獲之前必須先了解一下 Dart 單線程模型,只有了解了 Dart 的代碼執(zhí)行流程,我們才能知道該在什么地方去捕獲異常。

#2.6.1 Dart單線程模型

在 Java 和 Objective-C(以下簡(jiǎn)稱“OC”)中,如果程序發(fā)生異常且沒有被捕獲,那么程序?qū)?huì)終止,但是這在 Dart 或 JavaScript 中則不會(huì)!究其原因,這和它們的運(yùn)行機(jī)制有關(guān)系。Java 和 OC 都是多線程模型的編程語言,任意一個(gè)線程觸發(fā)異常且該異常未被捕獲時(shí),就會(huì)導(dǎo)致整個(gè)進(jìn)程退出。但 Dart 和 JavaScript 不會(huì),它們都是單線程模型,運(yùn)行機(jī)制很相似(但有區(qū)別),下面我們通過 Dart 官方提供的一張圖來看看 Dart 大致運(yùn)行原理:

圖2-12

Dart 在單線程中是以消息循環(huán)機(jī)制來運(yùn)行的,其中包含兩個(gè)任務(wù)隊(duì)列,一個(gè)是“微任務(wù)隊(duì)列” microtask queue,另一個(gè)叫做“事件隊(duì)列” event queue。從圖中可以發(fā)現(xiàn),微任務(wù)隊(duì)列的執(zhí)行優(yōu)先級(jí)高于事件隊(duì)列。

現(xiàn)在我們來介紹一下 Dart 線程運(yùn)行過程,如上圖中所示,入口函數(shù) main() 執(zhí)行完后,消息循環(huán)機(jī)制便啟動(dòng)了。首先會(huì)按照先進(jìn)先出的順序逐個(gè)執(zhí)行微任務(wù)隊(duì)列中的任務(wù),事件任務(wù)執(zhí)行完畢后程序便會(huì)退出,但是,在事件任務(wù)執(zhí)行的過程中也可以插入新的微任務(wù)和事件任務(wù),在這種情況下,整個(gè)線程的執(zhí)行過程便是一直在循環(huán),不會(huì)退出,而 Flutter 中,主線程的執(zhí)行過程正是如此,永不終止。

在 Dart 中,所有的外部事件任務(wù)都在事件隊(duì)列中,如 IO、計(jì)時(shí)器、點(diǎn)擊、以及繪制事件等,而微任務(wù)通常來源于 Dart 內(nèi)部,并且微任務(wù)非常少,之所以如此,是因?yàn)槲⑷蝿?wù)隊(duì)列優(yōu)先級(jí)高,如果微任務(wù)太多,執(zhí)行時(shí)間總和就越久,事件隊(duì)列任務(wù)的延遲也就越久,對(duì) 于GUI 應(yīng)用來說最直觀的表現(xiàn)就是比較卡,所以必須得保證微任務(wù)隊(duì)列不會(huì)太長(zhǎng)。值得注意的是,我們可以通過Future.microtask(…)方法向微任務(wù)隊(duì)列插入一個(gè)任務(wù)。

在事件循環(huán)中,當(dāng)某個(gè)任務(wù)發(fā)生異常并沒有被捕獲時(shí),程序并不會(huì)退出,而直接導(dǎo)致的結(jié)果是當(dāng)前任務(wù)的后續(xù)代碼就不會(huì)被執(zhí)行了,也就是說一個(gè)任務(wù)中的異常是不會(huì)影響其它任務(wù)執(zhí)行的。

#2.6.2 Flutter異常捕獲

Dart 中可以通過try/catch/finally來捕獲代碼塊異常,這個(gè)和其它編程語言類似,如果讀者不清楚,可以查看Dart語言文檔,不再贅述,下面我們看看 Flutter 中的異常捕獲。

#Flutter框架異常捕獲

Flutter 框架為我們?cè)诤芏嚓P(guān)鍵的方法進(jìn)行了異常捕獲。這里舉一個(gè)例子,當(dāng)我們布局發(fā)生越界或不合規(guī)范時(shí),F(xiàn)lutter 就會(huì)自動(dòng)彈出一個(gè)錯(cuò)誤界面,這是因?yàn)?Flutter 已經(jīng)在執(zhí)行 build 方法時(shí)添加了異常捕獲,最終的源碼如下:

@override
void performRebuild() {
 ...
  try {
    //執(zhí)行build方法  
    built = build();
  } catch (e, stack) {
    // 有異常時(shí)則彈出錯(cuò)誤提示  
    built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
  } 
  ...
}      

可以看到,在發(fā)生異常時(shí),F(xiàn)lutter 默認(rèn)的處理方式是彈一個(gè) ErrorWidget,但如果我們想自己捕獲異常并上報(bào)到報(bào)警平臺(tái)的話應(yīng)該怎么做?我們進(jìn)入_debugReportException()方法看看:

FlutterErrorDetails _debugReportException(
  String context,
  dynamic exception,
  StackTrace stack, {
  InformationCollector informationCollector
}) {
  //構(gòu)建錯(cuò)誤詳情對(duì)象  
  final FlutterErrorDetails details = FlutterErrorDetails(
    exception: exception,
    stack: stack,
    library: 'widgets library',
    context: context,
    informationCollector: informationCollector,
  );
  //報(bào)告錯(cuò)誤 
  FlutterError.reportError(details);
  return details;
}

我們發(fā)現(xiàn),錯(cuò)誤是通過FlutterError.reportError方法上報(bào)的,繼續(xù)跟蹤:

static void reportError(FlutterErrorDetails details) {
  ...
  if (onError != null)
    onError(details); //調(diào)用了onError回調(diào)
}

我們發(fā)現(xiàn)onErrorFlutterError的一個(gè)靜態(tài)屬性,它有一個(gè)默認(rèn)的處理方法 dumpErrorToConsole,到這里就清晰了,如果我們想自己上報(bào)異常,只需要提供一個(gè)自定義的錯(cuò)誤處理回調(diào)即可,如:

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

這樣我們就可以處理那些Flutter為我們捕獲的異常了,接下來我們看看如何捕獲其它異常。

#其它異常捕獲與日志收集

在 Flutter 中,還有一些 Flutter 沒有為我們捕獲的異常,如調(diào)用空對(duì)象方法異常、Future 中的異常。在 Dart 中,異常分兩類:同步異常和異步異常,同步異??梢酝ㄟ^try/catch捕獲,而異步異常則比較麻煩,如下面的代碼是捕獲不了Future的異常的:

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

Dart 中有一個(gè)runZoned(...) 方法,可以給執(zhí)行對(duì)象指定一個(gè) Zone。Zone 表示一個(gè)代碼執(zhí)行的環(huán)境范圍,為了方便理解,讀者可以將 Zone 類比為一個(gè)代碼執(zhí)行沙箱,不同沙箱的之間是隔離的,沙箱可以捕獲、攔截或修改一些代碼行為,如 Zone 中可以捕獲日志輸出、Timer 創(chuàng)建、微任務(wù)調(diào)度的行為,同時(shí) Zone 也可以捕獲所有未處理的異常。下面我們看看runZoned(...)方法定義:

R runZoned<R>(R body(), {
    Map zoneValues, 
    ZoneSpecification zoneSpecification,
    Function onError,
}) 

  • zoneValues: Zone 的私有數(shù)據(jù),可以通過實(shí)例zone[key]獲取,可以理解為每個(gè)“沙箱”的私有數(shù)據(jù)。

  • zoneSpecification:Zone 的一些配置,可以自定義一些代碼行為,比如攔截日志輸出行為等,舉個(gè)例子:

下面是攔截應(yīng)用中所有調(diào)用print輸出日志的行為。

  main() {
    runZoned(() => runApp(MyApp()), zoneSpecification: new ZoneSpecification(
        print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
          parent.print(zone, "Intercepted: $line");
        }),
    );
  }

這樣一來,我們 APP 中所有調(diào)用print方法輸出日志的行為都會(huì)被攔截,通過這種方式,我們也可以在應(yīng)用中記錄日志,等到應(yīng)用觸發(fā)未捕獲的異常時(shí),將異常信息和日志統(tǒng)一上報(bào)。ZoneSpecification 還可以自定義一些其他行為,讀者可以查看API文檔。

  • onError:Zone中未捕獲異常處理回調(diào),如果開發(fā)者提供了 onError 回調(diào)或者通過ZoneSpecification.handleUncaughtError指定了錯(cuò)誤處理回調(diào),那么這個(gè) zone 將會(huì)變成一個(gè) error-zone,該 error-zone 中發(fā)生未捕獲異常(無論同步還是異步)時(shí)都會(huì)調(diào)用開發(fā)者提供的回調(diào),如:

  runZoned(() {
      runApp(MyApp());
  }, onError: (Object obj, StackTrace stack) {
      var details=makeDetails(obj,stack);
      reportError(details);
  });

這樣一來,結(jié)合上面的FlutterError.onError我們就可以捕獲我們 Flutter 應(yīng)用中全部錯(cuò)誤了!需要注意的是,error-zone 內(nèi)部發(fā)生的錯(cuò)誤是不會(huì)跨越當(dāng)前 error-zone 的邊界的,如果想跨越 error-zone 邊界去捕獲異常,可以通過共同的“源”zone 來捕獲,如:

  var future = new Future.value(499);
  runZoned(() {
    var future2 = future.then((_) { throw "error in first error-zone"; });
    runZoned(() {
        var future3 = future2.catchError((e) { print("Never reached!"); });
    }, onError: (e) { print("unused error handler"); });
  }, onError: (e) { print("catches error of first error-zone."); });

#總結(jié)

我們最終的異常捕獲和上報(bào)代碼大致如下:

void collectLog(String line){
    ... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
    ... //上報(bào)錯(cuò)誤和日志邏輯
}


FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
    ...// 構(gòu)建錯(cuò)誤信息
}


void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    reportErrorAndLog(details);
  };
  runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
        collectLog(line); // 收集日志
      },
    ),
    onError: (Object obj, StackTrace stack) {
      var details = makeDetails(obj, stack);
      reportErrorAndLog(details);
    },
  );
}
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)