前言
错误监控是维护 App 稳定的重要手段,通过对线上问题的实时监控,来观察 App 是否出现异常状况,以便快速解决问题以及制定修复方案.对于集成了 Flutter 的 App,除了需要提供 crash 崩溃监控,还需要对 Flutter 异常进行监控.
一般来说,监控系统都会包含问题的实时收集上报、问题的归类识别(聚合)以及自动分配、问题定位、实时报警等几个模块.要做一套 Flutter 异常监控也不例外,图中是贝壳在 Flutter 异常监控的整套方案.首先在端上收集异常并进行初步处理,持续集成平台会处理各平台的 app 符号信息并管理 app 相关的基础信息,在监控后台,系统主要处理异常日志数据源,并经过预处理、解析、构建多纬度统计数据、最终展示到前端平台、并会根据一些阈值配置进行异常报警.
本文主要围绕其中移动端 Flutter 异常处理、监控后台异常预处理、监控后台异常的解析处理三部分来介绍贝壳在 Flutter 异常监控的实践与沉淀.
一、移动端 Flutter 异常处理
在介绍 Flutter 异常处理前,我们先了解下 Flutter 异常.
1.1. Flutter 异常
Flutter 异常是指程序中 Dart 代码运行时抛出的错误事件.一般来说,异常种类主要分为 Exception 和 Error,以及它们的子类型.当然开发者也可以自定义非 null 的错误类型.Dart 支持程序抛出非空类型的各种错误,如下代码所示:
void main(){ // 可以抛出任意非空的异常 throw "自定义错误"; throw Error();}
复制
对于 Flutter 应用来说,当程序出现异常时,通常情况程序不会崩溃退出,这点不同于 java 或者 Objective-C 这种编程语言.拿 Android Java 应用举例,当异常发生并且没有被捕获,那么默认的
uncaughtException
方法就会捕获到异常并且执行
System.exit()
杀掉程序,或者异常触发系统底层的异常进程也会直接被杀掉.
但是 Flutter 的处理方式则不一样,异常即使没有被我们主动捕获,系统的默认处理方式也只是 print,或者替换错误 widget,通常在 App 上表现为页面白屏(红屏)、用户操作不响应等,这也是为什么我们在崩溃监控之外需要通过额外的监控平台能力去处理 Flutter 异常.
在 Flutter 运行过程中,采用了事件循环的机制来运行任务(
https://dart.cn/articles/archive/event-loop)
,如下图所示,其中有两个不同优先级的队列,每当有事件任务触发,都会被放到其中一个队列中,其中运行的各个任务是互相独立的.当某个任务出现异常,会导致任务的后续代码不会继续执行,但不会影响其他任务的执行.
1.2. Flutter 异常捕获
和 java 类似,Flutter 也可以通过 try-catch 机制捕获,但是 try-catch 只能捕获同步代码块的异常,对于 future 异步代码块抛出的错误,需要采用 future 提供的 catchError 语句捕获,如下代码:
void main() {
// 使用try-catch捕获同步代码块异常
try {
throw AssertionError('throw AssertionError');
} catch (e) {
print(e);
// 使用catchError捕获异步代码块异常
Future.delayed(Duration(microseconds: 0))
.then((e) => throw AssertionError('throw AssertionError'))
.catchError((e) => print(e));
// 异步代码块通过try-catch捕获不到,下面catch逻辑不会执行
try {
Future.delayed(Duration(microseconds: 0))
.then((e) => throw AssertionError('throw AssertionError'));
} catch (e) {
print("不会执行");
复制
知道如何捕获错误后,只需再找到合适的地方去捕获 Flutter 错误,下文分为 3 个部分去介绍异常捕获.
1.2.1. Flutter 框架异常捕获
Flutter 框架本身已经捕获了许多 dart 抛出的异常,包括构建期间、布局期间和绘制期间的异常.它通过
FlutterError.reportError
统一处理,如下面代码:
// 框架先通过try-catch捕获错误,然后发送到reportError去统一处理
void performRebuild() {
try {
} catch (e, stack) {
built = ErrorWidget.builder(
// 方法对中调到reportError
_debugReportException(
ErrorDescription('building $this'),
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
static void reportError(FlutterErrorDetails details) {
if (onError != null)
onError(details);
// 系统提供的默认实现方式,输出到控制台,重写方法可以实现自己的处理逻辑.
static FlutterExceptionHandler onError = dumpErrorToConsole;
复制
我们可以
main
方法中重写
onError
方法去实现我们自己的逻辑,如下代码所示:
void main() {
// 重写onError方法,实现自定义逻辑
FlutterError.onError = (details) {
print(details);
runApp(MyApp());
复制
1.2.2. 其它 dart 异常捕获
对于其它未被 Flutter 框架捕获的 Dart 异常,比如 Future 中的异常等,会被错误发生所在
Zone
捕获,
Zone
表示一个代码执行的上下文,给异步代码和同步代码提供了一个稳定的运行环境,可以简单理解为一个沙盒,其对于内部发生且未被主动捕获的异常的默认处理方式也是打印输出错误.初始
main
函数就在默认区域 (
Zone.root
)的上下文中运行,我们可以通过将
runApp()
包裹到自定义的
Zone
里,重写捕获异常的方法
onError
,如下代码所示:
void main(){
runZoned(() {
runApp(MyApp());
}, onError: (error, stackTrace) {
// 自定义处理错误
print(error);
复制
1.2.3. 白屏(红屏)异常捕获
上文说到,Flutter 框架会捕获到一部分的 dart 异常,除了统一的回调处理,还对一部分导致页面白屏问题的异常,进行了替换错误 widget 的处理,
void performRebuild() {
try {
} catch (e, stack) {
// ErrorWidget.builder回调方法替换错误页面
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
//默认的处理方式,我们也可以在main中覆盖处理
static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;
复制
如上面代码所示,Flutter 框架通过
ErrorWidgetBuilder builder
对页面渲染失败异常进行统一替换 widget 的处理,我们通过对其覆盖重写,就能在众多的异常中捕获到页面渲染的异常,方便后面对异常进行分级分类处理.
注意,官方逻辑中,回调替换 widget 的地方也同样上报到了
reportError
,我们可以通过 aop 的方式将逻辑替换,否则对上报错误数量有一定影响.
到这里,捕获 Flutter 异常已经完成,最终使用了三个 hook 点去上报异常,为后续的后端服务解析处理做好了源数据准备.当然,光在这些地方收集异常还是不够的,还需要一些异常封装处理,来补充异常运行的状态信息.
1.3. Flutter 异常封装处理
异常信息的封装主要分为两个步骤:异常信息的提取处理、添加附加信息.
1.3.1. Flutter 异常提取处理
首先是异常种类的提取,一般通过
runtimeType
就能获取到异常类型;但是要注意的是,之前 hook 上报的地方,有些异常被封装成
FlutterErrorDetails
,所以需要对其 exception 进行判断.
const FlutterErrorDetails({
this.exception, //真实的异常
this.stack,
this.library = 'Flutter framework',
this.context,
this.stackFilter,
this.informationCollector,
this.silent = false,
复制
再者就是对异常的概述提取,我们通过使用 Flutter 框架中的一个函数
exceptionAsString
来获取,如下面代码:
String exceptionAsString() {
String? longMessage;
if (exception is AssertionError) {
final Object? message = exception.message;
final String fullMessage = exception.toString();
if (message is String && message != fullMessage) {
if (fullMessage.length > message.length) {
final int position = fullMessage.lastIndexOf(message);
if (position == fullMessage.length - message.length &&
position > 2 &&
fullMessage.substring(position - 2, position) == ': ') {
// Add a linebreak so that the filename at the start of the
// assertion message is always on its own line.
String body = fullMessage.substring(0, position - 2);
final int splitPoint = body.indexOf(' Failed assertion:');
if (splitPoint >= 0) {
body = '${body.substring(0, splitPoint)}\n${body.substring(splitPoint + 1)}';
longMessage = '${message.trimRight()}\n$body';
longMessage ??= fullMessage;
} else if (exception is String) {
longMessage = exception as String;
} else if (exception is Error || exception is Exception) {
longMessage = exception.toString();
} else {
longMessage = ' ${exception.toString()}';
longMessage = longMessage.trimRight();
if (longMessage.isEmpty)
longMessage = ' <no message available>';
return longMessage;
复制
还有就是堆栈的上报,异常上报的地方都会有 stack 信息,对于 Flutter 框架封装的
FlutterErrorDetails
,提取
stack
即可.处理完异常信息,我们需要给异常信息添加一些额外的运行通用信息,来帮助解决异常.
1.3.2. Flutter 异常附加信息
为了帮助 Flutter 异常的高效解决,我们在异常的上报中添加了一些附加信息,包括异常发生时的设备信息、页面信息、内存信息、路径埋点唯一检索信息.其中,页面信息的获取方式可以在我们的另一篇文章中找到(附地址).这些信息可以帮助我们查看异常的走势和修复状况,如下图:
上报的一些系统现状和运行信息,可以辅助开发同学定位问题:
二、 后台 Flutter 异常预处理
当监控后台收到移动端上报的异常日志,首先要做的就是将收到的异常日志进行预处理,其中最主要的两个模块就是异常的分级分类和异常堆栈的符号化解析.
2.1. Flutter 异常的分级分类
通过上面,我们知道 Flutter 异常并不会导致崩溃,那么 Flutter 异常一定会影响用户么?这里要从 Flutter 异常和 crash 崩溃不同的地方说起.通常,crash 发生时,一定代表我们的用户受到了影响,但 Flutter 异常却不一定.
在所有的 Flutter 异常中,有一部分异常用户并无感知.它可能是初期开发同学的代码不够规范导致无效调用引起,也可能是 build 的多次刷新报错;还有一部分网络异常导致的偶现错误,比如图片错误,这种问题端上同学也不能处理(也有其他的监控服务处理了,比如网络报警服务).在这种情况下,如果我们把所有的错误一股脑放到开发同学面前,不分轻重缓急,他们是没法高效的分优先级去处理.开发同学的精力毕竟有限,我们应该集中精力去处理那些能处理以及对用户真实发生影响的问题.
所以针对 Flutter 异常,我们将其分为 3 大类:
也是在经历了第一阶段开发同学对 Flutter 异常的处理不够积极的情况,我们优化了监控平台的能力,对 Flutter 的异常进行分级分类的处理.
一是区分上报信息,也就是上文提到的上报页面渲染失败异常,包括白屏(红屏)问题,二是后端服务对错误类型进一步分类处理.
首先是渲染失败导致的红屏和白屏是我们的一级问题,对于 CastError、RangeError、PlatformException、NoSuchMethodError、MissingPluginException 等多种错误类型,我们认为其是影响业务的,也将其列为一级问题.其它的,比如图片异常或者网络异常,我们将其放到二级问题.其中比较特殊的,比如 NoSuchMethodError,我们对其中的部分异常进行正则过滤,也放到二级错误中.
其中一级错误和二级错误分别展示到不同的地方以及提供后续不同的报警等处理方式,保证快速聚焦的核心问题上.
2.2. Flutter 的符号化解析
在 Flutter1.17 以上的版本中,官方支持了对 Flutter 产物去除符号表的功能,考虑到集成 Flutter 产物的 app 的安全性和包大小问题,我们在打包系统中集成了这个功能,通过官方支持的打包命令就可以在打包期间分离符号表文件.
—split-debug-info 可以分离出 debug info 符号表信息
但是这也导致 Flutter 异常上报的堆栈是去符号化的,难以阅读理解,如下所示:
"Warning: This VM has been configured to produce stack traces that violate the Dart standard.
*** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
pid: 16540, tid: 6125465600, name beikeshareengine_0.1.ui
isolate_dso_base: 10b860000, vm_dso_base: 10b860000
isolate_instructions: 10b86a000, vm_instructions: 10b866000
#00 abs 000000010bc7d08b _kDartIsolateSnapshotInstructions+0x41308b
#01 abs 000000010bb0f037 _kDartIsolateSnapshotInstructions+0x2a5037
#02 abs 000000010bcc72e7 _kDartIsolateSnapshotInstructions+0x45d2e7
复制
在这种情况下,我们需要一个符号化解析系统处理堆栈,将其转化为可理解的堆栈信息.当然剥离符号表信息不仅仅影响了 Flutter 异常的堆栈,对 native crash 中的相关 Flutter 堆栈行也会有影响,所以下文会对这两块分别阐述.
符号表解析首先要做的就是对编译打包过程中符号表文件的处理.
2.2.1. Flutter 符号表文件处理
上文说到,在编译过程中通过命令将符号文件生成并剥离出来并保存到指定目录,文件类似这样
app.ios-arm64.symbols
.首先我们会先将文件上传到 artifactory 仓库中,并在打包过程中分析出 app 产物的其它相关信息,比如版本、hash 等等,之后将这些信息发送监控平台进行分析处理,以供之后的符号化解析使用.
完整架构图如下:
有了符号表文件之后,剩下的就是对堆栈进行解析处理,首先是 Flutter 异常的符号化解析.
2.2.2. Flutter 异常符号化解析
首先需要了解
app.ios-arm64.symbols
这个从 Flutter 产物中剔除出来的符号文件,它存储了 Dart VM AOT 编译器将源代码映射为信息编码的所有信息,是采用了 DWARF 格式的高度压缩文件.这里拿 ios 举例,Android 同理,通过 file 命令可知,其是一个 ELF 文件:
➜ file app.ios-arm64.symbols
➜ app.ios-arm64.symbols : ELF 64-bit LSB shared object, ARM aarch64, version 1 (SYSV), dynamically linked, BuildID[md5/uuid]=XXXXX, with debug_info, not stripped
ELF (Executable and Linkable Format)是一种为可执行文件,目标文件,共享链接库和内核转储(core dumps)准备的标准文件格式.通过下面命令可以生成两个符号相关文件:
➜ dwarfdump app.ios-arm64.symbols --debug-info > info.txt
➜ dwarfdump app.ios-arm64.symbols --debug-line > line.txt
其中 info 文件中存储的是源码信息,line 文件中存储的是行号相关信息.info 文件中我们拿其中一个函数信息举例:
0x0010f67f: TAG_subprogram [3] *
AT_abstract_origin( {0x0003acac}"MaterialLocalizationDa.datePickerHelpText" )
AT_low_pc( 0x00000000001a0118 )
AT_high_pc( 0x00000000001a0134 )
复制
其中:
TAG_subprogram
是指代函数的意思,
AT_abstract_origin
是其源码信息,
AT_low_pc
和
AT_high_pc
是这个函数相对于符号表文件高与低的偏移量,下文简称为
pc_offset
在下面这样一行堆栈中,
#02 abs 000000010bcc72e7 _kDartIsolateSnapshotInstructions+0x45d2e7
复制
_kDartIsolateSnapshotInstructions
代表的是这行堆栈的错误信息是在
isolate_instructions
指令段中,后面跟的偏移量就是相对
isolate_instructions
起始的偏移量,下文简称为
isolate_offset
,它在一个符号表中是固定的.
我们只要通过
isolate_offset
找到
pc_offset
,继而就能找到源码信息.他们的关系也很明显,通过
nm
命令找到
isolate_instructions
相对符号表文件的偏移量
isolate_start
,然后通过相加的方式得到
pc_offset
➜ nm app.ios-arm64.symbols | grep_kDartIsolateSnapshotInstructions
➜ _kDartIsolateSnapshotInstructions b 0x6000
其中 0x6000 就是
isolate_start
isolate_start + isolate_offset = pc_offset
最终我们只要在 info 文件中找到
pc_offset
在哪个源码信息的
AT_low_pc
和
AT_high_pc
之间,就能找到源码信息.
同样的,拿到这些信息后在 line 文件中我们通过偏移地址的映射关系也能找到对应的行号信息,这里我们就不做阐述.
那么这一套解析逻辑我们如何实现呢,官方既然提供了剔出符号化的逻辑,当然也会有符号化解析的逻辑.通过对 flutter_tools 源码的阅读可知,官方同样提供了一个
SymbolizeCommand
的命令用于符号化解析,其通过获取符号文件与堆栈输入,最终通过
native_stack_traces
库中的
DwarfStackTraceDecoder
解析处理,如下代码所示:
@override
Future<FlutterCommandResult> runCommand() async {
Stream<List<int>> input;
IOSink output;
// 分析参数获取符号文件地址与堆栈
if (argResults.wasParsed('output')) {
final File outputFile = _fileSystem.file(stringArg('output'));
if (!outputFile.parent.existsSync()) {
outputFile.parent.createSync(recursive: true);
output = outputFile.openWrite();
} else {
final StreamController<List<int>> outputController = StreamController<List<int>>();
outputController
.stream
.transform(utf8.decoder)
.listen(_stdio.stdoutWrite);
output = IOSink(outputController);
if (argResults.wasParsed('input')) {
input = _fileSystem.file(stringArg('input')).openRead();
} else {
input = _stdio.stdin;
final Uint8List symbols = _fileSystem.file(stringArg('debug-info')).readAsBytesSync();
// 解析处理
await _dwarfSymbolizationService.decode(
input: input,
output: output,
symbols: symbols,
return FlutterCommandResult.success();
复制
其中对
DwarfStackTraceDecoder
逻辑的阅读也能验证上文逻辑.其中计算
pc_offset
偏移量的方式,官方还提供了其它几种计算方式:
PCOffset _retrievePCOffset(StackTraceHeader header, RegExpMatch match) {
if (match == null) return null;
final restString = match.namedGroup('rest');
// 第一种,通过isolate_offset/vm_offset计算,也就是上文提到的
if (restString.isNotEmpty) {
final offset = tryParseSymbolOffset(restString);
if (offset != null) return offset;
// 第二种,通过isolate_instructions和运行绝对地址计算
if (header != null) {
final addressString = match.namedGroup('absolute');
final address = int.tryParse(addressString, radix: 16);
return header.offsetOf(address);
// 第三种通过虚拟就地址计算,一般用不到
final virtualString = match.namedGroup('virtual');
if (virtualString != null) {
final address = int.tryParse(virtualString, radix: 16);
return PCOffset(address, InstructionsSection.none);
return null;
//对应上文提到的三种方式,分别和isolate_start/vm_start相加计算出pc_offset
int virtualAddressOf(PCOffset pcOffset) {
switch (pcOffset.section) {
case InstructionsSection.none:
// This address is already virtualized, so we don't need to change it.
return pcOffset.offset;
case InstructionsSection.vm:
return pcOffset.offset + vmStartAddress;
case InstructionsSection.isolate:
return pcOffset.offset + isolateStartAddress;
default:
throw "Unexpected value for instructions section";
复制
其中第二种利用堆栈 header 信息中的
isolate_instructions: 10b86a000, vm_instructions: 10b866000
这两个运行时偏移地址和
abs 000000010bc7d08b
相减也能得出
isolate_offset
,之后通过第一种的逻辑最终得到
pc_offset
.
除此之外,官方提供的
SymbolizeCommand
中的
runCommand
仅支持的文件堆栈输入输入,并且后端服务不可能直接依赖整个 dart 的执行环境,所以我们将
runCommand
中的逻辑拆分,并扩展可支持堆栈类型,如下代码:
if (options.wasParsed('input')) {
input = _fileSystem.file(stringArg('input')).openRead();
} else if (options.wasParsed('input-string')) {
//支持string输入的堆栈
String formatString = options['input-string'];
input = Stream.value(value.codeUnits);
} else {
input = _stdio.stdin;
复制
最终通过以下命令打包成一个 linux\macos 可执行脚本
flutter symbolize
,提供给后端服务用于解析堆栈信息.
➜ dart compile exe bin/symbolize.dart -o outputs/linux_x64_dart_2.13.3/symbolize
复制
除了 Flutter 异常的符号化解析,去除符号表也会影响到 Flutter 引起的 crash 中的堆栈解析,下面我们介绍解析过程.
2.2.3. Flutter crash 符号化解析
因为涉及到 crash 堆栈,Android 和 iOS 的堆栈与解析方式就有些差别了,下文我们分别描述 iOS 和 Android 中的 Flutter 堆栈的解析处理.
Flutter 打包后的 iOS 产物是 Framework,其中有 App.Framework 和 Flutter.Framework.其中 App.Framework 里是 Flutter 侧 dart 的相关代码,也是需要利用上文提到的符号化文件进行处理,而 Flutter.Framework 的符号化解析则利用 iOS 的 crash 解析方式处理,这里我们就不做叙述.
对于 iOS crash,其中 App.Framework 产物中引发的崩溃会包含类似下面的堆栈:
动态库名称 函数运行时地址 App.framework运行时基地址 相对App.framework偏移量
5 App 0x0000000104609950 0x104488000 + 1579344
36 App 0x00000001044911e4 0x104488000 + 37348
复制
我们需要做的就是将其转化为
flutter symbolize
脚本能够识别的堆栈,也就是上文提到的这种:
堆栈编号 函数运行时绝对地址 dart Isolate代码段 isolate_offset
#00 abs 000000010455b93f _kDartIsolateSnapshotInstructions+0xc793f
复制
也就是说我们要通过相对 App.Framework 的偏移量得到
isolate_offset
,按照上文一样的思路去处理.首先需要计算 App.Framework 中 isolate 和 vm 指令段相对 App.Framework 的偏移地址,通过这两个地址和相对 App.framework 的偏移量相减就能得到相对 isolate 和 vm 的偏移地址,也就是
isolate_offset
和
vm_offset
.那么如何得到 isolate 和 vm 指令段相对 App.Framework 的偏移地址呢,通过
nm
命令也能拿到
➜ nm App.Framework | grep _kDartIsolateSnapshotInstructions
➜ 0000000000008000 T _kDartIsolateSnapshotInstructions
➜ nm App.Framework | grep _kDartVmSnapshotInstructions
➜ 0000000000004000 T _kDartVmSnapshotInstructions
其中 0000000000008000 就是 isolate 指令段相对 App.Framework 的偏移量.
因为这个命令的执行逻辑是对 App.Framework 进行分析,所以它实际上也是在上文提到的持续集成打包时的符号化处理过程中,通过上面的命令分析得到,然后保存到异常监控平台.
具体解析实现步骤如下图:
1、相对 App.framework 偏移量减去持续集成 nm 命令得到的偏移量,得出 isolate_offset 或者 vm_offset;
2、然后利用上一步的结构拼接成
flutter symbolize
能够识别的堆栈.
3、对每行堆栈重复执行 1、2 步,然后使用
flutter symbolize
脚本解析出来.
Android 和 iOS 的解析同理,我们也只要处理其中包含 dart 代码的 libapp.so 相关的堆栈.对于 Android crash,其中包含的 libapp.so 相关堆栈如下:
#03 pc 00004828 /data/app/XXX/lib/arm/libapp.so (offset 0x200) (_kDartVmSnapshotInstructions+ 10280)
复制
它的处理方式就很简单,因为
isolate_offset
和
vm_offset
直接有了,所以我们只要拼接成转化为
flutter symbolize
脚本能够识别的堆栈转化为
flutter symbolize
脚本能够识别的堆栈,就可以处理.
到这里,我们已经将 Flutter 堆栈解析成可理解的堆栈信息了,下一步就是利用 Flutter 异常上报的信息,对 Flutter 异常进行解析处理.
三、 后台 Flutter 异常解析
这一部分主要的内容是对异常的聚合和分配,以及统计计算.
3.1.聚合
聚合异常是监控平台一个非常重要的能力,能够帮我们统计某个异常的实时影响情况,根据阈值预警、提前作出反应.
比如说一个异常短时间发生次数超过阈值,那么我们通过报警的方式通知给负责人,然后作出停止灰度,替换线上包或者热修等决策.
聚合采用分行解析堆栈信息的方式,找到错误发生最接近业务(非 Flutter 框架代码)或者最能体现错误的那一行栈帧.
通过以下正则能够分析出每行的类名、函数名、包名、文件名
"^#(\d+) +(.+) \((.+?):?(\d+)?:?(\d+)?\)$"
得出下面这个数据结构对象
public class StackFrame extends Symbol {
public String structureName;//包名
public String className;//类名
public String methodName;//函数名
public Component component;//组件
public String file = "<Unknown>";//文件
public int line = -1;//行号
public String content;//栈帧原始信息 or pageName
public boolean asynchronous = false;//是否异步帧
复制
之后便可以根据
structureName
和 App 构建集成平台的信息进行匹配,如果匹配成功,就把这行栈帧作为聚合的信息.
注意: 聚合信息中不要包含行号信息,因为可能发生异常被修改但并未修复的情况,去掉行号信息可以在这种情况下,让错误还是聚合成一种.
3.1.1.特殊处理
通过业务栈帧来聚合处理异常在一些情况下可能不生效.通过对大量 Flutter 异常堆栈的分析,我们发现,因为 future 异步调用的问题,有许多的堆栈中并没有业务栈帧,并且会把异常聚合到无效栈帧.
比如下面这种 PlatformException 错误信息,如果按照业务栈帧优先的逻辑,对于这种没有业务栈帧的堆栈,就会把第 1 行作为聚合信息,这样这会导致大量的系统相关的
MethodChannel
错误都聚合成一种错误,对我们的问题解决以及阈值报警都有很大的干扰.
#0 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319)
#1 <asynchronous suspension>
#2 PlatformViewsService.initUiKitView (package:flutter/src/services/platform_views.dart:168)
#3 _UiKitViewState._createNewUiKitView (package:flutter/src/widgets/platform_view.dart:621)
#4 _UiKitViewState._initializeOnce (package:flutter/src/widgets/platform_view.dart:571)
#5 _UiKitViewState.didChangeDependencies (package:flutter/src/widgets/platform_view.dart:581)
#6 StatefulElement._firstBuild (package:flutter/src/widgets/framework.dart:4376)
#167 _rootRun (dart:async/zone.dart:1126)
#168 _CustomZone.run (dart:async/zone.dart:1023)