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