寫在前面
在《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
回撥裡。
那麼如果這個Zone
的specification
裡實現了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
複製程式碼