前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 异常捕获详解

Flutter 异常捕获详解

原创
作者头像
大发明家
发布2021-12-15 15:28:09
8.1K0
发布2021-12-15 15:28:09
举报
文章被收录于专栏:技术博客文章
Flutter 异常

Flutter 异常指的是,Flutter 程序中 Dart 代码运行时意外发生的错误事件。我们可以通过与 Swift 类似的 try-catch

机制来捕获它。但 与 Swift 不同的是,Dart 程序不强制要求我们必须处理异常。

这是因为,Dart 采用事件循环的机制来运行任务,所以各个任务的运行状态是互相独立的。也就是说,即便某个任务出现了异常我们没有捕获它,Dart

程序也不会退出,只会导致当前任务后续的代码不会被执行,用户仍可以继续使用其他功能。

Dart 异常,根据来源又可以细分为 App 异常和 Framework 异常。Flutter 为这两种异常提供了不同的捕获方式。

App 异常的捕获方式

App 异常,就是应用代码的异常,通常由未处理应用层其他模块所抛出的异常引起。根据异常代码的执行时序,App

异常可以分为两类,即同步异常和异步异常:同步异常可以通过 try-catch 机制捕获,异步异常则需要采用 Future 提供的 catchError

语句捕获。

这两种异常的捕获方式,如下代码所示:

代码语言:txt
复制
// 使用 try-catch 捕获同步异常
代码语言:txt
复制
try {
代码语言:txt
复制
    throw SYReportException('发生一个dart 同步异常');
代码语言:txt
复制
}
代码语言:txt
复制
catch(e) {
代码语言:txt
复制
  print(e);
代码语言:txt
复制
}
代码语言:txt
复制
// 使用 catchError 捕获异步异常
代码语言:txt
复制
Future.delayed(Duration(seconds: 1)).then((e) {
代码语言:txt
复制
  if (sendFlag) {
代码语言:txt
复制
    print('异步异常发生之前 >>>>>>>>>>>');
代码语言:txt
复制
    throw SYReportException('发生一个dart 异步异常');
代码语言:txt
复制
  }
代码语言:txt
复制
  print('异步异常后执行的代码 <<<<<<<<<<<');
代码语言:txt
复制
});
代码语言:txt
复制
// 注意,以下代码无法捕获异步异常
代码语言:txt
复制
try {
代码语言:txt
复制
    Future.delayed(Duration(seconds: 1)).then((e) {
代码语言:txt
复制
      if (sendFlag) {
代码语言:txt
复制
        print('异步异常发生之前 >>>>>>>>>>>');
代码语言:txt
复制
        throw SYReportException('发生一个dart 异步异常');
代码语言:txt
复制
      }
代码语言:txt
复制
      print('异步异常后执行的代码 <<<<<<<<<<<');
代码语言:txt
复制
    });
代码语言:txt
复制
} catch (e) {
代码语言:txt
复制
    print("这是不会执行的. ");
代码语言:txt
复制
}

需要注意的是,这两种方式是不能混用的。可以看到,在上面的代码中,我们是无法使用 try-catch 去捕获一个异步调用所抛出的异常的。

同步的 try-catch 和异步的 catchError,为我们提供了直接捕获特定异常的能力,而如果我们想集中管理代码中的所有异常,Flutter

也提供了 Zone.runZoned 方法。

我们可以给代码执行对象指定一个 Zone,在 Dart 中,Zone

表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果我们想要观察沙盒中代码执行出现的异常,沙盒提供了 onError

回调函数,拦截那些在代码执行对象中的未捕获异常。

在下面的代码中,我们将可能抛出异常的语句放置在了 Zone 里。可以看到,在没有使用 try-catch 和 catchError

的情况下,无论是同步异常还是异步异常,都可以通过 Zone 直接捕获到:

代码语言:txt
复制
runZoned(() {
代码语言:txt
复制
  // 同步抛出异常
代码语言:txt
复制
  throw SYReportException('发生一个dart 同步异常');
代码语言:txt
复制
}, onError: (dynamic e, StackTrace stack) {
代码语言:txt
复制
  print('zone捕获到了同步异常');
代码语言:txt
复制
});
代码语言:txt
复制
runZoned(() {
代码语言:txt
复制
  // 异步抛出异常
代码语言:txt
复制
  Future.delayed(Duration(seconds: 1))
代码语言:txt
复制
      .then((e) => throw SYReportException('发生一个dart 异步异常'));
代码语言:txt
复制
}, onError: (dynamic e, StackTrace stack) {
代码语言:txt
复制
  print('zone捕获到了异步异常');
代码语言:txt
复制
});

因此,如果我们想要集中捕获 Flutter 应用中的未处理异常,可以把 main 函数中的 runApp 语句也放置在 Zone

中。这样在检测到代码中运行异常时,我们就能根据获取到的异常上下文信息,进行统一处理了:

代码语言:txt
复制
runZonedGuarded(() {
代码语言:txt
复制
    runApp(MyApp());
代码语言:txt
复制
}, (error, stackTrace) {
代码语言:txt
复制
    // 这个闭包中发生的Exception是捕获不到的 @山竹
代码语言:txt
复制
    SYExceptionReportChannel.reportException(error, stackTrace);
代码语言:txt
复制
}, zoneSpecification: ZoneSpecification(
代码语言:txt
复制
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
代码语言:txt
复制
  // 记录所有的打印日志
代码语言:txt
复制
  parent.print(zone, "line是啥:$line");
代码语言:txt
复制
},
代码语言:txt
复制
));

接下来,我们再看看 Framework 异常应该如何捕获吧。

Framework 异常的捕获方式

Framework 异常,就是 Flutter 框架引发的异常,通常是由应用代码触发了 Flutter

框架底层的异常判断引起的。比如,当布局不合规范时,Flutter 就会自动弹出一个触目惊心的红色错误界面,如下所示:

framework_error.png

这其实是因为,Flutter 框架在调用 build 方法构建页面时进行了 try-catch 的处理,并提供了一个

ErrorWidget,用于在出现异常时进行信息提示:

代码语言:txt
复制
@override
代码语言:txt
复制
void performRebuild() {
代码语言:txt
复制
  Widget built;
代码语言:txt
复制
  try {
代码语言:txt
复制
    // 创建页面
代码语言:txt
复制
    built = build();
代码语言:txt
复制
  } catch (e, stack) {
代码语言:txt
复制
    // 使用 ErrorWidget 创建页面
代码语言:txt
复制
    built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
代码语言:txt
复制
    ...
代码语言:txt
复制
  } 
代码语言:txt
复制
  ...
代码语言:txt
复制
}

这个页面反馈的信息比较丰富,适合开发期定位问题。但如果让用户看到这样一个页面,就很糟糕了。因此,我们通常会重写 ErrorWidget.builder

方法,将这样的错误提示页面替换成一个更加友好的页面。

下面的代码演示了自定义错误页面的具体方法。在这个例子中,我们自定义了错误页面,显示导航栏和可滚动的错误信息:

代码语言:txt
复制
// 重写 ErrorWidget 的builder,显示地优雅一些
代码语言:txt
复制
ErrorWidget.builder = (FlutterErrorDetails details) {
代码语言:txt
复制
  print('错误widget详细的错误信息为:' + details.toString());
代码语言:txt
复制
  return MaterialApp(
代码语言:txt
复制
    title: 'Error Widget',
代码语言:txt
复制
    theme: ThemeData(
代码语言:txt
复制
      primarySwatch: Colors.red,
代码语言:txt
复制
    ),
代码语言:txt
复制
    home: Scaffold(
代码语言:txt
复制
      appBar: AppBar(
代码语言:txt
复制
        title: Text('Widget渲染异常!!!'),
代码语言:txt
复制
      ),
代码语言:txt
复制
      body: _createBody(details),
代码语言:txt
复制
    ),
代码语言:txt
复制
  );
代码语言:txt
复制
};

运行效果如下所示:

custom_error_widget.png

比起之前触目惊心的红色错误页面,自定义的看起来优雅一些,当然也可以找UI帮忙设计更友好的界面。需要注意的是,ErrorWidget.builder

方法提供了一个参数 details

用于表示当前的错误上下文,为避免用户直接看到错误信息,这里我们并没有将它展示到界面上。但是,我们不能丢弃掉这样的异常信息,需要提供统一的异常处理机制,用于后续分析异常原因。

为了集中处理框架异常,Flutter 提供了 FlutterError 类,这个类的 onError

属性会在接收到框架异常时执行相应的回调。因此,要实现自定义捕获逻辑,我们只要为它提供一个自定义的错误处理回调即可。

在下面的代码中,我们使用 Zone 提供的 handleUncaughtError 语句,将 Flutter 框架的异常统一转发到当前的 Zone

中,这样我们就可以统一使用 Zone 去处理应用内的所有异常了:

代码语言:txt
复制
// framework异常捕获,转发到当前的 Zone
代码语言:txt
复制
FlutterError.onError = (FlutterErrorDetails details) async {
代码语言:txt
复制
    Zone.current.handleUncaughtError(details.exception, details.stack);
代码语言:txt
复制
};
异常上报

到目前为止,我们已经捕获到了应用中所有的未处理异常。但如果只是把这些异常在控制台中打印出来还是没办法解决问题,我们还需要把它们上报到开发者能看到的地方,用于后续分析定位并解决问题。

三方,我们一般都是用bugly。如果公司有自研的bug系统,那就更好了。

这些异常上报,我们将使用MethodChannel推送给Native,由Native上报到bugly或自研的异常系统。

这里只展示Dart的代码实现,至于Native怎么实现Channel,自行Google即可

Dart实现

代码如下:

代码语言:txt
复制
/// flutter exception channel
代码语言:txt
复制
class SYExceptionReportChannel {
代码语言:txt
复制
  static const MethodChannel _channel =
代码语言:txt
复制
      const MethodChannel('sy_exception_channel');
代码语言:txt
复制
  // 上报异常
代码语言:txt
复制
  static reportException(dynamic error, dynamic stack) {
代码语言:txt
复制
    print('捕获的异常类型 >>> : ${error.runtimeType}');
代码语言:txt
复制
    print('捕获的异常信息 >>> : $error');
代码语言:txt
复制
    print('捕获的异常堆栈 >>> : $stack');
代码语言:txt
复制
    Map reportMap = {
代码语言:txt
复制
      'type': "${error.runtimeType}",
代码语言:txt
复制
      'title': error.toString(),
代码语言:txt
复制
      'description': stack.toString()
代码语言:txt
复制
    };
代码语言:txt
复制
    // 得使用这个
代码语言:txt
复制
    print('这是通过convert转的json');
代码语言:txt
复制
    print(jsonEncode(reportMap));
代码语言:txt
复制
    _channel.invokeListMethod('reportException', reportMap);
代码语言:txt
复制
  }
代码语言:txt
复制
}

我们捕获到的异常后,由channel推送给Native,包含三个信息:

  • 异常的类型信息
  • 异常的简要说明信息(即error的toString的值)
  • 异常的堆栈信息
优化、封装及问题点

综合上述的阐述,我们将代码做一些封装和优化。

  • 优化: 异常捕获后,在debug和release的模式下是不一样的处理,debug模式,直接打印到控制台是最直观的,release模式下,无法感知哪里出了问题,所以我们需要上报,然后分析问题。

区分当前是debug还是release,有一个比较巧妙的方式,代码及注释如下:

代码语言:txt
复制
// 比较巧妙的一种方式判定是否是debug模式
代码语言:txt
复制
static bool get isInDebugMode {
代码语言:txt
复制
    bool inDebugMode = false;
代码语言:txt
复制
    // 如果debug模式下会触发赋值,只有在debug模式下才会执行assert
代码语言:txt
复制
    assert(inDebugMode = true);
代码语言:txt
复制
    return inDebugMode;
代码语言:txt
复制
}

基于上述的思路,我们将未捕获的异常转发到zone做一个判断:

代码语言:txt
复制
// framework异常捕获,转发到当前的 Zone
代码语言:txt
复制
    FlutterError.onError = (FlutterErrorDetails details) async {
代码语言:txt
复制
      // debug模式
代码语言:txt
复制
      if (ExceptionReportUtil.isInDebugMode) {
代码语言:txt
复制
        // 打印到控制台
代码语言:txt
复制
        FlutterError.dumpErrorToConsole(details);
代码语言:txt
复制
        // release模式
代码语言:txt
复制
      } else {
代码语言:txt
复制
        // 转发到zone
代码语言:txt
复制
        Zone.current.handleUncaughtError(details.exception, details.stack);
代码语言:txt
复制
      }
代码语言:txt
复制
    };
  • 封装: main函数中的代码,自然是越简练越好,但将未捕获的异常转发到zone及错误Widget重写必须放在main中,所以抽取一个工具类ExceptionReportUtil:
代码语言:txt
复制
/// 工具类
代码语言:txt
复制
class ExceptionReportUtil {
代码语言:txt
复制
  // 比较巧妙的一种方式判定是否是debug模式
代码语言:txt
复制
  static bool get isInDebugMode {
代码语言:txt
复制
    bool inDebugMode = false;
代码语言:txt
复制
    // 如果debug模式下会触发赋值,只有在debug模式下才会执行assert
代码语言:txt
复制
    assert(inDebugMode = true);
代码语言:txt
复制
    return inDebugMode;
代码语言:txt
复制
  }
代码语言:txt
复制
  // 初始化异常捕获配置
代码语言:txt
复制
  static void initExceptionCatchConfig() {
代码语言:txt
复制
    // framework异常捕获,转发到当前的 Zone
代码语言:txt
复制
    FlutterError.onError = (FlutterErrorDetails details) async {
代码语言:txt
复制
      // debug模式
代码语言:txt
复制
      if (ExceptionReportUtil.isInDebugMode) {
代码语言:txt
复制
        // 打印到控制台
代码语言:txt
复制
        FlutterError.dumpErrorToConsole(details);
代码语言:txt
复制
        // release模式
代码语言:txt
复制
      } else {
代码语言:txt
复制
        // 转发到zone
代码语言:txt
复制
        Zone.current.handleUncaughtError(details.exception, details.stack);
代码语言:txt
复制
      }
代码语言:txt
复制
    };
代码语言:txt
复制
    // 重写 ErrorWidget 的builder,显示地优雅一些
代码语言:txt
复制
    ErrorWidget.builder = (FlutterErrorDetails details) {
代码语言:txt
复制
      print('错误widget详细的错误信息为:' + details.toString());
代码语言:txt
复制
      return MaterialApp(
代码语言:txt
复制
        title: 'Error Widget',
代码语言:txt
复制
        theme: ThemeData(
代码语言:txt
复制
          primarySwatch: Colors.red,
代码语言:txt
复制
        ),
代码语言:txt
复制
        home: Scaffold(
代码语言:txt
复制
          appBar: AppBar(
代码语言:txt
复制
            title: Text('Widget渲染异常!!!'),
代码语言:txt
复制
          ),
代码语言:txt
复制
          body: _createBody(details),
代码语言:txt
复制
        ),
代码语言:txt
复制
      );
代码语言:txt
复制
    };
代码语言:txt
复制
  }
代码语言:txt
复制
  // 创建错误widget body
代码语言:txt
复制
  static Widget _createBody(dynamic details) {
代码语言:txt
复制
    // 正确代码
代码语言:txt
复制
    return Container(
代码语言:txt
复制
      color: Colors.white,
代码语言:txt
复制
      child: SingleChildScrollView(
代码语言:txt
复制
        child: Padding(
代码语言:txt
复制
          padding: const EdgeInsets.all(16.0),
代码语言:txt
复制
          child: Text(
代码语言:txt
复制
            details.toString(),
代码语言:txt
复制
            style: TextStyle(color: Colors.red),
代码语言:txt
复制
          ),
代码语言:txt
复制
        ),
代码语言:txt
复制
      ),
代码语言:txt
复制
    );
代码语言:txt
复制
  }
代码语言:txt
复制
}
  • 问题点: 在runZonedGuarded函数的闭包中接收未捕获的异常,然后上报,如果执行该闭包中的代码发生异常,是无法捕获的:

代码及注释如下:

代码语言:txt
复制
main(List<String> args) {
代码语言:txt
复制
  // 初始化Exception 捕获配置
代码语言:txt
复制
  ExceptionReportUtil.initExceptionCatchConfig();
代码语言:txt
复制
  runZonedGuarded(() {
代码语言:txt
复制
    runApp(MyApp());
代码语言:txt
复制
  }, (error, stackTrace) {
代码语言:txt
复制
    // 这个闭包中发生的Exception是捕获不到的 @山竹
代码语言:txt
复制
    SYExceptionReportChannel.reportException(error, stackTrace);
代码语言:txt
复制
  }, zoneSpecification: ZoneSpecification(
代码语言:txt
复制
    print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
代码语言:txt
复制
      // 记录所有的打印日志
代码语言:txt
复制
      parent.print(zone, "line是啥:$line");
代码语言:txt
复制
    },
代码语言:txt
复制
  ));
代码语言:txt
复制
}

我们通过SYExceptionReportChannel.reportException(error,

stackTrace)将错误上报给Native,但在Native如果没有实现channel的链接,那么必然会报MissingPluginException,这个异常是不在当前的zone中的,所以无法捕获。

missingPluginException.png

通过一个例子来验证我们的异常捕获

写了一个例子,来演示这个功能的实现,以及具体的效果:

demo_page.png

在点击第三个按钮之前,前面两个按钮都是正常工作,不会发生异常,点击之后就会产生异常。

通过打印信息,我们来看下每种异常具体捕获到了哪些信息:

  • Dart同步异常:

dart同步异常.png

  • Dart异步异常:

dart异步异常.png

  • flutter framework异常:

flutter_framework异常.png

通过异常类型、异常信息和异常的具体堆栈,对异常的定位将起到很大的帮助。

总结

对于 Flutter 应用的异常捕获,可以分为单个异常捕获和多异常统一拦截两种情况。

其中,单异常捕获,使用 Dart 提供的同步异常 try-catch,以及异步异常 catchError

机制即可实现。而对多个异常的统一拦截,可以细分为如下两种情况:一是 App 异常,我们可以将代码执行块放置到 Zone 中,通过 onError

回调进行统一处理;二是 Framework 异常,我们可以使用 FlutterError.onError 回调进行拦截。

在捕获到异常之后,我们需要上报异常信息,用于后续分析定位问题。

需要注意的是,Flutter 提供的异常拦截只能拦截 Dart 层的异常,而无法拦截 Engine 层的异常。这是因为,Engine 层的实现大部分是

C++ 的代码,一旦出现异常,整个程序就直接 Crash 掉了。不过通常来说,这类异常出现的概率极低,一般都是 Flutter 底层的

Bug,与我们在应用层的实现没太大关系,所以我们也无需过度担心。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • Flutter 异常
  • App 异常的捕获方式
  • Framework 异常的捕获方式
  • 异常上报
  • Dart实现
  • 优化、封装及问题点
  • 通过一个例子来验证我们的异常捕获
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档