前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter混编工程之异常处理

Flutter混编工程之异常处理

作者头像
用户1907613
发布2023-03-01 16:11:07
8260
发布2023-03-01 16:11:07
举报
文章被收录于专栏:Android群英传Android群英传

Flutter App层和Framework层的异常,通常是不会引起Crash的,但是Engine层的异常会造成Crash。而Flutter Engine部分的异常,主要是libfutter.so发生的异常,这部分的异常,在Dart层无法捕获,一般会交给类似Bugly这样的平台来收集。

我们能主动监控的,主要是Dart层的异常,这些异常虽然不会让App crash,但是统计这些异常对于提高我们的用户体验,是非常有必要的。

同步异常与异步异常

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

代码语言:javascript
复制
try {
  Future.error("error");
} catch (e){
  print(e)
}

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

代码语言:javascript
复制
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。

再进入_debugReportException中一探究竟,你会发现,应用层的异常被catch之后,都是通过FlutterError.reportError来处理的。

在reportError中,会调用onError来处理,默认的处理方式是dumpErrorToConsole,它就是onError的默认实现。

❝在这里我们还能发现如何判断debug模式,看源码是不是很有意思。 ❞

通过上面的源码,我们就可以了解到,当Flutter应用层崩溃后,SDK的处理,简而言之,就是会构建一个错误界面,同时回调onError函数。在这里,我们可以通过修改这个静态的回调函数,来创建自己的处理方式。

所以,很简单,我们只需要在main()中,执行下面的代码即可。

代码语言:javascript
复制
var defaultError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
  defaultError?.call(details);// 根据需要是否要保留default处理
  reportException(details);
};

defaultError?.call(details)就是默认将异常日志打印到console的方法,如果不用,这里可以去掉。

重写错误界面

前面我们看到了,在源代码中,Flutter自定义了一个ErrorWidget作为默认的异常界面,在平时的开发中,我们可以自定义ErrorWidget.builder,实现一个更友好的错误界面,例如封装一个统一的异常提示界面。

代码语言:javascript
复制
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的一个简单示例。

代码语言:javascript
复制
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来进行简化,代码如下所示。

代码语言:javascript
复制
void main() {
  runZonedGuarded(
    () => runApp(const MyApp(color: Colors.blue)),
    (Object error, StackTrace stack) {
      reportException(
        FlutterErrorDetails(
          exception: error,
          stack: stack,
        ),
      );
    },
  );
}

封装

下面我们将前面的异常处理方式都合并到一起,并针对EngineGroup的多入口处理,封装一个类,代码如下所示。

代码语言:javascript
复制
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进行下调用即可。

代码语言:javascript
复制
void main() => SafeApp().run(const MyApp(color: Colors.blue));

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

上报

在Flutter侧,我们只是获取了异常的相关信息,如果需要上报,那么我们需要借助Channel,桥接的Native,使用Bugly或其它平台进行上报,我们可以借助Pigeon来进行处理,还不熟悉的朋友可以参考我前面的文章。 Flutter混编工程之高速公路Pigeon Flutter混编工程之通讯之路 通过Channel,我们可以把异常数据报给Native侧,再让Native侧走自己的上报通道,例如Bugly等。

代码语言:javascript
复制
NativeCommonApi().reportException('------Flutter_Exception------\n${details.exceptionAsString()}\n${details.stack.toString()}');

同时,Flutter提供了exceptionAsString()方法,将异常信息展示的更加友好一点,我们可以借助它来做一些格式化的操作。

3.3版本API的改进

官方的API更新如下: https://docs.flutter.dev/testing/errors PlatformDispatcher.onError在以前的版本中,开发者必须手动配置自定义Zone才能捕获应用程序的所有异常和错误,但是自定义Zone对Dart核心库中的一些优化是有害的,这会减慢应用程序的启动时间。「在此版本中,开发者可以通过设置回调来捕获所有错误和异常,而不是使用自定义。」

所以,3.3之后,我们不用再设置Zone来捕获全局异常了,只用设置PlatformDispatcher.instance.onError即可。

代码语言:javascript
复制
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');
      },
    );
  }
}

本文原创公众号:群英传,授权转载请联系微信,授权后,请在原创发表24小时后转载。

作者:徐宜生

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-02-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 群英传 微信公众号,前往查看

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

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 同步异常与异步异常
  • Widget Build异常
  • 重写错误界面
  • 全局未捕获异常
  • 封装
  • 上报
  • 3.3版本API的改进
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档