W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗值獎勵
在介紹 Flutter 異常捕獲之前必須先了解一下 Dart 單線程模型,只有了解了 Dart 的代碼執(zhí)行流程,我們才能知道該在什么地方去捕獲異常。
在 Java 和 Objective-C(以下簡稱“OC”)中,如果程序發(fā)生異常且沒有被捕獲,那么程序?qū)K止,但是這在 Dart 或 JavaScript 中則不會!究其原因,這和它們的運行機制有關(guān)系。Java 和 OC 都是多線程模型的編程語言,任意一個線程觸發(fā)異常且該異常未被捕獲時,就會導(dǎo)致整個進程退出。但 Dart 和 JavaScript 不會,它們都是單線程模型,運行機制很相似(但有區(qū)別),下面我們通過 Dart 官方提供的一張圖來看看 Dart 大致運行原理:
Dart 在單線程中是以消息循環(huán)機制來運行的,其中包含兩個任務(wù)隊列,一個是“微任務(wù)隊列” microtask queue,另一個叫做“事件隊列” event queue。從圖中可以發(fā)現(xiàn),微任務(wù)隊列的執(zhí)行優(yōu)先級高于事件隊列。
現(xiàn)在我們來介紹一下 Dart 線程運行過程,如上圖中所示,入口函數(shù) main() 執(zhí)行完后,消息循環(huán)機制便啟動了。首先會按照先進先出的順序逐個執(zhí)行微任務(wù)隊列中的任務(wù),事件任務(wù)執(zhí)行完畢后程序便會退出,但是,在事件任務(wù)執(zhí)行的過程中也可以插入新的微任務(wù)和事件任務(wù),在這種情況下,整個線程的執(zhí)行過程便是一直在循環(huán),不會退出,而 Flutter 中,主線程的執(zhí)行過程正是如此,永不終止。
在 Dart 中,所有的外部事件任務(wù)都在事件隊列中,如 IO、計時器、點擊、以及繪制事件等,而微任務(wù)通常來源于 Dart 內(nèi)部,并且微任務(wù)非常少,之所以如此,是因為微任務(wù)隊列優(yōu)先級高,如果微任務(wù)太多,執(zhí)行時間總和就越久,事件隊列任務(wù)的延遲也就越久,對 于GUI 應(yīng)用來說最直觀的表現(xiàn)就是比較卡,所以必須得保證微任務(wù)隊列不會太長。值得注意的是,我們可以通過Future.microtask(…)
方法向微任務(wù)隊列插入一個任務(wù)。
在事件循環(huán)中,當(dāng)某個任務(wù)發(fā)生異常并沒有被捕獲時,程序并不會退出,而直接導(dǎo)致的結(jié)果是當(dāng)前任務(wù)的后續(xù)代碼就不會被執(zhí)行了,也就是說一個任務(wù)中的異常是不會影響其它任務(wù)執(zhí)行的。
Dart 中可以通過try/catch/finally
來捕獲代碼塊異常,這個和其它編程語言類似,如果讀者不清楚,可以查看Dart語言文檔,不再贅述,下面我們看看 Flutter 中的異常捕獲。
Flutter 框架為我們在很多關(guān)鍵的方法進行了異常捕獲。這里舉一個例子,當(dāng)我們布局發(fā)生越界或不合規(guī)范時,F(xiàn)lutter 就會自動彈出一個錯誤界面,這是因為 Flutter 已經(jīng)在執(zhí)行 build 方法時添加了異常捕獲,最終的源碼如下:
@override
void performRebuild() {
...
try {
//執(zhí)行build方法
built = build();
} catch (e, stack) {
// 有異常時則彈出錯誤提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
可以看到,在發(fā)生異常時,F(xiàn)lutter 默認(rèn)的處理方式是彈一個 ErrorWidget,但如果我們想自己捕獲異常并上報到報警平臺的話應(yīng)該怎么做?我們進入_debugReportException()
方法看看:
FlutterErrorDetails _debugReportException(
String context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector
}) {
//構(gòu)建錯誤詳情對象
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
//報告錯誤
FlutterError.reportError(details);
return details;
}
我們發(fā)現(xiàn),錯誤是通過FlutterError.reportError
方法上報的,繼續(xù)跟蹤:
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //調(diào)用了onError回調(diào)
}
我們發(fā)現(xiàn)onError
是FlutterError
的一個靜態(tài)屬性,它有一個默認(rèn)的處理方法 dumpErrorToConsole
,到這里就清晰了,如果我們想自己上報異常,只需要提供一個自定義的錯誤處理回調(diào)即可,如:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
這樣我們就可以處理那些Flutter為我們捕獲的異常了,接下來我們看看如何捕獲其它異常。
在 Flutter 中,還有一些 Flutter 沒有為我們捕獲的異常,如調(diào)用空對象方法異常、Future 中的異常。在 Dart 中,異常分兩類:同步異常和異步異常,同步異??梢酝ㄟ^try/catch
捕獲,而異步異常則比較麻煩,如下面的代碼是捕獲不了Future
的異常的:
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
Dart 中有一個runZoned(...)
方法,可以給執(zhí)行對象指定一個 Zone。Zone 表示一個代碼執(zhí)行的環(huán)境范圍,為了方便理解,讀者可以將 Zone 類比為一個代碼執(zhí)行沙箱,不同沙箱的之間是隔離的,沙箱可以捕獲、攔截或修改一些代碼行為,如 Zone 中可以捕獲日志輸出、Timer 創(chuàng)建、微任務(wù)調(diào)度的行為,同時 Zone 也可以捕獲所有未處理的異常。下面我們看看runZoned(...)
方法定義:
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
Function onError,
})
zoneValues
: Zone 的私有數(shù)據(jù),可以通過實例zone[key]
獲取,可以理解為每個“沙箱”的私有數(shù)據(jù)。zoneSpecification
:Zone 的一些配置,可以自定義一些代碼行為,比如攔截日志輸出行為等,舉個例子:
下面是攔截應(yīng)用中所有調(diào)用print
輸出日志的行為。
main() {
runZoned(() => runApp(MyApp()), zoneSpecification: new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, "Intercepted: $line");
}),
);
}
這樣一來,我們 APP 中所有調(diào)用print
方法輸出日志的行為都會被攔截,通過這種方式,我們也可以在應(yīng)用中記錄日志,等到應(yīng)用觸發(fā)未捕獲的異常時,將異常信息和日志統(tǒng)一上報。ZoneSpecification 還可以自定義一些其他行為,讀者可以查看API文檔。
onError
:Zone中未捕獲異常處理回調(diào),如果開發(fā)者提供了 onError 回調(diào)或者通過ZoneSpecification.handleUncaughtError
指定了錯誤處理回調(diào),那么這個 zone 將會變成一個 error-zone,該 error-zone 中發(fā)生未捕獲異常(無論同步還是異步)時都會調(diào)用開發(fā)者提供的回調(diào),如: runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
var details=makeDetails(obj,stack);
reportError(details);
});
這樣一來,結(jié)合上面的FlutterError.onError
我們就可以捕獲我們 Flutter 應(yīng)用中全部錯誤了!需要注意的是,error-zone 內(nèi)部發(fā)生的錯誤是不會跨越當(dāng)前 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 collectLog(String line){
... //收集日志
}
void reportErrorAndLog(FlutterErrorDetails details){
... //上報錯誤和日志邏輯
}
FlutterErrorDetails makeDetails(Object obj, StackTrace stack){
...// 構(gòu)建錯誤信息
}
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportErrorAndLog(details);
};
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
collectLog(line); // 收集日志
},
),
onError: (Object obj, StackTrace stack) {
var details = makeDetails(obj, stack);
reportErrorAndLog(details);
},
);
}
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: