前言
和Android中的Java語言類似,Dart中也可以通過try/catch/finally來捕獲程式碼塊異常。不同的是在Dart中發生異常的時候flutter APP並不會崩潰。在我的實踐中,debug版中的Dart異常會表現為紅屏加異常資訊,而release版則是空白的白屏。下面我們就從原始碼追溯Flutter的異常和捕獲
Flutter捕獲的異常
Flutter為我們提供了部分異常捕獲。在flutter開發中大家肯定遇到過螢幕程式設計紅色並帶有錯誤資訊的情況,甚至在Widget寬度越界時也會出現這樣的錯誤提示介面。雖然程式碼出現了錯誤,但是並不會導致APP崩潰,Flutter會幫我們捕獲異常。至於其中的原理,就需要我們去看一下原始碼了。
以StatelessWidget
為例,Widget
會建立對應的Element
,(至於為什麼要進入Element裡面,請大家自行了解Widget的繪製原理)其程式碼如下:
abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({ Key key }) : super(key: key);
/// Creates a [StatelessElement] to manage this widget's location in the tree.
///
/// It is uncommon for subclasses to override this method.
@override
StatelessElement createElement() => StatelessElement(this);
....
}
複製程式碼
追根溯源StatelessElement
父類到ComponentElement
,發現如下程式碼:
/// Calls the [StatelessWidget.build] method of the [StatelessWidget] object
/// (for stateless widgets) or the [State.build] method of the [State] object
/// (for stateful widgets) and then updates the widget tree.
@override
void performRebuild() {
if (!kReleaseMode && debugProfileBuildsEnabled)
...
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
_child = updateChild(null, built, slot);
}
}
複製程式碼
根據註釋不難發現,這個方法用來呼叫StatelessWidget
或者State
的build
方法,並更新Widget樹。不難發現,程式碼使用了try/catch
包裹了built = build();
也就是包裹了build
執行的方法。當捕獲到異常時使用ErrorWidget
彈出錯誤提示。不難發現ErrorWidget.builder
接受了一個_debugReportException
方法返回的FlutterErrorDetails
,並展示異常——這就是我們上文提到的紅屏吧。進入_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
上報。進入該方法:
/// 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);
}
複製程式碼
最後呼叫了onError
方法,追蹤原始碼:
static FlutterExceptionHandler onError = dumpErrorToConsole;
複製程式碼
onError
是FlutterError
的一個靜態屬性,它預設的處理方法是dumpErrorToConsole
。如果我們更改異常的處理方式,提供一個自定的錯誤回撥就可以了。
//在Flutter app入口配置自定義的異常上報回撥(customerReport)
void main(){
FlutterError.onError = (FlutterErrorDetails details) {
customerReport(details);
};
runApp(MyApp());
}
複製程式碼
在Flutter app入口配置自定義的異常上報回撥customerReport
,至於異常的上報,是在Flutter中直接請求網路還是與原生互動,我們暫不進行討論。至此,我們就可以收集和處理那些Flutter為我們捕獲的異常了。
Flutter沒有捕獲的異常(併發異常)
雖然Flutter為我們在很多關鍵的方法進行了異常捕獲,但遺憾的是,它並不能為我們捕獲併發異常。同步異常可以通過try/catch捕獲,而非同步異常則不會被捕獲:
try{
Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
print(e)
}
複製程式碼
上面的程式碼是捕獲不了Future
異常的。怎麼辦?
幸運的是,Flutter中與一個Zone
的概念,Dart中可通過Zone
表示指定程式碼執行的環境,不同的Zone程式碼上下文是不同的互不影響。類似一個沙盒概念,不同沙箱的之間是隔離的,沙箱可以捕獲、攔截或修改一些程式碼行為,我們可以再這裡面捕獲所有沒有被處理過的異常。
看一下runZoned
的原始碼:
R runZoned<R>(R body(),{
Map zoneValues,
ZoneSpecification zoneSpecification,
Function onError
}
)
複製程式碼
- zoneValues: Zone 的私有資料,可以通過例項zone[key]獲取,可以理解為每個“沙箱”的私有資料。
- zoneSpecification:Zone的一些配置,可以自定義一些程式碼行為,比如攔截日誌輸出行為等
- onError:Zone中未捕獲異常處理回撥,如果開發者提供了onError回撥或者通
我們可以通過onError
捕獲異常就像下面這樣:
runZoned(
() {
Future.error("error");
},
onError: (dynamic e, StackTrace stack) {
reportError(e, stack);
},
);
複製程式碼
我們可以讓runApp
執行在Zone中,這樣就可以捕獲我們Flutter應用中全部錯誤了!
runZoned(() {
runApp(MyApp());
}, onError: (Object obj, StackTrace stack) {
reportError(e, stack);
});
複製程式碼
zoneSpecification
則給了我們帶來了更大的可能性,它提供了forked
Zone的規範,使用在此類中給出的實力作為回撥覆蓋Zone中的預設行為。處理程式上具有相同命名方法的方法。例如:攔截應用中所有呼叫print輸出日誌的行為:
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
report(line)
},
),
);
複製程式碼
這樣使用onError
和zoneSpecification
搭配可以實現記錄日誌和手機崩潰資訊:
void main() {
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
report(line)
},
),
onError: (Object obj, StackTrace stack) {
customerReport(e, stack);
}
);
}
複製程式碼
Flutter engine 異常捕獲
flutter engine部分的異常,以Android為例,主要為libfutter.so發生的錯誤。這部分發生的異常的捕獲方式和原生髮生的異常的捕獲方式一樣,可以借用Bugly等實現。
總結
通過上述介紹,Flutter的異常捕獲整理如下:
異常收集
void main() {
FlutterError.onError = (FlutterErrorDetails details) {
customerReport(details);
};
runZoned(
() => runApp(MyApp()),
zoneSpecification: ZoneSpecification(
print: (Zone self, ZoneDelegate parent, Zone zone, String line) {
report(line)
},
),
onError: (Object obj, StackTrace stack) {
customerReport(e, stack);
}
);
}
複製程式碼
異常上報
其中異常的上報可以使用MethodChannel
傳遞給Native,實現方式略,有興趣的課參考:Flutter與android之間的通訊
而在我負責的專案中,我使用Bugly來收集異常資訊,我們可以將native接收到的Flutter異常交給Bugly上報。
而收集到的崩潰資訊則需要配置符號表(symbols)還原堆疊以確定崩潰資訊。詳情請參考:構建系統加入Flutter符號表