Flutter App层和Framework层的反常,通常是不会引起Crash的,可是Engine层的反常会形成Crash。而Flutter Engine部分的反常,主要是libfutter.so产生的反常,这部分的反常,在Dart层无法捕获,一般会交给相似Bugly这样的渠道来收集。

咱们能自动监控的,主要是Dart层的反常,这些反常尽管不会让App crash,可是计算这些反常关于提高咱们的用户体会,对错常有必要的。

同步反常与异步反常

关于同步反常来说,直接运用try-catch就能够捕获反常,假如要指定捕获的反常类型,能够运用on关键字。可是,try-catch不能捕获异步反常,就像下面的代码,是无法捕获的。

try {
  Future.error("error");
} catch (e){
  print(e)
}

这和在Java中,try-catch捕获Thread中的反常相似,关于异步反常来说,只能运用Future的catchError或者是onError来捕获反常,代码如下所示。

Future.delayed(Duration(seconds: 1)).then((value) => print(value), onError: (e) {});

Dart的履行队列是一个单线程模型,所以在事情循环队列中,当某个Task产生反常并没有被捕获时,程序并不会退出,仅仅当时的Task反常间断,也便是说一个Task产生的反常是不会影响其它Task履行的。

Widget Build反常

Widget在Build过程中假如产生反常,例如在build函数中出错(throw exception),咱们会看见一个深红色的反常界面,这个便是Flutter自带的反常处理界面,咱们来看下源代码中,Flutter对这类反常的处理办法。在ComponentElement的完成中,咱们找到performRebuild函数,这个是函数是build时所调用的,咱们在这儿,能够找到相关的完成。

如下所示,在履行到build()函数假如出错时,就会被catch,然后创建一个ErrorWidget。

Flutter混编工程之异常处理

再进入_debugReportException中一探终究,你会发现,应用层的反常被catch之后,都是经过FlutterError.reportError来处理的。
Flutter混编工程之异常处理

在reportError中,会调用onError来处理,默许的处理办法是dumpErrorToConsole,它便是onError的默许完成。
Flutter混编工程之异常处理

在这儿咱们还能发现如何判断debug形式,看源码是不是很有意思。

经过上面的源码,咱们就能够了解到,当Flutter应用层崩溃后,SDK的处理,简而言之,便是会构建一个过错界面,一同回调onError函数。在这儿,咱们能够经过修正这个静态的回调函数,来创建自己的处理办法。

Flutter混编工程之异常处理

所以,很简略,咱们只需求在main()中,履行下面的代码即可。

var defaultError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
  defaultError?.call(details);// 依据需求是否要保存default处理
  reportException(details);
};

defaultError?.call(details)便是默许将反常日志打印到console的办法,假如不必,这儿能够去掉。

重写过错界面

前面咱们看到了,在源代码中,Flutter自定义了一个ErrorWidget作为默许的反常界面,在平常的开发中,咱们能够自定义ErrorWidget.builder,完成一个更友爱的过错界面,例如封装一个一致的反常提示界面。

ErrorWidget.builder = (FlutterErrorDetails details) {
  return MaterialApp(
    theme: ThemeData(primarySwatch: Colors.red),
    home: Scaffold(
      appBar: AppBar(
        title: const Text('出错了,请稍后再试'),
      ),
      body: SingleChildScrollView(
        child: Padding(
            padding: const EdgeInsets.all(8.0),
            child: Text(details.toString()), // 后续修正为一致的过错页
          )),
    ),
  );
};

如上所示,经过修正ErrorWidget.builder,就能够将恣意自定义的界面作为反常界面了。

大局未捕获反常

前面讲到的,都是归于被捕获的反常,而有一些反常,在代码中是没有被捕获的,这就相似Android的UncaughtExceptionHandler,Flutter也供给了一个大局的反常处理钩子函数,一切的未捕获反常,无论是同步反常还是异步反常,都会在这儿被监听。

在Dart中,SDK供给了一个Zone的概念,一个Zone就相似一个沙箱,在Zone里面,能够拥有独立的反常处理、print函数等等功能,多个Zone之间是互相独立的,所以,咱们只需求将App运行在一个Zone里面,就能够凭借它的handleUncaughtError来处理一切的未捕获反常了。下面是运用Zone的一个简略示例。

void main() {
  runZoned(
    () => runApp(const MyApp(color: Colors.blue)),
    zoneSpecification: ZoneSpecification(
      handleUncaughtError: (
        Zone self,
        ZoneDelegate parent,
        Zone zone,
        Object error,
        StackTrace stackTrace,
      ) {
        reportException(
          FlutterErrorDetails(
            exception: error,
            stack: stackTrace,
          ),
        );
      },
    ),
  );
}

依据文档中的提高,能够运用runZonedGuarded来进行简化,代码如下所示。

void main() {
  runZonedGuarded(
    () => runApp(const MyApp(color: Colors.blue)),
    (Object error, StackTrace stack) {
      reportException(
        FlutterErrorDetails(
          exception: error,
          stack: stack,
        ),
      );
    },
  );
}

封装

下面咱们将前面的反常处理办法都合并到一同,并针对EngineGroup的多入口处理,封装一个类,代码如下所示。

class SafeApp {
  run(Widget app) {
    ErrorWidget.builder = (FlutterErrorDetails details) {
      return MaterialApp(
        theme: ThemeData(primarySwatch: Colors.red),
        home: Scaffold(
          appBar: AppBar(
            title: const Text('出错了,请稍后再试'),
          ),
          body: SingleChildScrollView(
            child: Padding(
                padding: const EdgeInsets.all(8.0),
                child: Text(details.toString()), // 后续修正为一致的过错页
              )),
        ),
      );
    };
    FlutterError.onError = (FlutterErrorDetails details) {
      Zone.current.handleUncaughtError(details.exception, details.stack!);
    };
    runZonedGuarded(
      () => runApp(const MyApp(color: Colors.blue)),
      (Object error, StackTrace stack) {
        reportException(
          FlutterErrorDetails(
            exception: error,
            stack: stack,
          ),
        );
      },
    );
  }
}

在这儿,咱们构建了下面这些反常处理的办法:

  • 一致的反常处理界面
  • 将Build反常一致转发到Zone中的反常处理函数来进行处理
  • 将一切的未捕获反常记载

这样的话,咱们在运用时,只需求对原始的App进行下调用即可。

void main() => SafeApp().run(const MyApp(color: Colors.blue));

这样就完成了反常处理的封装。

上报

在Flutter侧,咱们仅仅获取了反常的相关信息,假如需求上报,那么咱们需求凭借Channel,桥接的Native,运用Bugly或其它渠道进行上报,咱们能够凭借Pigeon来进行处理,还不熟悉的朋友能够参阅我前面的文章。
Flutter混编工程之高速公路Pigeon
Flutter混编工程之通讯之路
经过Channel,咱们能够把反常数据报给Native侧,再让Native侧走自己的上报通道,例如Bugly等。

NativeCommonApi().reportException('------Flutter_Exception------\n${details.exceptionAsString()}\n${details.stack.toString()}');

一同,Flutter供给了exceptionAsString()办法,将反常信息展现的更加友爱一点,咱们能够凭借它来做一些格式化的操作。

3.3版本API的改善

官方的API更新如下:
docs.flutter.dev/testing/err…
PlatformDispatcher.onError在曾经的版本中,开发者有必要手动装备自定义Zone才干捕获应用程序的一切反常和过错,可是自定义Zone对Dart核心库中的一些优化是有害的,这会减慢应用程序的发动时间。「在此版本中,开发者能够经过设置回调来捕获一切过错和反常,而不是运用自定义。」

所以,3.3之后,咱们不必再设置Zone来捕获大局反常了,只用设置PlatformDispatcher.instance.onError即可。

import 'package:flutter/material.dart';
import 'dart:ui';
Future<void> main() async {
  await myErrorsHandler.initialize();
  FlutterError.onError = (details) {
    FlutterError.presentError(details);
    myErrorsHandler.onErrorDetails(details);
  };
  PlatformDispatcher.instance.onError = (error, stack) {
    myErrorsHandler.onError(error, stack);
    return true;
  };
  runApp(const MyApp());
}
class MyApp extends StatelessWidget {
  const MyApp({super.key});
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      builder: (context, widget) {
        Widget error = const Text('...rendering error...');
        if (widget is Scaffold || widget is Navigator) {
          error = Scaffold(body: Center(child: error));
        }
        ErrorWidget.builder = (errorDetails) => error;
        if (widget != null) return widget;
        throw ('widget is null');
      },
    );
  }
}