Flutter線程模型/事件機制
在介紹Flutter異常捕捉原理之前邢笙,先說明一下Dart的模型严蓖。方便我們了解Dart代碼的執(zhí)行流程和獲取一個合適的異常捕捉切入點。
我們知道在Java中,如果程序運行發(fā)生異常并且沒有做捕獲處理拧额,程序會直接終止運行發(fā)生Crash宿亡。但這種情況在Dart中會有所不同常遂。Dart不同于Java的多線程模型,Dart和JavaScript類似屬于單線程模型挽荠。Dart的單線程模型是以消息循環(huán)機制來運行的克胳,其中包含兩個任務隊列,一個是“微任務隊列”microtask queue圈匆,另外一個叫“事件隊列” event queue漠另。其中微任務隊列優(yōu)先級高于事件隊列。
下圖是對Dart運行原理和事件機制的一個簡單說明:
如圖跃赚,main()函數(Dart程序的入口函數)執(zhí)行完后笆搓,消息循環(huán)機制就會啟動。main中的代碼將最先執(zhí)行纬傲,然后再執(zhí)行微任務隊列中的任務(按FIFO先進先出順序)满败,再次是事件隊列中的任務。執(zhí)行順序:Main > MicroTask > EventQueue叹括。在事件任務執(zhí)行過程中又可以插入新的微任務和事件任務算墨,所有任務執(zhí)行完畢程序就會退出。
有意思的是在事件循環(huán)中领猾,當某個任務發(fā)生異常并沒有被捕獲時米同,程序并不會退出骇扇。而直接導致的結果是當前任務的后續(xù)代碼不會被執(zhí)行。也就是說一個任務中的異常是不會影響其它任務執(zhí)行的面粮。
Flutter 異常分類
Flutter dart代碼異常(包含app 代碼異常少孝,和framework部分異常,和未處理的異步異常)
Flutter Engine 異常
Flutter Dart 代碼異常
1. Dart 代碼異常捕捉
Dart中也有try/catch/finally的存在熬苍,用于捕捉代碼同步異常稍走。
try {
result = await _methodChannel
.invokeMethod(METHOD_ADD_WALLET_START, {"cardNo": cardNo});
} catch(ignored) {
}
return result;
2. Framework異常捕捉
Flutter的框架本身也做了很多異常捕捉措施,例如著名的Flutter紅屏異常(布局發(fā)生越界或者不符合規(guī)范的寫法就會導致紅屏)柴底,就是因為Flutter在框架中已經幫我們預埋了異常的捕獲處理邏輯婿脸。這個我們可以通過Flutter的源碼來了解其實現細節(jié)。
Flutter中萬物皆Widget柄驻,無論是從StatefulWidget還是StatelessWidget中都能發(fā)現Widget在創(chuàng)建時都創(chuàng)建了對應的Element的狐树。這里以StatefulWidget源碼為例,發(fā)現其中會創(chuàng)建StatefulElement鸿脓。
abstract class StatefulWidget extends Widget {
/// Initializes [key] for subclasses.
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
...
}
進入StatefulElement抑钟,發(fā)現StatefulElement繼承于ComponentElement
class StatefulElement extends ComponentElement {
再次追蹤到ComponentElement中,會在其**performRebuild()**方法中發(fā)現熟悉的try/catch結構野哭。
void performRebuild() {
...
Widget built;
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
在這里我們發(fā)現發(fā)生異常時在塔,flutter框架對異常進行了捕捉,并且創(chuàng)建了一個ErrorWidget來做進一步的處理拨黔。Flutter的紅屏也就是從這里產生的蛔溃。這里就是我們實現Flutter框架異常捕捉的切入點。 查看_debugReportException的源碼來確定異常捕捉的具體實現方式篱蝇。_debugReportException的源碼如下:
FlutterErrorDetails _debugReportException(
DiagnosticsNode 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(details);這一句贺待。進入后發(fā)現:
/// Calls [onError] with the given details, unless it is null.
static void reportError(FlutterErrorDetails details) {
assert(details != null);
assert(details.exception != null);
if (onError != null)
onError(details);
}
}
繼續(xù)跟蹤這里的onError就會發(fā)現它是FlutterError的一個靜態(tài)屬性,有個名為dumpErrorToConsole的默認處理方法态兴。
static FlutterExceptionHandler onError = dumpErrorToConsole;
所以我們只需要實現一個自定義的FlutterError.onError來處理異常就能實現Flutter框架異常的捕捉和上報狠持。最終實現如下:
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
reportError(details);
};
...
}
3. dart的異步異常捕捉
剛才介紹的兩種異常捕捉方式并不足以應對所有的flutter異常疟位。例如下面的這種異常:
try {
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
} catch (e) {
print(e)
}
Dart有個Zone的概念瞻润,可以簡單的理解為沙箱。不同的Zone相處獨立甜刻,互不影響绍撞。借助于Zone就可以指定代碼的執(zhí)行環(huán)境,捕獲得院、攔截或者修改代碼行為傻铣。Flutter中有一個Zone.runZoned方法。
在runZoned方法的源碼中我們又看到了熟悉的onError祥绞。在這里我們注入自定義的onError回調方法非洲,用于捕獲方法1鸭限、方法2中未能捕獲處理的異常。 最終的實現如下:
runZoned(
() => MyApp(),
onError: (dynamic ex, StackTrace stack) {
reportError(ex, stack);
},
);
4. 最終實現
///flutter 應用入口
void main() {
// Flutter framework 異常捕獲
FlutterError.onError = (FlutterErrorDetails details) {
bool isDebugMode = false;
assert(() {
isDebugMode = true;
return true;
}());
if (isDebugMode) {
FlutterError.dumpErrorToConsole(details);
} else {
//profile,release兩個模式下下捕捉異常信息
reportFrameworkError(details);
}
};
//其他類型異常
runZoned(
() => runAutoSizeApp(MyApp(), width: 375, height: 667),
onError: (dynamic ex, StackTrace stack) {
reportError(ex, stack);
},
);
}
注:針對debug環(huán)境下的異常不需要捕捉上報两踏,我們任然讓它走舊有的異常處理邏輯败京。
異常信息的上報
在處理異常信息上報之前我們先來看看捕捉到的Flutter日志格式。
MissingPluginException(No implementation found for method getCache on channel com.zhongan.beeline/ZACache)
#0 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319:7)
<asynchronous suspension>
#1 CachePlugin.getCache (package:ZABank/plugin/CachePlugin.dart:34:37)
#2 new HomePageViewModel (package:ZABank/home/bloc/HomePageViewModel.dart:158:17)
#3 HomePageViewModel.create (package:ZABank/home/bloc/HomePageViewModel.dart:138:23)
#4 _NewHomeTabState.build.<anonymous closure> (package:ZABank/home/NewHomeTab.dart:36:47)
#5 BuilderStateDelegate.initDelegate (package:provider/src/delegate_widget.dart:249:14)
#6 _DelegateWidgetState._initDelegate (package:provider/src/delegate_widget.dart:118:21)
#7 _DelegateWidgetState.initState (package:provider/src/delegate_widget.dart:110:5)
#8 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4355:58)
#9 ComponentElement.mount (package:flutter/src/widgets/framework.dart:4201:5)
#10 Element.inflateWidget (package:flutter/src/widgets/framework.dart:3194:14)
#11 MultiChildRender
從上面的flutter crash日志來看梦染,我們可以將其簡單的分為兩部分:異常標題(異常類型和對應異常詳情說明)赡麦;異常堆棧信息。將這些信息上報到不同的異常監(jiān)控平臺需要做不同的處理
- 以Android端為例帕识,如果上報平臺為bugly泛粹。我們只需要將上面的異常日志分為異常標題和異常堆棧,通過methodchannel傳遞到native層肮疗,然后通過bugly接口直接上報即可晶姊。以下為bugly上報接口調用示例:
if (!CrashModule.hasInitialized()) return;
CrashReport.postException(4, excpetionType, excpetionMessage, stack, null);
-
如果我們的上報目標平臺為firebase,那需要做更進一步的處理伪货。因為firebase平臺目前給出的接口沒有bugly的靈活帽借,上報時接口只認Java的Exception對象。因此為了將flutter異常上報到firebase平臺我們需要對其進行解析轉換超歌,將其格式轉換為Java的異常類型然后再上報到firebase砍艾。
flutter異常的標題沒什么好說的,可以直接作為Java Exception的detailMessage字段巍举。
flutter異常堆棧部分按行切割后脆荷,每一行日志還需要按規(guī)則進行拆解。
以下為拆解規(guī)則:
#1 CachePlugin.getCache (package:ZABank/plugin/CachePlugin.dart:34:37)
丟棄 | class | method | file | line | 丟棄 |
---|---|---|---|---|---|
#1 | CachePlugin | getCache | package:ZABank/plugin/CachePlugin.dart | 34 | 37 |
這些字段分別對應了Java中StackTraceElement類的幾個字段懊悯。
*public final class StackTraceElement implements [java.io](http://java.io).Serializable {*
*// Normally initialized by VM (public constructor added in 1.5)*
*private String declaringClass;*
*private String methodName;*
*private String fileName;*
*private int lineNumber;*
最終我們就能在firebase中看見上報的flutter異常蜓谋。