Flutter異常監測與上報

xiangzhihong發表於2020-04-06

Flutter異常

眾所周知,軟體專案的交付是一個複雜的過程,任何原因都有可能導致交付的失敗。很多時候經常遇到的一個現象是,應用在開發測試時沒有任何異常,但一旦上線就問題頻出。出現這些異常,可能是因為不充分的機型適配或者使用者糟糕的網路狀況造成的,也可能是Flutter框架自身缺陷造成的,甚至是作業系統底層的問題。

而處理此類異常的最佳方式是捕獲使用者的異常資訊,將異常現場儲存起來並上傳至伺服器,然後通過分析異常上下文來定位引起異常的原因,並最終解決此類問題。

所謂Flutter異常,指的是Flutter程式中Dart程式碼執行時發生的錯誤。與Java和OC等多執行緒模型的程式語言不同,Dart是一門單執行緒的程式語言,採用事件迴圈機制來執行任務,所以各個任務的執行狀態是互相獨立的。也即是說,當程式執行過程中出現異常時,並不需要像Java那樣使用try-catch機制來捕獲異常,因為即便某個任務出現了異常,Dart程式也不會退出,只會導致當前任務後續的程式碼不會被執行,而其它功能仍然可以繼續使用。

在Flutter開發中,根據異常來源的不同,可以將異常分為Framework異常和Dart異常。Flutter對這兩種異常提供了不同的捕獲方式,Framework異常是由Flutter框架引發的異常,通常是由於錯誤的應用程式碼造成Flutter框架底層的異常判斷引起的,當出現Framework異常時,Flutter會自動彈出一個的紅色錯誤介面。而對於Dart異常,則可以使用try-catch機制和catchError語句進行處理。

除此之外,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方案

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方案

目前,Bugly還沒有提供Flutter外掛,那麼,我們針對混合工程,可以採用下面的方案。接入Bugly時,只需要完成一些前置應用資訊關聯繫結和 SDK 初始化工作,就可以使用 Dart 層封裝好的資料上報介面去上報異常了。可以看到,對於一個應用而言,接入資料上報服務的過程,總體上可以分為兩個步驟:

  1. 初始化 Bugly SDK;
  2. 使用資料上報介面。

這兩步對應著在 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 介面實現

考慮到 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 部分也就搞定了。

Android 介面實現

與 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官方文件

相關文章