Flutter 异常监控、符号解析及聚合分流实践
前言
错误监控是维护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应用举例,当异常发生并且没有被捕获,那么默认的方法就会捕获到异常并且执行杀掉程序,或者异常触发系统底层的异常进程也会直接被杀掉.
但是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抛出的异常,包括构建期间、布局期间和绘制期间的异常.它通过 统一处理,如下面代码:
// 框架先通过try-catch捕获错误,然后发送到reportError去统一处理
void performRebuild() {
...
try {
...
} catch (e, stack) {
built = ErrorWidget.builder(
// 方法对中调到reportError
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
...
}
static void reportError(FlutterErrorDetails details) {
...
if (onError != null)
onError(details);
}
// 系统提供的默认实现方式,输出到控制台,重写方法可以实现自己的处理逻辑.
static FlutterExceptionHandler onError = dumpErrorToConsole;
我们可以方法中重写方法去实现我们自己的逻辑,如下代码所示:
void main() {
// 重写onError方法,实现自定义逻辑
FlutterError.onError = (details) {
print(details);
};
runApp(MyApp());
}
1.2.2. 其它dart异常捕获
对于其它未被Flutter框架捕获的Dart异常,比如Future中的异常等,会被错误发生所在捕获,表示一个代码执行的上下文,给异步代码和同步代码提供了一个稳定的运行环境,可以简单理解为一个沙盒,其对于内部发生且未被主动捕获的异常的默认处理方式也是打印输出错误.初始函数就在默认区域 ( )的上下文中运行,我们可以通过将包裹到自定义的里,重写捕获异常的方法,如下代码所示:
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'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
...
}
//默认的处理方式,我们也可以在main中覆盖处理
static ErrorWidgetBuilder builder = _defaultErrorWidgetBuilder;
如上面代码所示,Flutter框架通过对页面渲染失败异常进行统一替换widget的处理,我们通过对其覆盖重写,就能在众多的异常中捕获到页面渲染的异常,方便后面对异常进行分级分类处理.
注意,官方逻辑中,回调替换widget的地方也同样上报到了,我们可以通过aop的方式将逻辑替换,否则对上报错误数量有一定影响.
到这里,捕获Flutter异常已经完成,最终使用了三个hook点去上报异常,为后续的后端服务解析处理做好了源数据准备.当然,光在这些地方收集异常还是不够的,还需要一些异常封装处理,来补充异常运行的状态信息.
1.3. Flutter异常封装处理
异常信息的封装主要分为两个步骤:异常信息的提取处理、添加附加信息.
1.3.1. Flutter异常提取处理
首先是异常种类的提取,一般通过就能获取到异常类型;但是要注意的是,之前hook上报的地方,有些异常被封装成,所以需要对其exception进行判断.
const FlutterErrorDetails({
this.exception, //真实的异常
this.stack,
this.library = 'Flutter framework',
this.context,
this.stackFilter,
this.informationCollector,
this.silent = false,
});
再者就是对异常的概述提取,我们通过使用Flutter框架中的一个函数来获取,如下面代码:
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 = ' ';
return longMessage;
}
还有就是堆栈的上报,异常上报的地方都会有stack信息,对于Flutter框架封装的,提取即可.处理完异常信息,我们需要给异常信息添加一些额外的运行通用信息,来帮助解决异常.
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符号表文件处理
上文说到,在编译过程中通过命令将符号文件生成并剥离出来并保存到指定目录,文件类似这样.首先我们会先将文件上传到artifactory仓库中,并在打包过程中分析出app产物的其它相关信息,比如版本、hash等等,之后将这些信息发送监控平台进行分析处理,以供之后的符号化解析使用.
完整架构图如下:

有了符号表文件之后,剩下的就是对堆栈进行解析处理,首先是Flutter异常的符号化解析.
2.2.2. Flutter异常符号化解析
首先需要了解这个从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 )
其中:
是指代函数的意思,
是其源码信息,
和 是这个函数相对于符号表文件高与低的偏移量,下文简称为
在下面这样一行堆栈中,
#02 abs 000000010bcc72e7 _kDartIsolateSnapshotInstructions+0x45d2e7
代表的是这行堆栈的错误信息是在指令段中,后面跟的偏移量就是相对起始的偏移量,下文简称为,它在一个符号表中是固定的.
我们只要通过找到,继而就能找到源码信息.他们的关系也很明显,通过命令找到相对符号表文件的偏移量,然后通过相加的方式得到
➜ nm app.ios-arm64.symbols | grep_kDartIsolateSnapshotInstructions
➜ _kDartIsolateSnapshotInstructions b 0x6000
其中0x6000就是
最终我们只要在info文件中找到在哪个源码信息的和之间,就能找到源码信息.
同样的,拿到这些信息后在line文件中我们通过偏移地址的映射关系也能找到对应的行号信息,这里我们就不做阐述.
那么这一套解析逻辑我们如何实现呢,官方既然提供了剔出符号化的逻辑,当然也会有符号化解析的逻辑.通过对flutter_tools源码的阅读可知,官方同样提供了一个的命令用于符号化解析,其通过获取符号文件与堆栈输入,最终通过库中的解析处理,如下代码所示:
@override
Future runCommand() async {
Stream> 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> outputController = StreamController>();
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();
}
}
其中对逻辑的阅读也能验证上文逻辑.其中计算偏移量的方式,官方还提供了其它几种计算方式:
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信息中的这两个运行时偏移地址和相减也能得出,之后通过第一种的逻辑最终得到.
除此之外,官方提供的中的仅支持的文件堆栈输入输入,并且后端服务不可能直接依赖整个dart的执行环境,所以我们将中的逻辑拆分,并扩展可支持堆栈类型,如下代码:
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可执行脚本,提供给后端服务用于解析堆栈信息.
➜ 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堆栈的解析处理.
iOS
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
我们需要做的就是将其转化为脚本能够识别的堆栈,也就是上文提到的这种:
堆栈编号 函数运行时绝对地址 dart Isolate代码段 isolate_offset
#00 abs 000000010455b93f _kDartIsolateSnapshotInstructions+0xc793f
也就是说我们要通过相对App.Framework的偏移量得到,按照上文一样的思路去处理.首先需要计算App.Framework中isolate和vm指令段相对App.Framework的偏移地址,通过这两个地址和相对App.framework的偏移量相减就能得到相对isolate和vm的偏移地址,也就是和.那么如何得到isolate和vm指令段相对App.Framework的偏移地址呢,通过命令也能拿到
➜ nm App.Framework | grep _kDartIsolateSnapshotInstructions
➜ 0000000000008000 T _kDartIsolateSnapshotInstructions
➜ nm App.Framework | gre p _kDartVmSnapshotInstructions
➜ 000000000000 4000 T _kDartVmSnapshotInstructions
其中0000000000008000就是isolate指令段相对App.Framework的偏移量.
因为这个命令的执行逻辑是对App.Framework进行分析,所以它实际上也是在上文提到的持续集成打包时的符号化处理过程中,通过上面的命令分析得到,然后保存到异常监控平台.
具体解析实现步骤如下图:
1、相对App.framework偏移量减去持续集成nm命令得到的偏移量,得出isolate_offset或者vm_offset;
2、然后利用上一步的结构拼接成 能够识别的堆栈.
3、对每行堆栈重复执行1、2步,然后使用 脚本解析出来.
android
Android和iOS的解析同理,我们也只要处理其中包含dart代码的libapp.so相关的堆栈.对于Android crash,其中包含的libapp.so相关堆栈如下:
#03 pc 00004828 /data/app/XXX/lib/arm/libapp.so (offset 0x200) (_kDartVmSnapshotInstructions+ 10280)
它的处理方式就很简单,因为和直接有了,所以我们只要拼接成转化为脚本能够识别的堆栈转化为脚本能够识别的堆栈,就可以处理.
到这里,我们已经将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 = "";//文件
public int line = -1;//行号
public String content;//栈帧原始信息 or pageName
public boolean asynchronous = false;//是否异步帧
}
之后便可以根据和App构建集成平台的信息进行匹配,如果匹配成功,就把这行栈帧作为聚合的信息.

注意: 聚合信息中不要包含行号信息,因为可能发生异常被修改但并未修复的情况,去掉行号信息可以在这种情况下,让错误还是聚合成一种.
3.1.1.特殊处理
通过业务栈帧来聚合处理异常在一些情况下可能不生效.通过对大量Flutter异常堆栈的分析,我们发现,因为future异步调用的问题,有许多的堆栈中并没有业务栈帧,并且会把异常聚合到无效栈帧.
比如下面这种PlatformException错误信息,如果按照业务栈帧优先的逻辑,对于这种没有业务栈帧的堆栈,就会把第1行作为聚合信息,这样这会导致大量的系统相关的错误都聚合成一种错误,对我们的问题解决以及阈值报警都有很大的干扰.
#0 MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319)
#1
#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)
#169 _CustomZone.runGuarded (dart:async/zone.dart:925)
#170 _invoke (dart:ui/hooks.dart:259)
#171 _drawFrame (dart:ui/hooks.dart:217)
所以除了业务栈帧优先聚合的逻辑,我们对异步栈帧也做了特殊处理 : 异步栈帧(第2行)的调用者高于被调用者.通过这种处理,这类错误会聚合到第三行上,代码中是这个错误中去.
除此之外,我们也提供了栈帧白名单的策略,也及白名单中的栈帧信息不属于错误信息.也可以使得异常不会聚合到某个无效栈帧中,进一步减少无效堆栈聚合的问题.
3.2.分配
当然,一个高效的监控平台,少不了自动分配异常的能力,这可以帮助负责人快速收到报警和响应错误.上文已经描述了异常发生时聚合的那几帧关键信息,对于分配也是如此.主要采用两个策略:
1、包含业务堆栈的异常,通过构建集成平台的组件维护信息,直接指派到负责人;
2、对于没有业务栈帧的异常,根据异常的种类来分配,比如是白屏问题,就根据上文提到的异常附加信息中的页面信息,来进行指派.
最终效果如下所示:

3.3.统计计算
对于crash监控中,崩溃率计算一般会采用两个口径:
会话崩溃率 : 用户每打开一次app计做一次会话,用 崩溃次数/会话次数 得到
设备崩溃率 : 每个用户崩溃只计做一次崩溃,用崩溃次数/用户数 得到
但这这两种都不适合Flutter,因为Flutter异常时,app并没有崩溃,那么按照上面提到的两种计算口径都不能真实的反应App稳定性.
比如打开一个页面,可能发生多次异常,但并未崩溃,那么按照会话崩溃率会得到 n/1这种不合理的异常率,尤其是混合开发中Flutter还不是app的全部功能的实现,通过会话和设备崩溃率统计还会有更多的偏差,因为用户打开App可能没有使用Flutter功能.
所以我们采用了一种新的统计口径:
页面异常率, 用户每打开一次页面计做一次pv, 用 异常数/pv数得到
通过这种计算方式,我们不仅能够得到app中Flutter本身整体的页面异常率,还能在后续给单业务页面计算页面的稳定性.
四、 结语
以上,就是贝壳在Flutter大规模应用时监控异常稳定性的一些实践和沉淀,希望对你有所帮助.对于Flutter异常的处理,未来还有一些需要做的,比如更细致的分类、根据异常种类进行自定义阈值报警、以及异步堆栈回溯、单页面分析等等,有机会的话会给大家带来更多相关的文章.
感兴趣的话记得点个喜欢