衆所周知,軟件項目的交付是一個複雜的過程,任何緣由都有可能致使交付的失敗。不少時候常常遇到的一個現象是,應用在開發測試時沒有任何異常,但一旦上線就問題頻出。出現這些異常,多是由於不充分的機型適配或者用戶糟糕的網絡情況形成的,也多是Flutter框架自身缺陷形成的,甚至是操做系統底層的問題。android
而處理此類異常的最佳方式是捕獲用戶的異常信息,將異常現場保存起來並上傳至服務器,而後經過分析異常上下文來定位引發異常的緣由,並最終解決此類問題。ios
所謂Flutter異常,指的是Flutter程序中Dart代碼運行時發生的錯誤。與Java和OC等多線程模型的編程語言不一樣,Dart是一門單線程的編程語言,採用事件循環機制來運行任務,因此各個任務的運行狀態是互相獨立的。也便是說,當程序運行過程當中出現異常時,並不須要像Java那樣使用try-catch機制來捕獲異常,由於即使某個任務出現了異常,Dart程序也不會退出,只會致使當前任務後續的代碼不會被執行,而其它功能仍然能夠繼續使用。git
在Flutter開發中,根據異常來源的不一樣,能夠將異常分爲Framework異常和Dart異常。Flutter對這兩種異常提供了不一樣的捕獲方式,Framework異常是由Flutter框架引起的異常,一般是因爲錯誤的應用代碼形成Flutter框架底層的異常判斷引發的,當出現Framework異常時,Flutter會自動彈出一個的紅色錯誤界面。而對於Dart異常,則可使用try-catch機制和catchError語句進行處理。github
除此以外,Flutter還提供了集中處理框架異常的方案。集中處理框架異常須要使用Flutter提供的FlutterError類,此類的onError屬性會在接收到框架異常時執行相應的回調。所以,要實現自定義捕獲異常邏輯,只須要爲它提供一個自定義的錯誤處理回調函數便可。編程
在Flutter開發中,根據異常來源的不一樣,能夠將異常分爲Framework異常和Dart異常。所謂Dart異常,指的是應用代碼引發的異常。根據異常代碼的執行時序,Dart異常能夠分爲同步異常和異步異常兩類。對於同步異常,可使用try-catch機制來進行捕獲,而異步異常的捕獲則比較麻煩,須要使用Future提供的catchError語句來進行捕獲,以下所示。安全
//使用try-catch捕獲同步異常 try { throw StateError('This is a Dart exception'); }catch(e) { print(e); } //使用catchError捕獲異步異常 Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')) .catchError((e)=>print(e));
須要說明的是,對於異步調用所拋出的異常是沒法使用try-catch語句進行捕獲的,所以下面的寫法就是錯誤的。服務器
//如下代碼沒法捕獲異步異常 try { Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future')) }catch(e) { print("This line will never be executed"); }
所以,對於Dart中出現的異常,同步異常使用的是try-catch,異步異常則使用的是catchError。若是想集中管理代碼中的全部異常,那麼能夠Flutter提供的Zone.runZoned()方法。在Dart語言中,Zone表示一個代碼執行的環境範圍,其概念相似沙盒,不一樣沙盒之間是互相隔離的。若是想要處理沙盒中代碼執行出現的異常,可使用沙盒提供的onError回調函數來攔截那些在代碼執行過程當中未捕獲的異常,以下所示。網絡
//同步拋出異常 runZoned(() { throw StateError('This is a Dart exception.'); }, onError: (dynamic e, StackTrace stack) { print('Sync error caught by zone'); }); //異步拋出異常 runZoned(() { Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')); }, onError: (dynamic e, StackTrace stack) { print('Async error aught by zone'); });
能夠看到,在沒有使用try-catch、catchError語句的狀況下,不管是同步異常仍是異步異常,均可以使用Zone直接捕獲到。
同時,若是須要集中捕獲Flutter應用中未處理的異常,那麼能夠把main函數中的runApp語句也放置在Zone中,這樣就能夠在檢測到代碼運行異常時對捕獲的異常信息進行統一處理,以下所示。多線程
runZoned<Future<Null>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { //異常處理 });
除了Dart異常外,Flutter應用開發中另外一個比較常見的異常是Framework異常。Framework異常指的是Flutter框架引發的異常,一般是因爲執行錯誤的應用代碼形成Flutter框架底層異常判斷引發的,當出現Framework異常時,系統會自動彈出一個的紅色錯誤界面,以下圖所示。
之因此會彈出一個錯誤提示頁面,是因爲系統在調用build()方法構建頁面時會進行try-catch處理,若是出現任何錯誤就會調用ErrorWidget頁面展現異常信息,而且Flutter框架在不少關鍵位置都自動進行了異常捕獲處理。架構
一般,此頁面反饋的錯誤信息對於開發環境的問題定位仍是頗有幫助的,但若是讓線上用戶也看到這樣的錯誤頁面,體驗上就不是很友比如較了。對於Framework異常,最通用的處理方式就是重寫ErrorWidget.builder()方法,而後將默認的錯誤提示頁面替換成一個更加友好的自定義提示頁面,以下所示。
ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){ //自定義錯誤提示頁面 return Scaffold( body: Center( child: Text("Custom Error Widget"), ) ); };
一般,只有當代碼運行出現錯誤時,系統纔會給出異常錯誤提示。爲了說明Flutter捕獲異常的工做流程,首先來看一個越界訪問的示例。首先,新建一個Flutter項目,而後修改main.dart文件的代碼,以下所示。
class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { List<String> numList = ['1', '2']; print(numList[5]); return Container(); } }
上面的代碼模擬的是一個越界訪問的異常場景。當運行上面的代碼時,控制檯會給出以下的錯誤信息。
RangeError (index): Invalid value: Not in range 0..2, inclusive: 5
對於程序中出現的異常,一般只須要在Flutter應用程序的入口main.dart文件中,使用Flutter提供的FlutterError類集中處理便可,以下所示。
Future<Null> main() async { FlutterError.onError = (FlutterErrorDetails details) async { Zone.current.handleUncaughtError(details.exception, details.stack); }; runZoned<Future<void>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { await _reportError(error, stackTrace); }); } Future<Null> _reportError(dynamic error, dynamic stackTrace) async { print('catch error='+error); }
同時,對於開發環境和線上環境還須要區別對待。由於,對於開發環境遇到的錯誤,通常是能夠當即定位並修復問題的,而對於線上問題才須要對日誌進行上報。所以,對於錯誤日誌上報,須要對開發環境和線上環境進行區分對待,以下所示。
Future<Null> main() async { FlutterError.onError = (FlutterErrorDetails details) async { if (isDebugMode) { FlutterError.dumpErrorToConsole(details); } else { Zone.current.handleUncaughtError(details.exception, details.stack); } }; … //省略其餘代碼 } bool get isDebugMode { bool inDebugMode = false; assert(inDebugMode = true); return inDebugMode; }
目前爲止,咱們已經對應用中出現的全部未處理異常進行了捕獲,不過這些異常還只能被保存在移動設備中,若是想要將這些異常上報到服務器還須要作不少的工做。
目前,支持Flutter異常的日誌上報的方案有Sentry、Crashlytics等。其中,Sentry是收費的且免費天數只有13天左右,不過它提供的Flutter插件能夠幫助開發者快速接入日誌上報功能。Crashlytics是Flutter官方支持的日誌上報方案,開源且免費,缺點是沒有公開的Flutter插件,而flutter_crashlytics插件接入起來也比較麻煩。
Sentry是一個商業級的日誌管理系統,支持自動上報和手動上報兩種方方。在Flutter開發中,因爲Sentry提供了Flutter插件,所以若是有日誌上報的需求,Sentry是一個不錯的選擇。
使用Sentry以前,須要先在官方網站註冊開發者帳號。若是尚未Sentry帳號,能夠先註冊一個,而後再建立一個App工程。等待工程建立完成以後,系統會自動生成一個DSN,能夠依次點擊【Project】→【Settings 】→【Client Keys】來打開DSN,以下圖所示。
接下來,使用Android Studio打開Flutter工程,在pubspec.yaml文件中添加Sentry插件依賴,以下所示。
dependencies: sentry: ">=3.0.0 <4.0.0"
而後,使用flutter packages get命令將插件拉取到本地。使用Sentry以前,須要先建立一個SentryClient對象,以下所示。
const dsn=''; final SentryClient _sentry = new SentryClient(dsn: dsn);
爲了方便對錯誤日誌進行上傳,能夠提供一個日誌的上報方法,而後在須要進行日誌上報的地方調用日誌上報方法便可,以下所示。
Future<void> _reportError(dynamic error, dynamic stackTrace) async { _sentry.captureException( exception: error, stackTrace: stackTrace, ); } runZoned<Future<void>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) { _reportError(error, stackTrace); //上傳異常日誌 });
同時,開發環境遇到的異常一般是不須要上報的,由於能夠當即定位並修復問題,線上遇到的問題才須要進行上報,所以在進行異常上報時還須要區分開發環境和線上環境。
const dsn='https://872ea62a55494a73b73ee139da1c1449@sentry.io/5189144'; final SentryClient _sentry = new SentryClient(dsn: dsn); Future<Null> main() async { FlutterError.onError = (FlutterErrorDetails details) async { if (isInDebugMode) { FlutterError.dumpErrorToConsole(details); } else { Zone.current.handleUncaughtError(details.exception, details.stack); } }; runZoned<Future<Null>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { await _reportError(error, stackTrace); }); } Future<Null> _reportError(dynamic error, dynamic stackTrace) async { if (isInDebugMode) { print(stackTrace); return; } final SentryResponse response = await _sentry.captureException( exception: error, stackTrace: stackTrace, ); //上報結果處理 if (response.isSuccessful) { print('Success! Event ID: ${response.eventId}'); } else { print('Failed to report to Sentry.io: ${response.error}'); } } bool get isInDebugMode { bool inDebugMode = false; assert(inDebugMode = true); return inDebugMode; }
在真機上運行Flutter應用,若是出現錯誤,就能夠在Sentry服務器端看到對應的錯誤日誌,以下圖所示。
除此以外,目前市面上還有不少優秀的日誌採集服務廠商,如Testin、Bugly和友盟等,不過它們大多尚未提供Flutter接入方案,所以須要開發者在原平生臺進行接入。
目前,Bugly尚未提供Flutter插件,那麼,咱們針對混合工程,能夠採用下面的方案。接入Bugly時,只須要完成一些前置應用信息關聯綁定和 SDK 初始化工做,就可使用 Dart 層封裝好的數據上報接口去上報異常了。能夠看到,對於一個應用而言,接入數據上報服務的過程,整體上能夠分爲兩個步驟:
這兩步對應着在 Dart 層須要封裝的 2 個原生接口調用,即 setup 和 postException,它們都是在方法通道上調用原生代碼宿主提供的方法。考慮到數據上報是整個應用共享的能力,所以咱們將數據上報類 FlutterCrashPlugin 的接口都封裝成了單例,以下所示。
class FlutterCrashPlugin { //初始化方法通道 static const MethodChannel _channel = const MethodChannel('flutter_crash_plugin'); static void setUp(appID) { //使用app_id進行SDK註冊 _channel.invokeMethod("setUp",{'app_id':appID}); } static void postException(error, stack) { //將異常和堆棧上報至Bugly _channel.invokeMethod("postException",{'crash_message':error.toString(),'crash_detail':stack.toString()}); } }
Dart 層是原生代碼宿主的代理,能夠看到這一層的接口設計仍是比較簡單的。接下來,咱們分別去接管數據上報的 Android 和 iOS 平臺上完成相應的實現便可。
考慮到 iOS 平臺的數據上報配置工做相對較少,所以咱們先用 Xcode 打開 example 下的 iOS 工程進行插件開發工做。須要注意的是,因爲 iOS 子工程的運行依賴於 Flutter 工程編譯構建產物,因此在打開 iOS 工程進行開發前,你須要確保整個工程代碼至少 build 過一次,不然 IDE 會報錯。如下是Bugly 異常上報 iOS SDK 接入指南
首先,咱們須要在插件工程下的 flutter_crash_plugin.podspec 文件中引入 Bugly SDK,即 Bugly,這樣咱們就能夠在原生工程中使用 Bugly 提供的數據上報功能了。
Pod::Spec.new do |s| ... s.dependency 'Bugly' end
而後,在原生接口 FlutterCrashPlugin 類中,依次初始化插件實例、綁定方法通道,並在方法通道中前後爲 setup 與 postException 提供 Bugly iOS SDK 的實現版本,以下所示。
@implementation FlutterCrashPlugin + (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar { //註冊方法通道 FlutterMethodChannel* channel = [FlutterMethodChannel methodChannelWithName:@"flutter_crash_plugin" binaryMessenger:[registrar messenger]]; //初始化插件實例,綁定方法通道 FlutterCrashPlugin* instance = [[FlutterCrashPlugin alloc] init]; //註冊方法通道回調函數 [registrar addMethodCallDelegate:instance channel:channel]; } - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if([@"setUp" isEqualToString:call.method]) { //Bugly SDK初始化方法 NSString *appID = call.arguments[@"app_id"]; [Bugly startWithAppId:appID]; } else if ([@"postException" isEqualToString:call.method]) { //獲取Bugly數據上報所須要的各個參數信息 NSString *message = call.arguments[@"crash_message"]; NSString *detail = call.arguments[@"crash_detail"]; NSArray *stack = [detail componentsSeparatedByString:@"\n"]; //調用Bugly數據上報接口 [Bugly reportExceptionWithCategory:4 name:message reason:stack[0] callStack:stack extraInfo:@{} terminateApp:NO]; result(@0); } else { //方法未實現 result(FlutterMethodNotImplemented); } } @end
至此,在完成了 Bugly iOS SDK 的接口封裝以後,FlutterCrashPlugin 插件的 iOS 部分也就搞定了。
與 iOS 相似,咱們須要使用 Android Studio 打開 example 下的 android 工程進行插件開發工做。一樣,在打開 android 工程前,你須要確保整個工程代碼至少 build 過一次,不然 IDE 會報錯。如下是Bugly 異常上報 Android SDK 接入指南。
首先,咱們須要在插件工程下的 build.gradle 文件引入 Bugly SDK,即 crashreport 與 nativecrashreport,其中前者提供了 Java 和自定義異常的的數據上報能力,然後者則是 JNI 的異常上報封裝,以下所示。
dependencies { implementation 'com.tencent.bugly:crashreport:latest.release' implementation 'com.tencent.bugly:nativecrashreport:latest.release' }
而後,在原生接口 FlutterCrashPlugin 類中,依次初始化插件實例、綁定方法通道,並在方法通道中前後爲 setup 與 postException 提供 Bugly Android SDK 的實現版本,代碼以下。
public class FlutterCrashPlugin implements MethodCallHandler { //註冊器,一般爲MainActivity public final Registrar registrar; //註冊插件 public static void registerWith(Registrar registrar) { //註冊方法通道 final MethodChannel channel = new MethodChannel(registrar.messenger(), "flutter_crash_plugin"); //初始化插件實例,綁定方法通道,並註冊方法通道回調函數 channel.setMethodCallHandler(new FlutterCrashPlugin(registrar)); } private FlutterCrashPlugin(Registrar registrar) { this.registrar = registrar; } @Override public void onMethodCall(MethodCall call, Result result) { if(call.method.equals("setUp")) { //Bugly SDK初始化方法 String appID = call.argument("app_id"); CrashReport.initCrashReport(registrar.activity().getApplicationContext(), appID, true); result.success(0); } else if(call.method.equals("postException")) { //獲取Bugly數據上報所須要的各個參數信息 String message = call.argument("crash_message"); String detail = call.argument("crash_detail"); //調用Bugly數據上報接口 CrashReport.postException(4,"Flutter Exception",message,detail,null); result.success(0); } else { result.notImplemented(); } } }
在完成了 Bugly Android 接口的封裝以後,因爲 Android 系統的權限設置較細,考慮到 Bugly 還須要網絡、日誌讀取等權限,所以咱們還須要在插件工程的 AndroidManifest.xml 文件中,將這些權限信息顯示地聲明出來,以下所示。
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.hangchen.flutter_crash_plugin"> <!-- 電話狀態讀取權限 --> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <!-- 網絡權限 --> <uses-permission android:name="android.permission.INTERNET" /> <!-- 訪問網絡狀態權限 --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- 訪問wifi狀態權限 --> <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> <!-- 日誌讀取權限 --> <uses-permission android:name="android.permission.READ_LOGS" /> </manifest>
至此,在完成了極光 Android SDK 的接口封裝和權限配置以後,FlutterCrashPlugin 插件的 Android 部分也搞定了。FlutterCrashPlugin 插件爲 Flutter 應用提供了數據上報的封裝,不過要想 Flutter 工程可以真正地上報異常消息,咱們還須要爲 Flutter 工程關聯 Bugly 的應用配置。
在單獨爲 Android/iOS 應用進行數據上報配置以前,咱們首先須要去Bugly 的官方網站,爲應用註冊惟一標識符(即 AppKey)。這裏須要注意的是,在 Bugly 中,Android 應用與 iOS 應用被視爲不一樣的產品,因此咱們須要分別註冊。
在獲得了 AppKey 以後,咱們須要依次進行 Android 與 iOS 的配置工做。iOS 的配置工做相對簡單,整個配置過程徹底是應用與 Bugly SDK 的關聯工做,而這些關聯工做僅須要經過 Dart 層調用 setUp 接口,訪問原生代碼宿主所封裝的 Bugly API 就能夠完成,所以無需額外操做。
而 Android 的配置工做則相對繁瑣些。因爲涉及 NDK 和 Android P 網絡安全的適配,咱們還須要分別在 build.gradle 和 AndroidManifest.xml 文件進行相應的配置工做。首先,因爲 Bugly SDK 須要支持 NDK,所以咱們須要在 App 的 build.gradle 文件中爲其增長 NDK 的架構支持,以下所示。
defaultConfig { ndk { // 設置支持的SO庫架構 abiFilters 'armeabi' , 'x86', 'armeabi-v7a', 'x86_64', 'arm64-v8a' } }
而後,因爲 Android P 默認限制 http 明文傳輸數據,所以咱們須要爲 Bugly 聲明一項網絡安全配置 network_security_config.xml,容許其使用 http 傳輸數據,並在 AndroidManifest.xml 中新增同名網絡安全配置。
//res/xml/network_security_config.xml <?xml version="1.0" encoding="utf-8"?> <!-- 網絡安全配置 --> <network-security-config> <!-- 容許明文傳輸數據 --> <domain-config cleartextTrafficPermitted="true"> <!-- 將Bugly的域名加入白名單 --> <domain includeSubdomains="true">android.bugly.qq.com</domain> </domain-config> </network-security-config> //AndroidManifest/xml <application ... android:networkSecurityConfig="@xml/network_security_config" ...> </application>
至此,Flutter 工程所需的原生配置工做和接口實現,就所有搞定了。接下來,咱們就能夠在 Flutter 工程中的 main.dart 文件中,使用 FlutterCrashPlugin 插件來實現異常數據上報能力了。固然,咱們首先還須要在 pubspec.yaml 文件中,將工程對它的依賴顯示地聲明出來,以下所示。
dependencies: flutter_push_plugin: git: url: xxx
在下面的代碼中,咱們在 main 函數裏爲應用的異常提供了統一的回調,並在回調函數內使用 postException 方法將異常上報至 Bugly。而在 SDK 的初始化方法裏,因爲 Bugly 視 iOS 和 Android 爲兩個獨立的應用,所以咱們判斷了代碼的運行宿主,分別使用兩個不一樣的 App ID 對其進行了初始化工做。
此外,爲了與你演示具體的異常攔截功能,咱們還在兩個按鈕的點擊事件處理中分別拋出了同步和異步兩類異常,代碼以下:
//上報數據至Bugly Future<Null> _reportError(dynamic error, dynamic stackTrace) async { FlutterCrashPlugin.postException(error, stackTrace); } Future<Null> main() async { //註冊Flutter框架的異常回調 FlutterError.onError = (FlutterErrorDetails details) async { //轉發至Zone的錯誤回調 Zone.current.handleUncaughtError(details.exception, details.stack); }; //自定義錯誤提示頁面 ErrorWidget.builder = (FlutterErrorDetails flutterErrorDetails){ return Scaffold( body: Center( child: Text("Custom Error Widget"), ) ); }; //使用runZone方法將runApp的運行放置在Zone中,並提供統一的異常回調 runZoned<Future<Null>>(() async { runApp(MyApp()); }, onError: (error, stackTrace) async { await _reportError(error, stackTrace); }); } class MyApp extends StatefulWidget { @override State<StatefulWidget> createState() => _MyAppState(); } class _MyAppState extends State<MyApp> { @override void initState() { //因爲Bugly視iOS和Android爲兩個獨立的應用,所以須要使用不一樣的App ID進行初始化 if(Platform.isAndroid){ FlutterCrashPlugin.setUp('43eed8b173'); }else if(Platform.isIOS){ FlutterCrashPlugin.setUp('088aebe0d5'); } super.initState(); } @override Widget build(BuildContext context) { return MaterialApp( home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Crashy'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ RaisedButton( child: Text('Dart exception'), onPressed: () { //觸發同步異常 throw StateError('This is a Dart exception.'); }, ), RaisedButton( child: Text('async Dart exception'), onPressed: () { //觸發異步異常 Future.delayed(Duration(seconds: 1)) .then((e) => throw StateError('This is a Dart exception in Future.')); }, ) ], ), ), ); } }
運行上面的代碼,模擬異常上傳,而後咱們打開Bugly 開發者後臺,選擇對應的 App,切換到錯誤分析選項查看對應的面板信息。能夠看到,Bugly 已經成功接收到上報的異常上下文了,以下圖所示。
對於 Flutter 應用的異常捕獲,能夠分爲單個異常捕獲和多異常統一攔截兩種狀況。其中,單異常捕獲,使用 Dart 提供的同步異常 try-catch,以及異步異常 catchError 機制便可實現。而對多個異常的統一攔截,能夠細分爲以下兩種狀況:一是 App 異常,咱們能夠將代碼執行塊放置到 Zone 中,經過 onError 回調進行統一處理;二是 Framework 異常,咱們可使用 FlutterError.onError 回調進行攔截。
須要注意的是,Flutter 提供的異常攔截只能攔截 Dart 層的異常,而沒法攔截 Engine 層的異常。這是由於,Engine 層的實現大部分是 C++ 的代碼,一旦出現異常,整個程序就直接 Crash 掉了。不過一般來講,這類異常出現的機率極低,通常都是 Flutter 底層的 Bug,與咱們在應用層的實現沒太大關係,因此咱們也無需過分擔憂。
若是咱們想要追蹤 Engine 層的異常(好比給 Flutter 提 Issue),則須要藉助於原生系統提供的 Crash 監聽機制。不過,這方面的內容比較繁瑣,具體能夠參考:Flutter官方文檔