# Dart線程模型及異常捕獲
## Dart單線程模型
在Java和OC中,如果程序發生異常且沒有被捕獲,那么程序將會終止,但在Dart或JavaScript中則不會,究其原因,這和它們的運行機制有關系,Java和OC都是多線程模型的編程語言,任意一個線程觸發異常且沒被捕獲時,整個進程就退出了。但Dart和JavaScript不同,它們都是單線程模型,運行機制很相似(但有區別),下面我們通過Dart官方提供的一張圖來看看dart大致運行原理:

Dart 在單線程中是以消息循環機制來運行的,其中包含兩個任務隊列,一個是“微任務隊列” **microtask queue**,另一個叫做“事件隊列” **event queue**。從圖中可以發現,微任務隊列的執行優先級高于事件隊列。
現在我們來介紹一下Dart線程運行過程,如上圖中所示,入口函數 main() 執行完后,消息循環機制便啟動了。首先會按照先進先出的順序逐個執行微任務隊列中的任務,當所有微任務隊列執行完后便開始執行事件隊列中的任務,事件任務執行完畢后再去執行微任務,如此循環往復,生生不息。
在Dart中,所有的外部事件任務都在事件隊列中,如IO、計時器、點擊、以及繪制事件等,而微任務通常來源于Dart內部,并且微任務非常少,之所以如此,是因為微任務隊列優先級高,如果微任務太多,執行時間總和就越久,事件隊列任務的延遲也就越久,對于GUI應用來說最直觀的表現就是比較卡,所以必須得保證微任務隊列不會太長。值得注意的是,我們可以通過`Future.microtask(…)`方法向微任務隊列插入一個任務。
在事件循環中,當某個任務發生異常并沒有被捕獲時,程序并不會退出,而直接導致的結果是**當前任務**的后續代碼就不會被執行了,也就是說一個任務中的異常是不會影響其它任務執行的。
## Flutter異常捕獲
Dart中可以通過`try/catch/finally`來捕獲代碼塊異常,這個和其它編程語言類似,,如果讀者不清楚,可以查看Dart語言文檔,不在贅述,下面我們看看Flutter中的異常捕獲。
### Flutter框架異常捕獲
Flutter 框架為我們在很多關鍵的方法進行了異常捕獲。這里舉一個例子,當我們布局發生越界或不合規范時,Flutter就會自動彈出一個錯誤界面,這是因為Flutter已經在執行build方法時添加了異常捕獲,最終的源碼如下:
```
@override
void performRebuild() {
...
try {
//執行build方法
built = build();
} catch (e, stack) {
// 有異常時則彈出錯誤提示
built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
}
...
}
```
可以看到,在發生異常時,Flutter默認的處理方式是彈一個ErrorWidget,但如果我們想自己捕獲異常并上報到報警平臺的話應該怎么做?我們進入`_debugReportException()`方法看看:
```
FlutterErrorDetails _debugReportException(
String context,
dynamic exception,
StackTrace stack, {
InformationCollector informationCollector
}) {
//構建錯誤詳情對象
final FlutterErrorDetails details = FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'widgets library',
context: context,
informationCollector: informationCollector,
);
//報告錯誤
FlutterError.reportError(details);
return details;
}
```
我們發現,錯誤是通過`FlutterError.reportError`方法上報的,繼續跟蹤:
```
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details); //調用了onError回調
}
```
我們發現`onError`是`FlutterError`的一個靜態屬性,它有一個默認的處理方法 `dumpErrorToConsole`,到這里就清晰了,如果我們想自己上報異常,只需要提供一個自定義的錯誤處理回調即可,如:
```
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
```
這樣我們就可以處理那些Flutter為我們捕獲的異常了,接下來我們看看如何捕獲其它異常。
### 其它異常捕獲與日志收集
在Flutter中,還有一些Flutter沒有為我們捕獲的異常,如調用空對象方法異常、Future中的異常。在Dart中,異常分兩類:同步異常和異步異常,同步異常可以通過`try/catch`捕獲,而異步異常則比較麻煩,如下面的代碼是捕獲不了`Future`的異常的:
```
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
```
Dart中有一個`runZoned(...)` 方法,可以給執行對象指定一個Zone。Zone表示一個代碼執行的環境范圍,為了方便理解,讀者可以將Zone類比為一個代碼執行沙箱,不同沙箱的之間是隔離的,沙箱可以捕獲、攔截或修改一些代碼行為,如Zone中可以捕獲日志輸出、Timer創建、微任務調度的行為,同時Zone也可以捕獲所有未處理的異常。下面我們看看`runZoned(...)`方法定義:
```
R runZoned<R>(R body(), {
Map zoneValues,
ZoneSpecification zoneSpecification,
Function onError,
})
```
- zoneValues: Zone 的私有數據,可以通過實例`zone[key]`獲取,可以理解為每個“沙箱”的私有數據。
- zoneSpecification:Zone的一些配置,可以自定義一些代碼行為,比如攔截日志輸出行為等,舉個例子:
下面是攔截應用中所有調用`print`輸出日志的行為。
```
main() {
runZoned(() => runApp(MyApp()), zoneSpecification: new ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
parent.print(zone, "Intercepted: $line");
}),
);
}
```
這樣一來,我們APP中所有調用`print`方法輸出日志的行為都會被攔截,通過這種方式,我們也可以在應用中記錄日志,等到應用觸發未捕獲的異常時,將異常信息和日志統一上報。ZoneSpecification還可以自定義一些其他行為,讀者可以查看API文檔。
- onError:Zone中未捕獲異常處理回調,如果開發者提供了onError回調或者通過`ZoneSpecification.handleUncaughtError`指定了錯誤處理回調,那么這個zone將會變成一個error-zone,該error-zone中發生未捕獲異常(無論同步還是異步)時都會調用開發者提供的回調,如:
```
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
var details=makeDetails(obj,stack);
reportError(details);
});
```
這樣一來,結合上面的`FlutterError.onError`我們就可以捕獲我們Flutter應用中全部錯誤了!需要注意的是,error-zone內部發生的錯誤是不會跨越當前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){
...// 構建錯誤信息
}
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);
},
);
}
```
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示