Flutter | 异常处理

简介: Flutter | 异常处理

在了解 Flutter 异常捕获之前需要先了解一下 Dart 的异常处理以及 Dart 的单线程模型,只有知道了代码的执行流程,我们才能只要该在什么地方去捕获异常


Dart 中的异常


Dart 可以抛出和捕获异常,如果没有被捕获,则会抛出,最终导致程序终止运行


和 Java 不同,Dart 中的所有异常时非检查异常,方法不会声明它们抛出的异常,也不要求捕获任何异常


Dart 提供了 Exception 和 Error 类型,以及一些子类型。也可以自定义异常类型。此外,Dart 程序可以抛出任何 非null 对象,不仅限 Exception 和 Error 对象。


throw


throw FormatException('Expected at least 1 section');

抛出任意的对象


throw 'Out of llamas!';


在使用表达式的地方抛出异常


void distanceTo(Point other) => throw UnimplementedError();


Catch


捕获异常


try {
  breedMoreLlamas();
} on OutOfLlamasException {
  // 一个特殊的异常
  buyMoreLlamas();
} on Exception catch (e) {
  // 其他任何异常
  print('Unknown exception: $e');
} catch (e) {
  // 没有指定的类型,处理所有异常
  print('Something really unknown: $e');
}


catch 函数可以指定 1到2个参数,第一个为异常对象,第二个为堆栈信息(StackTrace对象)


try {
  // ···
} on Exception catch (e) {
  print('Exception details:\n $e');
} catch (e, s) {
  print('Exception details:\n $e');
  print('Stack trace:\n $s');
}


如果部分异常需要处理,可使用 rethrow 将异常重新抛出


void misbehave() {
  try {
    dynamic foo = true;
    print(foo++); // Runtime error
  } catch (e) {
    print('misbehave() partially handled ${e.runtimeType}.');
    rethrow; // Allow callers to see the exception.
  }
}
void main() {
  try {
    misbehave();
  } catch (e) {
    print('main() finished handling ${e.runtimeType}.');
  }
}


finally


无论是否 try 住异常,finally 都会执行。如果 try 住异常,会先执行对应的 catch,最后执行 finally


Dart 单线程模型


如果程序中发送异常且没有被捕获,那么程序将会被终止,但是这在 Dart 中则不会,根本原因是因为和他的运行机制有关系。例如 java 是多线程模型的编程语言,任意一个线程触发异常且异常没有被捕获时,就会导致整个进程退出,但是 Dart 不会,因为 Dart 是单线程模型,运行机制很相似,但是还是有一些区别,下面根据一张图来大致看一下(翻译自官方提供的图):


Dart 在单线程机制中是以消息循环机制来运行的,其中包含两个任务队列,一个是 微任务队列 microtask queue,一个是事件队列 event queue 。从图中可知道,微任务队列高于事件队列


现在来介绍一下 Dart 线程的运行过程,如上图,入口 main 执行完成之后,消息循环机制就会启动,首先会按照先进先出的顺序逐个执行微任务队列中的任务,事件执行完成之后程序便会退出,但是在事件任务执行的过程中也可以插入新的微任务和事件任务,这种情况下整个县城的执行过程便是一直在循环,不会退出,而在 Flutter 中,主线程的执行过程正是如此,永不终止


在 Dart 中,所有的外部事件任务都在事件队列中,如 IO,计时器,点击,以及绘制事件等。而微任务通常来源于 Dart 内部,并且微任务非常少,之所以如此,是因为微任务队列的优先级更高,如果微任务·太多,执行时间就会越久,时间队列的延迟也就越久,对于 UI 来说最直观的感受就是 卡,所以必须要保证微任务队里不会太长。我们可以通过 Future.microtask()方法向微任务队列添加一个任务


Flutter 异常捕获


Flutter 框架异常捕获


Flutter 框架为我们在很多地方都进行了异常补货,例如,当布局发生越界或者不规范时,Flutter 会自动弹出一个错误页面,


这是因为 Flutter 已经在 build 方法时添加了异常捕获,源码如下:


@override
void performRebuild() {
........
  try {
    built = build();
  } catch (e, stack) {
    _debugDoingBuild = false;
    built = ErrorWidget.builder(
      _debugReportException(
        ErrorDescription('building $this'),
        e,
        stack,
        informationCollector: () sync* {
          yield DiagnosticsDebugCreator(DebugCreator(this));
        },
      ),
    );
  } 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));
  }
  ......
}


可以看到,在发生异常时,Flutter 的处理方式是弹一个 ErrorWidget,那如果我们自己想要捕获异常并上报到报警平台的话应该怎么做?


我们进入 _debugReportException() 方法看看:


FlutterErrorDetails _debugReportException(
  DiagnosticsNode context,
  Object 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 是一个静态属性,他有一个默认的处理方法 dumpErrorToConsole ,这个方法定义在 assertions.dart 中,继续跟踪 onError 就可以找到这个方法


通过上面的代码我们可以知道,如果要将自己捕获的异常上报的话可以直接调用 reportError()方法,这个方法本身是一个静态方法,如下所示:


class ExceptionTest extends StatelessWidget {
  final error =
      (FlutterErrorDetails detail) => FlutterError.reportError(detail);
  @override
  Widget build(BuildContext context) {
    try {
      throw NullThrownError();
    } catch (e, s) {
      var details = FlutterErrorDetails(
        exception: e,
        stack: s,
        library: "lib.exception",
      );
      error(details);
    }
    // TODO: implement build
    throw UnimplementedError();
  }
}


最终,自己抛出的异常会上报到系统,然后就会弹到 ErrorWidget 中,并且会在控制台打印堆栈信息:


runZoned()


Dart 中有一个 runZoned() 方法,可以给执行的对象指定一个 Zone,Zone 表示一个代码执行的环境范围,为了方便理解,读者可以将 Zone 类比作为一个代码执行沙箱,不同沙箱直接是隔离的。沙箱可以捕获,拦截或修改一些代码行为,如 Zone 中可以捕获日志的输出,Timer 创建,微任务调用的行为,同时 Zone 也可以捕获所有未处理的异常,下面看一下 runZoned() 方法的定义:


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


zoneValues:Zone 的私有数据,可以通过实例 zone[key] 获取,可以理解为每个沙箱的私有数据


zoneSpecification:Zone 的一些配置,可以自定义一些行为,比如拦截日志输出行为等,


runZoned(() {
  print('hello world');
}, zoneSpecification: new ZoneSpecification(
  print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    parent.print(zone, "Intercepted $line");
  },
));


上面会输入如下:I/flutter ( 4182): Intercepted hello world ;如果不调用 parent.print ,则不会打印


这样一来,我们 app 中所有调用 print 方法输入日志的行为都会被拦截,通过这种方式,我们也可以在应用中记录日志,等到应用触发未捕获的异常时,将以此和日志进行上报


onError


Zone 中未捕获以此处理回调。如果开发者提供了 onError 回调或者通过 ZoneSpecification.handleUncaughtError 指定了错误回调,那么这个 zone 将会变成一个 error-zone ,该 error-zone 中发生未捕获的异常(无论是同步还是异步)时都会调用开发者提供的回调,如:


runZoned(() {
  print('hello world');
  throw NullThrownError();
}, zoneSpecification: new ZoneSpecification(
  print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    parent.print(zone, "Intercepted $line");
  },
), onError: (Object obj, StackTrace stack) {
  var details = FlutterErrorDetails(
    exception: obj,
    stack: stack,
    library: "lib.exception",
  );
  error(details);
});


这样一来,结合上面的 FlutterError.onError 我们就可以捕获我们 Flutter 应用中的全部错误了。


需要注意的是 error-zone 内部发生的错误是不会跨越 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."); });


最终,我们异常捕获的代码大致如下


void main() {
  runZoned(() => runApp(MyApp()), zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    collectLog(line); //收集日志
  }), onError: (Object obj, StackTrace stack) {
    makeDetails(obj, stack); //构建错误信息,并上报平台
  });
}
相关实践学习
通过日志服务实现云资源OSS的安全审计
本实验介绍如何通过日志服务实现云资源OSS的安全审计。
相关文章
|
文件存储
PowerShell系列(十):PowerShell CmdletPowerShell Cmdlet 参数详解
【2月更文挑战第5篇】强制类型参数使用比较频繁,基本上涉及新建、更新、配置等命令都需要针对特定的对应进行操作,所有需要强制输入一个参数来确认操作的对象是谁。
PowerShell系列(十):PowerShell CmdletPowerShell Cmdlet 参数详解
|
5月前
|
传感器 资源调度 算法
DDMA-MIMO雷达多子带相干累积目标检测算法——论文阅读
本文提出一种多子带相干累积(MSCA)算法,通过引入空带和子带相干处理,解决DDMA-MIMO雷达的多普勒模糊与能量分散问题。该方法在低信噪比下显著提升检测性能,实测验证可有效恢复目标速度,适用于车载雷达高精度感知。
698 4
DDMA-MIMO雷达多子带相干累积目标检测算法——论文阅读
|
11月前
|
机器学习/深度学习 自然语言处理 算法
突破!自然语言强化学习(NLRL):一个可处理语言反馈的强化学习框架
自然语言强化学习(NLRL)是一种将传统强化学习扩展到自然语言表示空间的新型框架,通过结合大型语言模型(LLMs),实现对语言反馈的直接处理。相比传统方法,NLRL在语言任务中具有更强的适用性和解释性,已在迷宫、突破和井字棋等游戏中展现良好性能。其优势包括语言反馈处理能力、增强的可解释性以及与LLMs的高效结合,但也面临语言歧义性、计算资源需求高及泛化能力有限等挑战。论文链接:https://arxiv.org/abs/2411.14251
332 24
|
机器学习/深度学习 编解码 算法
《探秘目标检测算法:YOLO与Faster R-CNN的原理及发展之旅》
目标检测是计算机视觉的重要任务,旨在识别图像或视频中的目标及其类别。早期依赖滑动窗口和人工特征(如HOG、SIFT),结合SVM等分类器,但计算量大、精度有限。随着深度学习兴起,R-CNN系列(R-CNN、Fast R-CNN、Faster R-CNN)逐步引入CNN和区域提议网络(RPN),显著提升速度和精度。YOLO系列(v1-v8)将检测视为回归问题,直接预测边界框和类别,以速度快著称。近年,基于Transformer的DETR等模型崭露头角,利用自注意力机制捕捉全局信息。未来,目标检测将在精度、速度和泛化能力上取得更大突破。
674 16
|
机器学习/深度学习 数据可视化 大数据
机器学习与大数据分析的结合:智能决策的新引擎
机器学习与大数据分析的结合:智能决策的新引擎
710 15
|
JSON API 开发者
闲鱼商品详情API接口(闲鱼API系列)
闲鱼商品详情API为开发者提供便捷、高效且合规的途径,获取闲鱼平台上特定商品的详细信息,如标题、价格、描述和图片等。该接口采用GET请求方式,需传入app_key、item_id、timestamp和sign等参数,返回JSON格式数据。示例代码展示了如何使用Python调用此API,包括生成签名和处理响应。开发者需替换实际的app_key、app_secret和商品ID,并关注官方文档以确保接口使用的准确性。
3513 1
|
机器学习/深度学习 人工智能 文字识别
ultralytics YOLO11 全新发布!(原理介绍+代码详见+结构框图)
本文详细介绍YOLO11,包括其全新特性、代码实现及结构框图,并提供如何使用NEU-DET数据集进行训练的指南。YOLO11在前代基础上引入了新功能和改进,如C3k2、C2PSA模块和更轻量级的分类检测头,显著提升了模型的性能和灵活性。文中还对比了YOLO11与YOLOv8的区别,并展示了训练过程和结果的可视化
23058 0
|
Docker 容器 文件存储
蓝易云 - 使用WSL修改docker文件存储位置
注意:你需要确保新的文件存储路径存在,且Docker有权限访问和写入。
530 3
|
机器学习/深度学习 Python
【Python 机器学习专栏】模型选择中的交叉验证与网格搜索
【4月更文挑战第30天】交叉验证和网格搜索是机器学习中优化模型的关键技术。交叉验证通过划分数据集进行多次评估,如K折和留一法,确保模型性能的稳定性。网格搜索遍历预定义参数组合,寻找最佳参数设置。两者结合能全面评估模型并避免过拟合。Python中可使用`sklearn`库实现这一过程,但需注意计算成本、过拟合风险及数据适应性。理解并熟练应用这些方法能提升模型性能和泛化能力。
729 0