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

2021-03-06 17:12 更新

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

#2.6.1 Dart單線程模型

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

圖2-12

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

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

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

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

#2.6.2 Flutter異常捕獲

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

#Flutter框架異常捕獲

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

下面是攔截應(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方法輸出日志的行為都會被攔截,通過這種方式,我們也可以在應(yīng)用中記錄日志,等到應(yīng)用觸發(fā)未捕獲的異常時,將異常信息和日志統(tǒng)一上報。ZoneSpecification 還可以自定義一些其他行為,讀者可以查看API文檔。

  • onError:Zone中未捕獲異常處理回調(diào),如果開發(fā)者提供了 onError 回調(diào)或者通過ZoneSpecification.handleUncaughtError指定了錯誤處理回調(diào),那么這個 zone 將會變成一個 error-zone,該 error-zone 中發(fā)生未捕獲異常(無論同步還是異步)時都會調(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)用中全部錯誤了!需要注意的是,error-zone 內(nèi)部發(fā)生的錯誤是不會跨越當(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é)

我們最終的異常捕獲和上報代碼大致如下:

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


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


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)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號