likes
comments
collection
share

Flutter : 关于 Zone

作者站长头像
站长
· 阅读数 10

写在前面

在《Flutter 实战》这本书里的 Flutter异常捕获 一节,讲到了如何对异步异常进行捕获,里面就提到了用 Zone 来做处理。

Zone表示一个代码执行的环境范围,为了方便理解,读者可以将Zone类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。

这一篇主要就是对 Zone 进行一些用法和参数上的了解。

内容

先从异常的例子讲起

在 Flutter 里,同步代码里异常的捕获使用try-catch进行。我们用await的方式来处理 Future,同样也是同步代码,所以也可以被try-catch捕获:

main() async {
  try {
    throw "My Error";
  } catch (e) {
    print(e);
  }

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

打印结果:

My Error
My Future Error

对于不是这种同步写法的异步错误,那么我们就需要通过Zone来处理。Zone给代码提供了一个环境,并能捕获到里面的相关信息。以《Flutter 实战》一书里给的例子,它通过 Dart 里的runZoned()方法,让当前 Zone fork 一个新的 Zone 出来,把代码放在里面执行。

R runZoned<R>(R body(),
    {Map<Object?, Object?>? zoneValues,
    ZoneSpecification? zoneSpecification,
    @Deprecated("Use runZonedGuarded instead") Function? onError}) {
  checkNotNullable(body, "body");
  ...
  return _runZoned<R>(body, zoneValues, zoneSpecification);
}

R _runZoned<R>(R body(), Map<Object?, Object?>? zoneValues,
        ZoneSpecification? specification) =>
    Zone.current
        .fork(specification: specification, zoneValues: zoneValues)
        .run<R>(body);

可以看出runZoned()其实是对Zone.current.fork().run()的一个封装。

所以通过给我们的整个 App 套上一个 Zone,就可以捕获所有的异常了。

runZoned(() {
    runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
    var details=makeDetails(obj,stack);
    reportError(details);
});

Zone

main

当我们运行main()函数的时候,其实就已经运行在一个 root 的 Zone 里:

main() {
  Zone.root.run(() => print("hello"));
  print(Zone.root == Zone.current);
}

打印结果:

hello
true

main()里调用Zone.root.run()方法跟直接在main()里没区别,而且由于已经默认运行起来,所以我们也不能对Zone进行一些定制修改,这也是为什么我们要新建一个Zone来处理。

参数

Zone 里有几个参数:

  • zoneValues
  • zoneSpecification
  • onError

zoneValues

这是 Zone 的私有数据,可以通过实例 zone[key] 获取,并且可以被自己 fork 出来的子 Zone 继承。

  Zone parentZone = Zone.current.fork(zoneValues: {"parentValue": 1});
  Zone childZone = parentZone.fork();
  // childZone 可以通过父的 key 获得相应的 value
  childZone.run(() => {print(childZone["parentValue"])});

zoneSpecification

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

abstract class ZoneSpecification {
...
  const factory ZoneSpecification(
      {HandleUncaughtErrorHandler? handleUncaughtError,
      RunHandler? run,
      RunUnaryHandler? runUnary,
      RunBinaryHandler? runBinary,
      RegisterCallbackHandler? registerCallback,
      RegisterUnaryCallbackHandler? registerUnaryCallback,
      RegisterBinaryCallbackHandler? registerBinaryCallback,
      ErrorCallbackHandler? errorCallback,
      ScheduleMicrotaskHandler? scheduleMicrotask,
      CreateTimerHandler? createTimer,
      CreatePeriodicTimerHandler? createPeriodicTimer,
      PrintHandler? print,
      ForkHandler? fork}) = _ZoneSpecification;
 ...
}
修改 print 行为

比方说在一个 Zone 里,我们想修改它的 print 行为,就可以这样做:

main() {
  Zone parentZone = Zone.current.fork(specification: new ZoneSpecification(
      print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
    parent.print(zone, "Intercepted: $line");
     // 还可以在这里把打印写入文件,发送给服务器
  }));
  parentZone.run(() => print("hello"));
  print("hello");
}

打印结果:

Intercepted: hello
hello

可以看到当我们parentZone运行起来后,print 的行为就已经被我们修改了。

由于最后面的 print 不在同一个 Zone 里,所以这个 print 不会发生改变。

修改 run 的行为

假如我们想在进入run()的时候做一些事情,就可以:

main() {
  Zone parentZone = Zone.current.fork(
      specification: new ZoneSpecification(
      run: <R>(self, parent, zone, f) {
    print("Enter run");
    return parent.run(zone, f);
  }));
  parentZone.run(() => print("hello"));
}

打印结果:

Enter run
hello
修改注册回调
main() {
  Zone.root;
  Zone parentZone = Zone.current.fork(specification:
      new ZoneSpecification(registerCallback: <R>(self, parent, zone, f) {
    // 调用我们实际注册的回调函数,第一次这里进来的 f 是 start(),第二次进来则是 end()
    f();
    return f;
  }));
  parentZone.run(() {
    Zone.current.registerCallback(() => start());
    print("hello");
    Zone.current.registerCallback(() => end());
  });
}

void start() {
  print("start");
}

void end() {
  print("end");
}

打印结果:

start
hello
end

onError

虽然runZoned()方法有onError的回调,但官方新版本里推荐是用runZonedGuarded()来代替:

 runZonedGuarded(() {
    throw "null";
  }, (Object error, StackTrace stack) {
    print("error = ${error.toString()}");
  });

打印结果

error = null

也就是说在Zone里那些没有被我们捕获的异常,都会走到onError回调里。 那么如果这个Zonespecification里实现了handleUncaughtError或者是实现了onError回调,那么这个 Zone就变成了一个error-zone

那么error-zone里发生的异常是不会跨越边界的,例如:

var future = new Future.value(499);
var future2 = future.then((_) {throw "future error";});
  runZonedGuarded(() {
    var future3 = future2.catchError((e) {
      print(e.toString()); // 不会打印
    });
  }, (Object error, StackTrace stack) {
    print(" error = ${error.toString()}"); // 不会打印
  });

future函数在error-zone的外面定义,并定义了执行完毕后会抛出异常,当在error-zone里调用的时候,此时这个异常就无法被error-zone捕获了,因为已经超出了它的边界,解决的做法就是在定义future函数那里再套一个Zone,这样这个错误就会被外面的error-zone捕获了:

  var future = new Future.value(499);
  runZonedGuarded(() {
    var future2 = future.then((_) {
      throw "future error";
    });
    runZonedGuarded(() {
      var future3 = future2.catchError((e) {
        print(e.toString());
      });
    }, (Object error, StackTrace stack) {
      print(" error = ${error.toString()}");
    });
  }, (Object error, StackTrace stack) {
    print("out error = ${error.toString()}");
  });

输出结果:

out error = future error