App異常,就是應(yīng)用代碼的異常,通常由未處理應(yīng)用層其他模塊所拋出的異常引起。根據(jù)異常代碼的執(zhí)行時(shí)序,App異??梢苑譃閮深?,即同步異常和異步異常:同步異常可以通過try-catch機(jī)制捕獲,異步異常則需要采用Future提供的catchError語句捕獲
//使用try-catch捕獲同步異常
try {
throw StateError('This is a Dart exception.');
}
catch(e) {
print(e);
}
//使用catchError捕獲異步異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'))
.catchError((e)=>print(e));
//***注意,以下代碼無法捕獲異步異常***
try {
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'))
}
catch(e) {
print("This line will never be executed. ");
}
需要注意的是,這兩種方式是不能混用的。可以看到,在上面的代碼中,我們是無法使用try-catch去捕獲一個(gè)異步調(diào)用所拋出的異常的。
同步的try-catch和異步的catchError,為我們提供了直接捕獲特定異常的能力,而如果我們想集中管理代碼中的所有異常,F(xiàn)lutter也提供了Zone.runZoned方法。
我們可以給代碼執(zhí)行對象指定一個(gè)Zone,在Dart中,Zone表示一個(gè)代碼執(zhí)行的環(huán)境范圍,其概念類似沙盒,不同沙盒之間是互相隔離的。如果我們想要觀察沙盒中代碼執(zhí)行出現(xiàn)的異常,沙盒提供了onError回調(diào)函數(shù),攔截那些在代碼執(zhí)行對象中的未捕獲異常。
在下面的代碼中,我們將可能拋出異常的語句放置在了Zone里。可以看到,在沒有使用try-catch和catchError的情況下,無論是同步異常還是異步異常,都可以通過Zone直接捕獲到:
runZoned(() {
//同步拋出異常
throw StateError('This is a Dart exception.');
}, onError: (dynamic e, StackTrace stack) {
print('Sync error caught by zone');
});
runZoned(() {
//異步拋出異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw StateError('This is a Dart exception in Future.'));
}, onError: (dynamic e, StackTrace stack) {
print('Async error aught by zone');
});
因此,如果我們想要集中捕獲Flutter應(yīng)用中的未處理異常,可以把main函數(shù)中的runApp語句也放置在Zone中。這樣在檢測到代碼中運(yùn)行異常時(shí),我們就能根據(jù)獲取到的異常上下文信息,進(jìn)行統(tǒng)一處理了:
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
//Do sth for error
});
Framework異常的捕獲方式
Framework異常,就是Flutter框架引發(fā)的異常,通常是由應(yīng)用代碼觸發(fā)了Flutter框架底層的異常判斷引起的。比如,當(dāng)布局不合規(guī)范時(shí),F(xiàn)lutter就會(huì)自動(dòng)彈出一個(gè)觸目驚心的紅色錯(cuò)誤界面,如下所示:

這其實(shí)是因?yàn)椋現(xiàn)lutter框架在調(diào)用build方法構(gòu)建頁面時(shí)進(jìn)行了try-catch 的處理,并提供了一個(gè)ErrorWidget,用于在出現(xiàn)異常時(shí)進(jìn)行信息提示:
@override
void performRebuild() {
Widget built;
try {
//創(chuàng)建頁面
built = build();
} catch (e, stack) {
//使用ErrorWidget創(chuàng)建頁面
built = ErrorWidget.builder(_debugReportException(ErrorDescription("building $this"), e, stack));
...
}
...
}
這個(gè)頁面反饋的信息比較豐富,適合開發(fā)期定位問題。但如果讓用戶看到這樣一個(gè)頁面,就很糟糕了。因此,我們通常會(huì)重寫ErrorWidget.builder方法,將這樣的錯(cuò)誤提示頁面替換成一個(gè)更加友好的頁面。
下面的代碼演示了自定義錯(cuò)誤頁面的具體方法。在這個(gè)例子中,我們直接返回了一個(gè)居中的Text控件
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){
return Scaffold(
body: Center(
child: Text("Custom Error Widget"),
)
);
};
比起之前觸目驚心的紅色錯(cuò)誤頁面,白色主題的自定義頁面看起來稍微友好些了。需要注意的是,ErrorWidget.builder方法提供了一個(gè)參數(shù)details用于表示當(dāng)前的錯(cuò)誤上下文,為避免用戶直接看到錯(cuò)誤信息,這里我們并沒有將它展示到界面上。但是,我們不能丟棄掉這樣的異常信息,需要提供統(tǒng)一的異常處理機(jī)制,用于后續(xù)分析異常原因。
為了集中處理框架異常,F(xiàn)lutter提供了FlutterError類,這個(gè)類的onError屬性會(huì)在接收到框架異常時(shí)執(zhí)行相應(yīng)的回調(diào)。因此,要實(shí)現(xiàn)自定義捕獲邏輯,我們只要為它提供一個(gè)自定義的錯(cuò)誤處理回調(diào)即可。
在下面的代碼中,我們使用Zone提供的handleUncaughtError語句,將Flutter框架的異常統(tǒng)一轉(zhuǎn)發(fā)到當(dāng)前的Zone中,這樣我們就可以統(tǒng)一使用Zone去處理應(yīng)用內(nèi)的所有異常了:
FlutterError.onError = (FlutterErrorDetails details) async {
//轉(zhuǎn)發(fā)至Zone中
Zone.current.handleUncaughtError(details.exception, details.stack);
};
runZoned<Future<Null>>(() async {
runApp(MyApp());
}, onError: (error, stackTrace) async {
//Do sth for error
});
異常上報(bào)
到目前為止,我們已經(jīng)捕獲到了應(yīng)用中所有的未處理異常。但如果只是把這些異常在控制臺中打印出來還是沒辦法解決問題,我們還需要把它們上報(bào)到開發(fā)者能看到的地方,用于后續(xù)分析定位并解決問題。
關(guān)于開發(fā)者數(shù)據(jù)上報(bào),目前市面上有很多優(yōu)秀的第三方SDK服務(wù)廠商,比如友盟、Bugly,以及開源的Sentry等,而它們提供的功能和接入流程都是類似的??梢灾v異常日志上報(bào)給第三方sdk廠商。