102

捕获 flutter app的崩溃日志并上报

 4 years ago
source link: https://www.tuicool.com/articles/UvQFzyn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

flutter 的崩溃日志收集主要有两个方面:

  1. flutter dart 代码的异常(包含app和framework代码两种情况,一般不会引起闪退,你猜为什么)
  2. flutter engine 的崩溃日志(一般会闪退)

Flutter App 代码异常捕获

人写的代码是无数异常交织起来的偶然产物,代码发生异常才是正常情况。

除了在关键的地方加上 try-catch 让它们变成已知异常之外,抓到未知异常才是真本事。

比如下面的一段代码中的 try-catch 是无效的:

try {
    Future.error("asynchronous surprise");
} catch (e){
    print(e)
}

好在,Dart 有一个 Zone 的概念,有点类似 sandbox 的意思。不同的 Zone 代码上下文是不同的互不影响,Zone 还可以创建新的子Zone。Zone 可以重新定义自己的 printtimersmicrotasks 还有最关键的 how uncaught errors are handled 未捕获异常的处理

runZoned(() {
    Future.error("asynchronous error");
}, onError: (dynamic e, StackTrace stack) {
    reportError(e, stack);
});

reportError 里即可以进行上报处理(详见后面介绍)。

Flutter framework 异常捕获

注册 FlutterError.onError 回调,用于收集 Flutter framework 外抛的异常。

FlutterError.onError = (FlutterErrorDetails details) {
    reportError(details.exception, details.stack);
};

该 error 一般是由 Widgetbuild 的时候抛出,如下:

@override
void performRebuild() {
  Widget built;
  try {
    built = build();
  } catch (e, stack) {
    built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
  } finally {
    _dirty = false;
  }
  try {
    _child = updateChild(_child, built, slot);
  } catch (e, stack) {
    built = ErrorWidget.builder(_debugReportException(ErrorDescription('building $this'), e, stack));
    _child = updateChild(null, built, slot);
  }
}

该代码有删减,具体请参看 framework.dart源码 。其中的 _debugReportException 就是 FlutterError 的调用点:

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;
}

Flutter engine 异常捕获

flutter engine 部分的异常,以Android 为例,主要为 libfutter.so 发生的错误。

这部份可以直接交给native崩溃收集sdk来处理,比如 firebase crashlyticsbuglyxCrash 等等

reportError 堆栈上报

线上app 出现异常虽然捕获了,但只是打印出来是没办法解决问题,还需要把他上报到开发者能看到的地方。

上面提到了许多崩溃收集的sdk,以 bugly 为例,它支持自定义异常上报,我们只需将 dart 异常及堆栈通过 MethodChannel 传递给 bugly sdk 即可。

Dart:

var channel = new MethodChannel("crash_handler");

Future<void> reportError(dynamic exception, StackTrace stack) async {
  try {
    return channel.invokeMethod("report_error", <String, dynamic>{
      "type": exception.runtimeType.toString(), // exception type
      "message": exception.toString(),          // message
      "stack": stack.toString(),                // stacktrace
    });
  } catch(ignored) {
  }
}

Android Java代码如下:

public class CrashHandler implements MethodCallHandler {
  @Override
  public void onMethodCall(MethodCall call, Result result) {
    switch (call.method) {
      case "report_error":
        postFlutterExcetion(call.argument("type"), call.argument("message"), call.argument("stack"))
        result.success(true);
        break;
      default:
        result.notImplemented();
    }
  }
  
  static void postFlutterExcetion(String excpetionType, String excpetionMessage, String stack) {
    if (!CrashModule.hasInitialized()) return;
    CrashReport.postException(4, excpetionType, excpetionMessage, stack, null);
  }
}

// ...

MethodChannel(flutterView, "crash_handler").setMethodCallHandler(new CrashHandler())

iOS 类似的注册一个MethodChannel并在 handleMethodCall 中调用 Bugly 的 reportExceptionWithCategory 即可。

其它的sdk类似处理,此处略过不表。

堆栈还原

收集到异常之后,需要查符合表还原堆栈。下面以Android 端 bugly 收集到堆栈为一例:

1 #00 pc 00016998 /system/lib/libc.so (__memcpy_base+104) [armeabi-v7a::2b2dac1c583b68da2f7c58e7ed352851]
2 #01 pc 00158aed /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
3 #02 pc 00138041 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
4 #03 pc 00139461 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
5 #04 pc 00013057 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+530) [armeabi-v7a::ac423f49f579c99cfababb65014363e9]
6 #05 pc 00013127 /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130) [armeabi-v7a::ac423f49f579c99cfababb65014363e9]
7 #06 pc 00007889 /system/lib/libandroid.so (ALooper_pollOnce+64) [armeabi-v7a::954c216fdf1faa9aa08f41bc27503a87]
8 #07 pc 001394fb /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
9 #08 pc 001371eb /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
10 #09 pc 00138441 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
11 #10 pc 0004185b /system/lib/libc.so (_ZL15__pthread_startPv+30) [armeabi-v7a::2b2dac1c583b68da2f7c58e7ed352851]
12 #11 pc 000192a5 /system/lib/libc.so (__start_thread+6) [armeabi-v7a::2b2dac1c583b68da2f7c58e7ed352851]
13 java:
14 [Failed to get Java stack]

首先需要确认该 flutter engine 所属版本号,在命令行执行:

flutter --version

输出如下:

Flutter 1.5.4-hotfix.2 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 7a4c33425d (9 weeks ago) • 2019-04-29 11:05:24 -0700
Engine • revision 52c7a1e849
Tools • Dart 2.3.0 (build 2.3.0-dev.0.5 a1668566e5)

可以看到 Engine 的 revision 为 52c7a1e849

其次,在 flutter infra 上找到对应cpu abi 的 symbols.zip 并下载 : https://console.cloud.google.com/storage/browser/flutter_infra/flutter/52c7a1e849a170be4b2b2fe34142ca2c0a6fea1f/android-arm-release

解压后,可以得到带有符合信息的 debug so 文件—— libflutter.so,并将其放到文件夹 armeabi-v7a 下。如需要x86等的符号信息,类似操作。

mkdir -p ~/Downloads/flutter-52c7a1e849/armeabi-v7a
unzip symbols.zip -d ~/Downloads/flutter-52c7a1e849/armeabi-v7a

使用 ndk-stack 手动还原堆栈:

  1. 将原始堆栈保存到 stack.txt 中,注意第一行要以 *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 开头

    *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
    1 #00 pc 00016998 /system/lib/libc.so (__memcpy_base+104) [armeabi-v7a::2b2dac1c583b68da2f7c58e7ed352851]
    2 #01 pc 00158aed /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
    3 #02 pc 00138041 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
    4 #03 pc 00139461 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
    5 #04 pc 00013057 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+530) [armeabi-v7a::ac423f49f579c99cfababb65014363e9]
    ...
    
  2. 执行ndk-stack命令

    $ANDROID_NDK/ndk-stack -sym ~/Downloads/flutter-52c7a1e849/armeabi-v7a -dump stack.txt > re-stack.txt
    

打开文件 re-stack.txt 即可看到还原后的堆栈:

********** Crash dump: **********
#00 0x00016998 /system/lib/libc.so (__memcpy_base+104) [armeabi-v7a::2b2dac1c583b68da2f7c58e7ed352851]
#01 0x00158aed /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
fml::WriteAtomically(fml::UniqueObject<int, fml::internal::os_unix::UniqueFDTraits> const&, char const*, fml::Mapping const&)
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/platform/posix/file_posix.cc:203:3
flutter::PersistentCacheStore(fml::RefPtr<fml::TaskRunner>, std::__1::shared_ptr<fml::UniqueObject<int, fml::internal::os_unix::UniqueFDTraits> >, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::unique_ptr<fml::Mapping, std::__1::default_delete<fml::Mapping> >)::$_0::operator()()
/b/s/w/ir/k/src/out/android_release/../../flutter/shell/common/persistent_cache.cc:114:0
auto fml::internal::CopyableLambda<flutter::PersistentCacheStore(fml::RefPtr<fml::TaskRunner>, std::__1::shared_ptr<fml::UniqueObject<int, fml::internal::os_unix::UniqueFDTraits> >, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::unique_ptr<fml::Mapping, std::__1::default_delete<fml::Mapping> >)::$_0>::operator()<>() const
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/make_copyable.h:24:0
#02 0x00138041 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
fml::MessageLoopImpl::FlushTasks(fml::MessageLoopImpl::FlushType)
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/message_loop_impl.cc:140:5
fml::MessageLoopImpl::RunExpiredTasksNow()
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/message_loop_impl.cc:148:0
#03 0x00139461 /data/app/com.netease.cartoonreader-1/lib/arm/libflutter.so [armeabi-v7a::2c9d8634bdb07ea641970181b0b00b84]
fml::MessageLoopAndroid::OnEventFired()
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/platform/android/message_loop_android.cc:92:5
fml::MessageLoopAndroid::MessageLoopAndroid()::$_0::operator()(int, int, void*) const
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/platform/android/message_loop_android.cc:42:0
fml::MessageLoopAndroid::MessageLoopAndroid()::$_0::__invoke(int, int, void*)
/b/s/w/ir/k/src/out/android_release/../../flutter/fml/platform/android/message_loop_android.cc:40:0
#04 0x00013057 /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+530) [armeabi-v7a::ac423f49f579c99cfababb65014363e9]

...

上传符号表 bugly 自动还原堆栈

一个个崩溃堆栈手动还原多麻烦

(噫 你怎么这么多崩溃) ,下载好 debug so 之后你也可以使用 bugly给的符号表上传工具上传到bugly上,文档见: https://bugly.qq.com/docs/user-guide/symbol-configuration-android/?v=20181014122344#_4

输出符号表:

java -jar buglySymbolAndroid.jar -i ~/Downloads/flutter-52c7a1e849/armeabi-v7a

将符号表上传之后,bugly 会自动将堆栈还原,如下图所示

2a2aMve.png!web

本文完,如你更好的方案请不要犹豫快留言告诉我。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK