Flutter異常原始碼和一些經驗總結
Flutter 異常指的是,Flutter 程式中 Dart 程式碼執行時意外發生的錯誤事件。我們可以通過與 Java 類似的 try-catch 機制來捕獲它。但與 Java 不同的是,Dart 程式不強制要求我們必須處理異常。
這是因為,Dart 採用事件迴圈的機制來執行任務,所以各個任務的執行狀態是互相獨立的。也就是說,即便某個任務出現了異常我們沒有捕獲它,Dart 程式也不會退出,只會導致當前任務後續的程式碼不會被執行,使用者仍可以繼續使用其他功能。也因為flutter本身是單執行緒模型,是一種事件驅動機制而產生的,
Dart 異常,根據來源又可以細分為 App 異常和 Framework 異常。Flutter 為這兩種異常提供了不同的捕獲方式,接下來我們就一起看看吧。
Flutter異常型別
Flutter異常主要分為了兩大型別Exception
和Error
我們先來看看定義
1 Exception
abstract class Exception {
factory Exception([var message]) => _Exception(message);
}
/** Default implementation of [Exception] which carries a message. */
class _Exception implements Exception {
final dynamic message;
_Exception([this.message]);
String toString() {
Object? message = this.message;
if (message == null) return "Exception";
return "Exception: $message";
}
}
複製程式碼
2 Error
class Error {
Error(); // Prevent use as mixin.
static String safeToString(Object? object) {
if (object is num || object is bool || null == object) {
return object.toString();
}
if (object is String) {
return _stringToSafeString(object);
}
return _objectToString(object);
}
/** Convert string to a valid string literal with no control characters. */
external static String _stringToSafeString(String string);
external static String _objectToString(Object object);
// 獲取堆疊資訊
external StackTrace? get stackTrace;
}
複製程式碼
異常產生
Flutter異常產生自定義異常和系統內部產生的異常,都是通過throw
丟擲來
比如
throw Error()
throw ('異常')
throw Exception('異常')
複製程式碼
異常捕獲
Flutter異常:基本這種寫法能涵蓋大部分的型別異常
Future<void> main() async {
FlutterError.onError = (details) async {
if (kDebugMode {
/// 將錯誤輸出到控制檯
FlutterError.dumpErrorToConsole(details);
} else {
/// 將Framework的異常轉發到當前Zone的onError回撥中
Zone.current.handleUncaughtError(details.exception, details.stack);
}
};
runZoned(() {
runApp(App()); //應用首頁的第一個Widget
}, onError: (error, stackTrace) async {
/// 所有的Flutter異常都會在此處統一捕獲。有些異常可能debug下無法捕獲
final String crashMessage = error.toString() ?? '';
final String crashStack = stackTrace.toString();
final String _crashMessage = crashMessage.toLowerCase();
});
}
複製程式碼
為啥可以重寫這個方法呢?答案在framework.dart
中的Element
中的performRebuild
方法
ErrorWidget.builder是一個類的靜態方法是可以覆蓋的。同樣的問題是FlutterError.error也是一個靜態方法也是可以覆蓋的。所以我們在獲取日誌和設定統一錯誤頁面的時候原因就找到了根源所在。
但是FlutterError.error僅僅只是捕獲由於framework.dart而產生的異常,也就是通常所說的介面異常。另外的比如陣列越界,空安全異,空物件方法異常、Future中的異常等,就只能通過Zone進行捕獲
因此我們可以統一去處理佈局型別產生的異常,而不會出現標紅(release模型下會變成灰色很醜)
ErrorWidget.builder = (_) {
return Text('錯誤頁面');
};
複製程式碼
Framework捕獲的異常
我們先來看看framework.dart 丟擲來的異常 一般是flutter本身框架而產生的佈局頁面產生的異常
@override
void performRebuild() {
...
try {
built = build();
/// 會丟擲異常就是上面圖片飈紅的地方
debugWidgetBuilderValue(widget, built);
...
} catch (e, stack) {
_debugDoingBuild = false;
built = ErrorWidget.builder(
...
);
} finally {
/// _dirty標誌該元件是否需要重新build
_dirty = false;
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
built = ErrorWidget.builder(
_debugReportException()
...
);
}
}
複製程式碼
_debugReportException裡面是通過FlutterError.reportError(details)進行異常資料上報的;當然
FlutterError
還有其他方法比如
/// 最多的日誌條數 設定的預設是100
wrapWidth
/// 輸出錯誤到控制檯
dumpErrorToConsole
複製程式碼
Zone捕獲的異常
Zone是可以理解為是一個盒子的概念,Dart異常捕獲都是我們寫大dart程式碼產生的異常。比如說空異常,空安全,陣列越界,方法呼叫等這種型別的異常,目前都是捕獲方式有兩種方式try...catch
和catchError
。如果異常被我們自己的程式碼捕獲了是不會往頂層拋的
Dart異常程式碼捕獲:
第一種方式:
test().then((_) {
print("執行");
}).catchError((e) {
print("能捕獲異常");
});
Future test() {
return Future(() {
throw "error";
});
}
第二種方式:
try {
await test();
} catch (e) {
print("捕獲異常");
}
複製程式碼
但是上面的方法只能捕獲同步而產生的異常。不能捕獲非同步產生的異常。引出zone的作用就來了。
zone的作用其實就是一個隔離環境。比如如果你想攔截所有的日誌加上特殊的東西就可以通過zoneSpecification
,onError
用來收取為捕獲的異常比如上面的非同步產生的異常
runZoned(() {
runApp(App()); //應用首頁的第一個Widget
},
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
/// 所有的日誌都會在這裡出現 在這裡你可以自己過濾
// parent.print(zone, "Intercepted: $line");
}),
複製程式碼
典型異常
影響功能的異常
比如介面異常
Text(null);
複製程式碼
debug狀態下回變紅,release模式下會出現灰色螢幕(體驗非常不好),那怎麼做呢重寫ErrorWidget.builder
ErrorWidget.builder = (FlutterErrorDetails errorDetails) {
return Text('自定義頁面')
};
複製程式碼
Matrix4.scale() 引起的異常,
這個其實是系統內部的一個bug, 你可以通過修改原始碼來處理(風險自控)
double sx = 0;
double sy = 0;
double sz = 0;
複製程式碼
Provider(Provider._inheritedElementOf)異常
異常資訊
NoSuchMethodError: The getter 'widget' was called on null
複製程式碼
寫一個BaseViewModel基類,利用dispose方法來確保notifyListeners的正確性,讓你自己的viewModel繼承BaseViewModel即可
/// 解決 notifyListeners 被釋放後仍然會拋異常
class BaseViewModel extends ChangeNotifier {
bool _destroy = false;
bool get destroy => _destroy;
@override
void notifyListeners() {
if (!_destroy) {
/// 物件銷燬的時候無必要去重新發起 這個異常其實本身是InheritWidget裡面丟擲來的
super.notifyListeners();
}
}
@override
void dispose() {
super.dispose();
_destroy = true;
}
}
複製程式碼
setState異常
會有比如在使用者在佈局未完成的時的時候觸發了setState導致的可以加個判斷
if(mouted) {
setState({});
}
複製程式碼
方法呼叫異常
有個安全的做法是類似的
當方法
model.funcA() 方法呼叫異常有兩種辦法
第一種:
if(model.funcA != null) {
}
第二種:
model?.funcA?.call()
複製程式碼