6

Flutter异常监控 - 壹 | 从Zone说起 - 码里特别有禅

 1 year ago
source link: https://www.cnblogs.com/xuge2it/p/17016909.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

如果你正需要处理Flutter异常捕获,那么恭喜你,找对地了,这里从根源上给你准备了Flutter异常捕获需要是所有知识和原理,让你更深刻认识Flutter Zone概念。

Zone是什么

/// A zone represents an environment that remains stable across asynchronous
/// calls.

SDK中描述:表示一个环境,这个环境为了保持稳定异步调用。

通俗理解39 | 线上出现问题,该如何做好异常捕获与信息采集?中描述:

我们可以给代码执行对象指定一个 Zone,在 Dart 中,Zone 表示一个代码执行的环境范围,其概念类似沙盒,不同沙盒之间是互相隔离的。如果我们想要观察沙盒中代码执行出现的异常,沙盒提供了 onError 回调函数,拦截那些在代码执行对象中的未捕获异常。

Zone创建

Dart提供了runZoned方法,支持Zone的快速创建

R runZoned<R>(R body(),
    {Map<Object?, Object?>? zoneValues,
    ZoneSpecification? zoneSpecification,
    @Deprecated("Use runZonedGuarded instead") Function? onError}) {
  • zoneValues: Zone 的私有数据,可以通过实例zone[key]获取,可以理解为每个“沙箱”的私有数据。
  • zoneSpecification:Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出和错误等

Zone的作用

import 'dart:async';

//OUTPUT:Uncaught error: Would normally kill the program
void main() {
  runZonedGuarded(() {
    Timer.run(() {
      throw 'Would normally kill the program';
    });
  }, (error, stackTrace) {
    print('Uncaught error: $error');
  });
}

用try catch一样可以捕获,为啥要通过Zone来捕获?

  1. Zone回调收拢了异步捕获入口,提高了可维护性。
  2. 未预料的未捕获异常可以帮你自动捕获到,提高便捷性。

是不是所有异常都可以捕获到?

不是, 只能处理情况1。

  1. Zone默认捕获范围主要针对异步异常或者一般逻辑异常等常规异常,比如Future中出了问题,或者逻辑处理了1/0,(见Tag3),捕获异步异常原理见简话-Flutter异常处理 - 掘金

  2. Dart中另外比较容易出现的异常是framework异常,比如build异常等,这种异常Zone无法捕获到,原因可以参看Flutter异常捕获和Crash崩溃日志收集 。如果想Zone来处理可这样抛给它(见Tag1)

  3. Flutter Engine和Native异常,isolate异常 不是runZonedGuarded和FlutterError.onError 能处理范围。

  4. isolate异常处理(见Tag2)

    原理参考特别放送 | 温故而知新,与你说说专栏的那些思考题

并发 Isolate 的异常是无法通过 try-catch 来捕获的。并发 Isolate 与主 Isolate 通信是采用 SendPort 的消息机制,而异常本质上也可以视作一种消息传递机制。所以,如果主 Isolate 想要捕获并发 Isolate 中的异常消息,可以给并发 Isolate 传入 SendPort。而创建 Isolate 的函数 spawn 中就恰好有一个类型为 SendPort 的 onError 参数,因此并发 Isolate 可以通过往这个参数里发送消息,实现异常通知。

完整Dart异常捕获代码

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    Zone.current.handleUncaughtError(details.exception, details.stack);//Tag1
    //或customerReport(details);
  };

  //Tag2
  Isolate.current.addErrorListener(
      RawReceivePort((dynamic pair) async {
        final isolateError = pair as List<dynamic>;
        customerReport(details);
      }).sendPort,
    );

  runZoned(
    () => runApp(MyApp()),
    zoneSpecification: ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
            report(line)
      },
    ),
    onError: (Object obj, StackTrace stack) {
      //Tag3
      customerReport(e, stack);
    }
  );
}

在部分或全部代码中覆盖一组有限的方法

例如print()scheduleMicrotask()

main() {
  runZoned(() {
    print("test");
  }, zoneSpecification: ZoneSpecification(
      print: (self, parent, zone, s) {
        parent.print(zone, "hook it: $s");
      }
  ));
}

//OUTPUT:hook it: test

上面实现的原理是什么呢?

简单讲就是runZoned从root Zone fork了一个子Zone,print打印时如果当前Zone

不为空则使用当前Zone的print来打印,而不使用root Zone的print方法。详细见Dart中Future、Zone、Timer的源码学习

每次代码进入或退出区域时执行一个操作

例如启动或停止计时器,或保存堆栈跟踪。

如下例子,Zone提供了一个hook点,在执行其中方法时候,可以做额外包装操作(Tag1,Tag2),比如耗时方法打印,这样在不破坏原有代码基础上实现了无侵入的统一逻辑注入。

import 'dart:async';

final total = new Stopwatch();
final user = new Stopwatch();

final specification = ZoneSpecification(run: <R>(self, parent, zone, f) {
  //Tag1
  user.start();
  try {
    return parent.run(zone, f);
  } finally {
    //Tag2
    user.stop();
  }
});

void main() {
  runZoned(() {
    total.start();
    a();
    b();
    c().then((_) {
      print(total.elapsedMilliseconds);
      print(user.elapsedMilliseconds);
    });
  }, zoneSpecification: specification);
}

void a() {
  print('a');
}

void b() {
  print('b');
}

Future<void> c() {
  return Future.delayed(Duration(seconds: 5), () => print('c'));
}

a
b
c
5005
6

将数据(称为 Zone本地值)与各个其他Zone相关联

这个作用类似java中的threadlocal,每个Zone相当于有自己值的作用范围,Zone直接值的传递和共享通过zonevalue来实现。

import 'dart:async';

void main() {
  Zone firstZone = Zone.current.fork(zoneValues: {"name": "bob"});
  Zone secondZone = firstZone.fork(zoneValues: {"extra_values": 12345});
  secondZone.run(() {
    print(secondZone["name"]); // bob
    print(secondZone["extra_values"]); // 12345
  });
}

案例说明:

和Linux类似地,当Zone做Fork的时候,会将父Zone所持有的ZoneSpecification、ZoneValues会继承下来,可以直接使用。并且是支持追加的,secondZone在firstZone的基础之上,又追加了extra_values属性,不会因为secondZone的ZoneValues就导致name属性被替换掉。

简话-Flutter异常处理 - 掘金

Zones | Dart

Brian Ford - Zones - NG-Conf 2014 - YouTube

[Flutter] 认识Zone和异常处理 - 掘金

2.8 Flutter异常捕获 | 《Flutter实战·第二版》

特别放送 | 温故而知新,与你说说专栏的那些思考题

欢迎搜索公众号:【码里特别有禅】 里面整理收集了最详细的Flutter进阶与优化指南。关注我,获取我的最新文章~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK