Flutter 異常
Flutter 異常
指的是,F(xiàn)lutter 程序中 Dart 代碼運(yùn)行時(shí)意外發(fā)生的錯(cuò)誤事件。我們可以通過與 Swift 類似的 try-catch
機(jī)制來捕獲它赋除。但與 Swift 不同的是纵顾,Dart 程序不強(qiáng)制要求我們必須處理異常蔽午。
這是因?yàn)椋?code>Dart 采用事件循環(huán)的機(jī)制
來運(yùn)行任務(wù),所以各個(gè)任務(wù)的運(yùn)行狀態(tài)是互相獨(dú)立
的。也就是說,即便某個(gè)任務(wù)出現(xiàn)了異常我們沒有捕獲它浮禾,Dart
程序也不會(huì)退出
交胚,只會(huì)導(dǎo)致當(dāng)前任務(wù)后續(xù)的代碼不會(huì)被執(zhí)行,用戶仍可以繼續(xù)使用其他功能盈电。
Dart 異常
蝴簇,根據(jù)來源又可以細(xì)分為 App 異常
和 Framework 異常
。Flutter 為這兩種異常提供了不同的捕獲方式匆帚。
APP異常捕獲
App 異常
熬词,就是應(yīng)用代碼的異常,通常由未處理應(yīng)用層其他模塊所拋出的異常引起吸重。根據(jù)異常代碼的執(zhí)行時(shí)序互拾,App 異常可以分為兩類嚎幸,即同步異常
和異步異常
:同步異常
可以通過 try-catch
機(jī)制捕獲颜矿,異步異常
則需要采用Future
提供的catchError
語句捕獲。
這兩種異常的捕獲方式嫉晶,如下代碼所示:
// 使用 try-catch 捕獲同步異常
try {
throw SYReportException('發(fā)生一個(gè)dart 同步異常');
}
catch(e) {
print(e);
}
// 使用 catchError 捕獲異步異常
Future.delayed(Duration(seconds: 1)).then((e) {
if (sendFlag) {
print('異步異常發(fā)生之前 >>>>>>>>>>>');
throw SYReportException('發(fā)生一個(gè)dart 異步異常');
}
print('異步異常后執(zhí)行的代碼 <<<<<<<<<<<');
}).catchError((error){
print('error);
});
// 注意骑疆,以下代碼無法捕獲異步異常
try {
Future.delayed(Duration(seconds: 1)).then((e) {
if (sendFlag) {
print('異步異常發(fā)生之前 >>>>>>>>>>>');
throw SYReportException('發(fā)生一個(gè)dart 異步異常');
}
print('異步異常后執(zhí)行的代碼 <<<<<<<<<<<');
});
} catch (e) {
print("這是不會(huì)執(zhí)行的. ");
}
需要注意的是,這兩種方式是不能混用的替废」棵可以看到,在上面的代碼中椎镣,我們是無法使用 try-catch 去捕獲一個(gè)異步調(diào)用所拋出的異常
的诈火。
同步的 try-catch
和異步的 catchError
,為我們提供了直接捕獲特定異常的能力状答,而如果我們想集中管理
代碼中的所有異常柄瑰,F(xiàn)lutter 也提供了 Zone.runZoned
方法。
我們可以給代碼執(zhí)行對(duì)象指定一個(gè) Zone
剪况,在 Dart 中,Zone
表示一個(gè)代碼執(zhí)行的環(huán)境范圍
蒲跨,其概念類似沙盒
译断,不同沙盒
之間是互相隔離的。如果我們想要觀察沙盒中代碼執(zhí)行出現(xiàn)的異常或悲,沙盒提供了 onError 回調(diào)函數(shù)
孙咪,攔截那些在代碼執(zhí)行對(duì)象中的未捕獲異常。
在下面的代碼中巡语,我們將可能拋出異常的語句放置在了 Zone 里翎蹈。可以看到男公,在沒有使用try-catch
和 catchError
的情況下荤堪,無論是同步異常還是異步異常,都可以通過Zone
直接捕獲到:
runZoned(() {
// 同步拋出異常
throw SYReportException('發(fā)生一個(gè)dart 同步異常');
}, onError: (dynamic e, StackTrace stack) {
print('zone捕獲到了同步異常');
});
runZoned(() {
// 異步拋出異常
Future.delayed(Duration(seconds: 1))
.then((e) => throw SYReportException('發(fā)生一個(gè)dart 異步異常'));
}, onError: (dynamic e, StackTrace stack) {
print('zone捕獲到了異步異常');
});
因此,如果我們想要集中捕獲 Flutter 應(yīng)用中的未處理異常澄阳,可以把 main 函數(shù)中的 runApp
語句也放置在 Zone
中拥知。這樣在檢測(cè)到代碼中運(yùn)行異常時(shí),我們就能根據(jù)獲取到的異常上下文信息碎赢,進(jìn)行統(tǒng)一處理了:
runZonedGuarded(() {
runApp(MyApp());
}, (error, stackTrace) {
// 這個(gè)閉包中發(fā)生的Exception是捕獲不到的 @山竹
SYExceptionReportChannel.reportException(error, stackTrace);
}, zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
// 記錄所有的打印日志
parent.print(zone, "line是啥:$line");
},
));
接下來低剔,我們?cè)倏纯?Framework 異常應(yīng)該如何捕獲吧。
Framework 異常的捕獲
Framework 異常
肮塞,就是 Flutter 框架
引發(fā)的異常
襟齿,通常是由應(yīng)用代碼觸發(fā)了 Flutter 框架底層的異常判斷
引起的。比如枕赵,當(dāng)布局不合規(guī)范時(shí)猜欺,F(xiàn)lutter 就會(huì)自動(dòng)彈出一個(gè)觸目驚心的紅色錯(cuò)誤界面,如下所示:
這其實(shí)是因?yàn)椋?code>Flutter 框架在調(diào)用 build
方法構(gòu)建頁(yè)面時(shí)進(jìn)行了 try-catch
的處理烁设,并提供了一個(gè) ErrorWidget
替梨,用于在出現(xiàn)異常時(shí)進(jìn)行信息提示:
這個(gè)頁(yè)面反饋的信息比較豐富,適合開發(fā)期定位問題装黑。但如果讓用戶看到這樣一個(gè)頁(yè)面副瀑,就很糟糕了。因此恋谭,我們通常會(huì)重寫 ErrorWidget.builder
方法糠睡,將這樣的錯(cuò)誤提示頁(yè)面替換成一個(gè)更加友好的頁(yè)面。
下面的代碼演示了自定義錯(cuò)誤頁(yè)面的具體方法疚颊。在這個(gè)例子中狈孔,我們自定義了錯(cuò)誤頁(yè)面,顯示導(dǎo)航欄和可滾動(dòng)的錯(cuò)誤信息:
// 重寫 ErrorWidget 的builder材义,顯示地優(yōu)雅一些
ErrorWidget.builder = (FlutterErrorDetails details) {
print('錯(cuò)誤widget詳細(xì)的錯(cuò)誤信息為:' + details.toString());
return MaterialApp(
title: 'Error Widget',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: Scaffold(
appBar: AppBar(
title: Text('Widget渲染異常>椤!其掂!'),
),
body: _createBody(details),
),
);
};
運(yùn)行效果如下所示:
比起之前觸目驚心的紅色錯(cuò)誤頁(yè)面油挥,自定義的看起來優(yōu)雅一些,當(dāng)然也可以找UI幫忙設(shè)計(jì)更友好的界面款熬。需要注意的是深寥,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)的所有異常了:
// framework異常捕獲,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
FlutterError.onError = (FlutterErrorDetails details) async {
Zone.current.handleUncaughtError(details.exception, details.stack);
};
異常上報(bào)
到目前為止排拷,我們已經(jīng)捕獲到了應(yīng)用中所有的未處理異常侧漓。但如果只是把這些異常在控制臺(tái)中打印出來還是沒辦法解決問題,我們還需要把它們上報(bào)到開發(fā)者能看到的地方监氢,用于后續(xù)分析定位并解決問題布蔗。
三方,我們一般都是用bugly
浪腐。如果公司有自研的bug系統(tǒng)纵揍,那就更好了。
這些異常上報(bào)议街,我們將使用MethodChannel
推送給Native
泽谨,由Native
上報(bào)到bugly或自研的異常系統(tǒng)。
這里只展示Dart的代碼實(shí)現(xiàn)特漩,至于Native怎么實(shí)現(xiàn)Channel吧雹,自行Google即可
Dart實(shí)現(xiàn)
代碼如下:
/// flutter exception channel
class SYExceptionReportChannel {
static const MethodChannel _channel =
const MethodChannel('sy_exception_channel');
// 上報(bào)異常
static reportException(dynamic error, dynamic stack) {
print('捕獲的異常類型 >>> : ${error.runtimeType}');
print('捕獲的異常信息 >>> : $error');
print('捕獲的異常堆棧 >>> : $stack');
Map reportMap = {
'type': "${error.runtimeType}",
'title': error.toString(),
'description': stack.toString()
};
// 得使用這個(gè)
print('這是通過convert轉(zhuǎn)的json');
print(jsonEncode(reportMap));
_channel.invokeListMethod('reportException', reportMap);
}
}
我們捕獲到的異常后,由channel推送給Native涂身,包含三個(gè)信息:
- 異常的類型信息
- 異常的簡(jiǎn)要說明信息(即error的toString的值)
- 異常的堆棧信息
優(yōu)化雄卷、封裝及問題點(diǎn)
綜合上述的闡述,我們將代碼做一些封裝和優(yōu)化蛤售。
優(yōu)化:異常捕獲后龙亲,在debug和release的模式下是不一樣的處理,debug模式悍抑,直接打印到控制臺(tái)是最直觀的,release模式下杜耙,無法感知哪里出了問題搜骡,所以我們需要上報(bào),然后分析問題佑女。
區(qū)分當(dāng)前是debug還是release记靡,有一個(gè)比較巧妙的方式谈竿,代碼及注釋如下:
// 比較巧妙的一種方式判定是否是debug模式
static bool get isInDebugMode {
bool inDebugMode = false;
// 如果debug模式下會(huì)觸發(fā)賦值,只有在debug模式下才會(huì)執(zhí)行assert
assert(inDebugMode = true);
return inDebugMode;
}
基于上述的思路摸吠,我們將未捕獲的異常轉(zhuǎn)發(fā)到zone做一個(gè)判斷:
// framework異常捕獲空凸,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
FlutterError.onError = (FlutterErrorDetails details) async {
// debug模式
if (ExceptionReportUtil.isInDebugMode) {
// 打印到控制臺(tái)
FlutterError.dumpErrorToConsole(details);
// release模式
} else {
// 轉(zhuǎn)發(fā)到zone
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
封裝:main函數(shù)中的代碼,自然是越簡(jiǎn)練越好寸痢,但將未捕獲的異常轉(zhuǎn)發(fā)到zone及錯(cuò)誤Widget重寫必須放在main中呀洲,所以抽取一個(gè)工具類
ExceptionReportUtil
/// 工具類
class ExceptionReportUtil {
// 比較巧妙的一種方式判定是否是debug模式
static bool get isInDebugMode {
bool inDebugMode = false;
// 如果debug模式下會(huì)觸發(fā)賦值,只有在debug模式下才會(huì)執(zhí)行assert
assert(inDebugMode = true);
return inDebugMode;
}
// 初始化異常捕獲配置
static void initExceptionCatchConfig() {
// framework異常捕獲啼止,轉(zhuǎn)發(fā)到當(dāng)前的 Zone
FlutterError.onError = (FlutterErrorDetails details) async {
// debug模式
if (ExceptionReportUtil.isInDebugMode) {
// 打印到控制臺(tái)
FlutterError.dumpErrorToConsole(details);
// release模式
} else {
// 轉(zhuǎn)發(fā)到zone
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
// 重寫 ErrorWidget 的builder道逗,顯示地優(yōu)雅一些
ErrorWidget.builder = (FlutterErrorDetails details) {
print('錯(cuò)誤widget詳細(xì)的錯(cuò)誤信息為:' + details.toString());
return MaterialApp(
title: 'Error Widget',
theme: ThemeData(
primarySwatch: Colors.red,
),
home: Scaffold(
appBar: AppBar(
title: Text('Widget渲染異常!O追场滓窍!'),
),
body: _createBody(details),
),
);
};
}
// 創(chuàng)建錯(cuò)誤widget body
static Widget _createBody(dynamic details) {
// 正確代碼
return Container(
color: Colors.white,
child: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
details.toString(),
style: TextStyle(color: Colors.red),
),
),
),
);
}
}
問題點(diǎn): 在runZonedGuarded函數(shù)的閉包中接收未捕獲的異常,然后上報(bào)巩那,如果執(zhí)行該閉包中的代碼發(fā)生異常吏夯,是無法捕獲的:
代碼及注釋如下:
main(List<String> args) {
// 初始化Exception 捕獲配置
ExceptionReportUtil.initExceptionCatchConfig();
runZonedGuarded(() {
runApp(MyApp());
}, (error, stackTrace) {
// 這個(gè)閉包中發(fā)生的Exception是捕獲不到的 @山竹
SYExceptionReportChannel.reportException(error, stackTrace);
}, zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
// 記錄所有的打印日志
parent.print(zone, "line是啥:$line");
},
));
}
我們通過SYExceptionReportChannel.reportException(error, stackTrace)
將錯(cuò)誤上報(bào)給Native,但在Native如果沒有實(shí)現(xiàn)channel的鏈接即横,那么必然會(huì)報(bào)MissingPluginException
噪生,這個(gè)異常是不在當(dāng)前的zone中的,所以無法捕獲令境。