Flutter異常捕獲和Crash崩潰日誌收集

前言

和Android中的Java語言相似,Dart中也能夠經過try/catch/finally來捕獲代碼塊異常。不一樣的是在Dart中發生異常的時候flutter APP並不會崩潰。在個人實踐中,debug版中的Dart異常會表現爲紅屏加異常信息,而release版則是空白的白屏。下面咱們就從源碼追溯Flutter的異常和捕獲android

Flutter捕獲的異常

Flutter爲咱們提供了部分異常捕獲。在flutter開發中你們確定遇到過屏幕編程紅色並帶有錯誤信息的狀況,甚至在Widget寬度越界時也會出現這樣的錯誤提示界面。雖然代碼出現了錯誤,可是並不會致使APP崩潰,Flutter會幫咱們捕獲異常。至於其中的原理,就須要咱們去看一下源碼了。 以StatelessWidget爲例,Widget會建立對應的Element,(至於爲何要進入Element裏面,請你們自行了解Widget的繪製原理)其代碼以下:編程

abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree. /// /// It is uncommon for subclasses to override this method. @override StatelessElement createElement() => StatelessElement(this); .... } 複製代碼

追根溯源StatelessElement父類到ComponentElement,發現以下代碼:bash

/// Calls the [StatelessWidget.build] method of the [StatelessWidget] object
/// (for stateless widgets) or the [State.build] method of the [State] object
/// (for stateful widgets) and then updates the widget tree.
@override
void performRebuild() {
 if (!kReleaseMode && debugProfileBuildsEnabled)
    ...
    try {
      built = build();
      debugWidgetBuilderValue(widget, built);
    } catch (e, stack) {
      built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      _dirty = false;
      assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
    }
    try {
      _child = updateChild(_child, built, slot);
      assert(_child != null);
    } catch (e, stack) {
      built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
      _child = updateChild(null, built, slot);
    }
}
複製代碼

根據註釋不難發現,這個方法用來調用StatelessWidget或者Statebuild方法,並更新Widget樹。不難發現,代碼使用了try/catch包裹了built = build();也就是包裹了build執行的方法。當捕獲到異常時使用ErrorWidget彈出錯誤提示。不難發現ErrorWidget.builder接受了一個_debugReportException方法返回的FlutterErrorDetails,並展現異常——這就是咱們上文提到的紅屏吧。進入_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上報。進入該方法:併發

/// 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);
  }
複製代碼

最後調用了onError方法,追蹤源碼:app

static FlutterExceptionHandler onError = dumpErrorToConsole;
複製代碼

onErrorFlutterError的一個靜態屬性,它默認的處理方法是dumpErrorToConsole。若是咱們更改異常的處理方式,提供一個自定的錯誤回調就能夠了。less

//在Flutter app入口配置自定義的異常上報回調(customerReport)
void main(){
  FlutterError.onError = (FlutterErrorDetails details) {
    customerReport(details);
  };
  runApp(MyApp());
}
複製代碼

在Flutter app入口配置自定義的異常上報回調customerReport,至於異常的上報,是在Flutter中直接請求網絡仍是與原生交互,咱們暫不進行討論。至此,咱們就能夠收集和處理那些Flutter爲咱們捕獲的異常了。異步

Flutter沒有捕獲的異常(併發異常)

雖然Flutter爲咱們在不少關鍵的方法進行了異常捕獲,但遺憾的是,它並不能爲咱們捕獲併發異常。同步異常能夠經過try/catch捕獲,而異步異常則不會被捕獲:ide

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

上面的代碼是捕獲不了Future異常的。怎麼辦? 幸運的是,Flutter中與一個Zone的概念,Dart中可經過Zone表示指定代碼執行的環境,不一樣的Zone代碼上下文是不一樣的互不影響。相似一個沙盒概念,不一樣沙箱的之間是隔離的,沙箱能夠捕獲、攔截或修改一些代碼行爲,咱們能夠再這裏面捕獲全部沒有被處理過的異常。 看一下runZoned的源碼:post

R runZoned<R>(R body(),{
    Map zoneValues,
    ZoneSpecification zoneSpecification, 
    Function onError
    }
)
複製代碼
  • zoneValues: Zone 的私有數據,能夠經過實例zone[key]獲取,能夠理解爲每一個「沙箱」的私有數據。
  • zoneSpecification:Zone的一些配置,能夠自定義一些代碼行爲,好比攔截日誌輸出行爲等
  • onError:Zone中未捕獲異常處理回調,若是開發者提供了onError回調或者通

咱們能夠經過onError捕獲異常就像下面這樣:

runZoned(
      () {
        Future.error("error");
      },
      onError: (dynamic e, StackTrace stack) {
        reportError(e, stack);
      },
    );
複製代碼

咱們可讓runApp運行在Zone中,這樣就能夠捕獲咱們Flutter應用中所有錯誤了!

runZoned(() {
    runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
    reportError(e, stack);
});
複製代碼

zoneSpecification則給了咱們帶來了更大的可能性,它提供了forkedZone的規範,使用在此類中給出的實力做爲回調覆蓋Zone中的默認行爲。處理程序上具備相同命名方法的方法。例如:攔截應用中全部調用print輸出日誌的行爲:

runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            report(line)
      },
    ),
  );
複製代碼

這樣使用onErrorzoneSpecification搭配能夠實現記錄日誌和手機崩潰信息:

void main() {
  runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            report(line)
      },
    ),
    onError: (Object obj, StackTrace stack) {
      customerReport(e, stack);
    }
  );
}
複製代碼

Flutter engine 異常捕獲

flutter engine部分的異常,以Android爲例,主要爲libfutter.so發生的錯誤。這部分發生的異常的捕獲方式和原生髮生的異常的捕獲方式同樣,能夠借用Bugly等實現。

總結

經過上述介紹,Flutter的異常捕獲整理以下:

異常收集

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    customerReport(details);
  };
  runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            report(line)
      },
    ),
    onError: (Object obj, StackTrace stack) {
      customerReport(e, stack);
    }
  );
}
複製代碼

異常上報

其中異常的上報可使用MethodChannel傳遞給Native,實現方式略,有興趣的課參考:Flutter與android之間的通信 而在我負責的項目中,我使用Bugly來收集異常信息,咱們能夠將native接收到的Flutter異常交給Bugly上報。 而收集到的崩潰信息則須要配置符號表(symbols)還原堆棧以肯定崩潰信息。詳情請參考:構建系統加入Flutter符號表

相關文章
相關標籤/搜索