Flutter : 關於 Zone

JDChi發表於2021-07-04

寫在前面

在《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
複製程式碼

相關文章