做 Flutter 项目最难受的时刻,不是功能赶不上版本,而是线上崩溃了你却不知道为什么。用户那边一句话“打开就闪退”,你这边无论如何复现不了,日志库里也没有历史记录,只能靠猜。等跨端场景再加上鸿蒙,问题会更明显:不同系统的崩溃形态不一样,Dart 层异常和原生崩溃混在一起,不搭一套清晰的监控链路,光定位问题就能浪费一个下午。
我最近在一个同时支持 Android、iOS 和鸿蒙 HarmonyOS Next 的项目里,把 Sentry 这套体系从初始化、日志采集、异常上报到生产环境噪音治理完整落了一遍。核心用到的就是sentry_flutter和sentry_logging两个库,前者负责 Flutter 崩溃的采集和上传,后者负责把业务日志转成 Sentry 能读懂的面包屑和事件。这篇文章不是文档翻译,而是我真机接入之后的结构化经验:从为什么选它,到每一步怎么配置,再到鸿蒙上哪些能落地、哪些要先评估,最后附上生产环境的调参方法和排查清单。
如果你正在考虑给项目上崩溃监控,或者已经上了但 issue 一大堆看不出重点,甚至只是好奇鸿蒙上跑 Flutter 的稳定性怎么做,这篇文章都能给你一个可以直接照做的路线图。
1. 为什么崩溃监控要用 Sentry + sentry_logging
1.1 生产环境下 Flutter 崩溃诊断的难点
Flutter 项目在开发模式下有很多辅助信息,但一旦发布,Dart 的异常分布在好几个层面,排查起来根本不是同一套思路。
同步代码抛出的异常会在当前调用栈一路外抛,最后被 Flutter 框架捕获;异步任务里的异常,包括 Future 回调、Timer、事件流里的错误,如果没有人监听,就直接被吞掉;渲染阶段的异常会触发 FlutterError.onError,但线上用户看到的就是白屏或者错误页。你如果没有监控,这三类问题对运营和客服来说都叫“用不了”,对开发来说却对应完全不同的根因,沟通成本非常高。
跨端的问题更麻烦。同一个业务代码在 Android 上只是一个红色报错,到了鸿蒙 HarmonyOS Next 上可能变成 Native 层的内存问题;同一个页面逻辑在 iOS 上正常,在鸿蒙上却因为某个插件没有实现直接抛 MissingPluginException。所以监控方案必须同时覆盖 Dart 层和平台层,还要把用户操作链路和业务日志关联起来。只看堆栈不看病程,根本没法做稳定性治理。
1.2 为什么要选 Sentry,而不是自建上报
很多团队一开始会想:我不就是想把错误收集起来再发到服务端吗?自己写个队列和上报接口,两三天不就好了?这个想法我听过很多次,真正落地的时候往往要面对这些东西:本地日志队列怎么设计、崩溃现场怎么快照、堆栈怎么符号化、上报失败怎么重试、重复错误怎么聚合、版本怎么区分、告警规则怎么配、数据看板怎么做。做完之后还要持续维护,因为到了多端场景,每端都有各自的坑。
Sentry 之所以值得选,是因为它把这些脏活都处理掉了。Sentry 官方维护 Flutter SDK,sentry_flutter自动接入未捕获异常、FlutterError 和路由生命周期面包屑;后端支持开源自托管,也可以直接用官方 SaaS;Issue 聚合并通过 fingerprint 把同一个错误的不同堆栈归一化,Release 管理能直接看到每个版本的崩溃率变化。这些能力自建方案想在一年内做到同等水平,投入的时间成本非常高。
还有一点很关键:Sentry 的 Dart SDK 上层协议是纯 Dart 实现,不需要在每个平台都写一套自定义网络层。这意味着在鸿蒙上只要 Dart 代码能跑起来,日志上报和 Dart 异常上报这条路就是通的,不会像某些 APM 一样只支持 Android 和 iOS,换到 ohos 体系就完全抓瞎。
1.3 sentry_logging 在整个监控体系里的定位
sentry_flutter本身已经做了一堆自动采集:未捕获异常、Flutter 错误、生命周期面包屑、失败的网络请求。但这些自动采集有一个通病——它只告诉你在哪里崩了,不告诉你为什么崩。
举个例子,你的 App 在订单确认页崩溃,自动采集能抓到当前页面和堆栈,但用户进入这个页面之前做了什么?上一个接口有没有报错?是点击重试后才崩溃的,还是首次进入就崩溃?这些信息自动采集拿不到。
logging是 Dart 和 Flutter 官方推荐的标准日志接口,sentry_logging的作用就是把它接入 Sentry 生态。在业务代码里写一条log.severe('订单接口连续重试3次失败', e, {'orderId': ...}),这条信息会在后续崩溃出现之前被记录下来,变成一个面包屑。最终你在崩溃详情页看到的是完整时间线:用户进入页面、发起请求、请求失败、点击重试、再次失败、然后崩溃。定位问题从猜谜变成了看证据,省掉的排查时间非常可观。
2. 方案设计思路:日志、异常、链路三位一体
2.1 核心设计:面包屑的上下文价值
Sentry 把一条崩溃记录看成一个事件,事件上可以附带最多 100 条面包屑。每条面包屑都有时间戳、级别、类别和自定义数据。接入sentry_logging之后,普通业务日志会按级别自动转换成面包屑,跟崩溃事件一起上报,用来还原崩溃前的用户操作路径。
这个过程很像侦探破案。崩溃堆栈相当于案发现场的脚印,面包屑则是嫌疑人之前的行动轨迹。只看脚印你只能确认他来过,结合轨迹才能知道他来这里做了什么。我在业务里的做法是统一四个约定:
- Logger 名称统一用“模块/子模块”,例如
order/payment、network/http; - 用户关键动作统一记录
user_action: xxx,方便后续搜索; - 网络层统一记录请求方法、路径、状态码和耗时;
- 失败重试日志必须带重试次数,比如
retry_count=2。
这些约定初看会增加一点代码量,但上线一到两周你就会发现,它能帮你把大量 issue 从“无法定位”变成“一眼看穿”。
2.2 日志级别与事件生成的分配策略
接入SentryLogging之前,要把日志级别和事件/面包屑的关系理清楚。我总结成一张表,日常写代码时照着这个规则来,不会出错。
| Level | 是否生成面包屑 | 是否生成事件 | 典型场景 |
|---|---|---|---|
| FINEST / FINER | 否 | 否 | 高频调试信息 |
| FINE | 可按环境开启 | 否 | 函数进入、退出 |
| INFO | 是 | 否 | 页面打开、请求发出 |
| WARNING | 是 | 否 | 重试、降级、缓存命中 |
| SEVERE | 是 | 是 | 接口失败、业务异常 |
| SHOUT | 是 | 是 | 致命异常 |
面包屑的价值在于还原现场,所以 INFO 到 WARNING 的日志最适合做成面包屑;如果闹到 SEVERE,说明已经是一个需要被关注的错误了,除了当上下文记录,还应该触发独立事件。实际配置里我推荐这样设置:
options.addIntegration( SentryLogging( minBreadcrumbLevel: Level.INFO, maxBreadcrumbLevel: Level.WARNING, minEventLevel: Level.SEVERE, ), );这样做的好处是,日志系统里日常的流程记录不会占据事件名额,也不会刷屏;一旦出现severe级别日志,Sentry 会立刻看到一个独立 Issue。而崩溃发生时,INFO 和 WARNING 级别的面包屑已经提前记录好了,崩溃详情里照样能还原上下文。
2.3 鸿蒙 HarmonyOS Next 的适配判断
很多人在鸿蒙 NEXT 上接监控,第一反应是找有没有官方插件。实际拆开看要分两层:
第一层是 Dart 层,也就是sentry_logging和sentry_flutter里不依赖原生代码的部分。日志记录、异常捕获、面包屑管理、HTTP 上报,这些跑在 Dart VM 上,只要 Flutter 能在鸿蒙设备上正常跑起来,这条路就通的。我的经验是,鸿蒙适配阶段先用 Dart 层把日志和 Dart 异常打通,收益最大,落地也最快。
第二层是原生崩溃层。HarmonyOS NEXT 的 Native Crash 需要原生 SDK 做内存快照、信号处理、符号化,这些依赖 Sentry 官方对 ohos 的适配进度和你项目里使用的 Flutter 鸿蒙工具链版本。如果暂时没有现成能力,可以先用系统日志和用户反馈做补充,同时设计一个评估清单:这个 Crash 是否高频、是否核心路径、是否只有鸿蒙用户遇到、能否通过日志还原现场。满足这几点再考虑投入原生层适配,否则先处理 Dart 层数据更划算。
还有一个小细节:不要在上报时把平台写死成 Android 或 iOS。我给事件加了platform标签,ohos、android、ios分开统计,才能看出哪个平台真正有问题。
3. 从零开始的完整接入步骤
3.1 依赖引入与版本选择
先加依赖。我当前项目用的是 Sentry 8.x 系列,它对应 Dart 3 和较新的 Flutter 版本,对logging的接入方式也最成熟。在项目根目录执行:
flutter pub add sentry sentry_flutter sentry_logging logging或者直接改pubspec.yaml:
dependencies: flutter: sdk: flutter sentry: ^8.5.0 sentry_flutter: ^8.5.0 sentry_logging: ^8.5.0 logging: ^1.2.0版本选择上不要盲目追新,建议看CHANGELOG确认 breaking change。如果项目用到了鸿蒙的 Flutter 分支,还要留意官方sentry包的 Dart SDK 约束是否跟当前工具链一致。遇到依赖冲突时,优先考虑手动调整主 SDK 版本,而不是到处加dependency_overrides,否则以后升级会很难受。
3.2 Sentry 项目和 DSN 准备
在 Sentry 控制台新建项目,平台选择 Flutter。创建完成后会拿到一串 DSN,格式大致是:
https://公钥@上报服务器地址/项目IDDSN 的每个部分都有含义:协议决定走 http 还是 https,公钥是项目标识,服务器地址是上报入口,项目 ID 用于区分不同项目。要注意 DSN 里的公钥不算密钥,不会导致环境被入侵,但也不要直接在代码里硬编码并提交到公开仓库,建议通过启动参数传入:
flutter run --dart-define=SENTRY_DSN=https://xxx@xxx/1在代码里用String.fromEnvironment('SENTRY_DSN')读取,这样不同环境可以注入不同 DSN,也更方便换自托管服务器。
3.3 初始化配置:一次性看懂每个参数
main.dart里的初始化是整个监控链路的起点。我常用的模板如下:
import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_logging/sentry_logging.dart'; Future<void> main() async { WidgetsFlutterBinding.ensureInitialized(); await SentryFlutter.init( (options) { options.dsn = const String.fromEnvironment('SENTRY_DSN'); options.environment = const String.fromEnvironment('APP_ENV', defaultValue: 'development'); options.release = 'com.example.app@1.2.0+12'; options.sampleRate = 1.0; options.tracesSampleRate = 0.1; options.attachStacktrace = true; options.enableAppLifecycleBreadcrumbs = true; options.enableCaptureFailedRequests = true; options.addIntegration( SentryLogging( minBreadcrumbLevel: Level.INFO, maxBreadcrumbLevel: Level.WARNING, minEventLevel: Level.SEVERE, ), ); options.beforeSend = (event, {hint}) { if (event.environment == 'debug') return null; return event; }; }, appRunner: () => runApp(const MyApp()), ); }逐个解释关键参数:
environment用来区分开发、测试、生产。同一个 release 如果环境不同,Sentry 面板会分开统计,避免测试环境的噪音污染生产数据。release字符串承载版本信息,Sentry 会按它聚合崩溃率和发布健康度,格式建议是“包名@版本号+构建号”,每次发版都要核对。
sampleRate控制事件采样率,普通项目先设 1.0,保证不漏数据。tracesSampleRate控制性能追踪采样率,因为 traces 的量通常比事件大很多,生产环境设 0.1 或更低就够。attachStacktrace打开后会在没有显式堆栈的日志上附加调用栈,对日志定位很有用。
enableCaptureFailedRequests会让失败的 HTTP 请求自动产生面包屑,配合网络日志能快速看出崩溃前是否有接口连环失败。beforeSend会在事件真正上传前做最后一道过滤,这里演示了把 debug 环境的误报消息直接拦截掉。
appRunner是初始化完成后再启动 App 的入口。别看只是个回调,它的意义是保证 SDK 的监听器先挂好,再进入业务代码,否则压启动阶段的异常就会漏掉。
3.4 logging 包改造:把业务日志变成监控数据
初始化只是第一步,业务代码里怎么用logging才是决定监控质量的关键。我一般会做一个很小的日志工具,避免业务代码每次都要拼一长串参数:
import 'package:logging/logging.dart'; final Logger log = Logger('auth/login'); void recordInfo(String message, [Map<String, Object?>? extras]) { log.info(message, null, extras); } void recordWarning(String message, Object? error, StackTrace? stackTrace, [Map<String, Object?>? extras]) { log.warning(message, error, stackTrace, extras); } void recordError(String message, Object? error, StackTrace? stackTrace, [Map<String, Object?>? extras]) { log.severe(message, error, stackTrace, extras); }logging的log方法支持传入错误对象、堆栈和自定义结构化字段,这些字段会跟着面包屑进入 Sentry。尽量用结构化字段代替简单拼串,因为 Sentry 面板里可以做字段索引和筛选,比在一大段字符串里 grep 高效得多。
还有一点经验:不要在build方法里打日志。Flutter 的 build 会因布局变化反复执行,导致面包屑瞬间爆炸。日志放在事件回调、网络层、页面生命周期这些真正有业务含义的位置,信息密度高,面板也干净。
4. 异常上报与场景示例实战
4.1 Dart 异常是怎么一路变成 Sentry Issue 的
很多人接入后只关心“代码里哪一行上报了”,我建议先理解整条链路,后面排查问题会省事很多。
Flutter 进入生产模式后,Dart 的未捕获异常会经过几个监听点:FlutterError 处理框架渲染异常,PlatformDispatcher 的 onError 处理引擎层异常,再加上 SDK 内部还会用 runZonedGuarded 捕获异步错误。SentryFlutter 在初始化时会把监听器挂到这些位置,异常一出现就带着当前堆栈、Scope 里的用户信息、标签、面包屑一起组装成 SentryEvent,然后写入本地缓存,再异步上传到服务端。
服务端收到事件后,会根据 stack trace 和 fingerprint 做聚合。同一个错误不管发生多少次,最终会收敛到一个 Issue 上,并在面板里显示趋势线、版本分布、设备分布。这也是为什么要设置好 release 和 tags——你没给数据,面板就分析不出来。
4.2 手动上报业务异常的正确姿势
自动捕获覆盖不到的业务异常,需要用Sentry.captureException或Sentry.captureMessage手动上报。手动上报的关键是别丢上下文。
import 'package:sentry_flutter/sentry_flutter.dart'; Future<void> fetchOrderDetail(String orderId) async { try { // 业务请求 } catch (e, st) { await Sentry.captureException( e, stackTrace: st, hint: Hint.withMap({'source': 'fetchOrderDetail'}), ); rethrow; } }需要手动上报的场景典型有:接口返回业务错误码但 HTTP 状态码 200、数据库读写异常、需要人工确认的降级逻辑触发、isolate 里发生的错误。这些情况不会触发 Flutter 全局崩溃捕获,但确实是线上故障的元凶。
用户信息最好在初始化后尽早设置,不要每次上报都重复添加:
Sentry.configureScope((scope) { scope.setUser(SentryUser(id: userId, email: email)); scope.setTag('membership_level', 'vip'); });设置好用户信息之后,同一个用户反复触发同一种异常,Sentry 面板会直接展示影响用户数。这个指标在判断优先级时,比单纯的崩溃次数更能说明问题。
4.3 场景一:登录接口异常排查
假设线上反馈:部分用户登录时闪退。你只拿到一条用户描述,听起来很玄。接好 Sentry 后查看 issue,崩溃堆栈指向订单页的一个空对象。然后你顺着面包屑往下看,时间线是这样的:
用户点击登录按钮,发送登录请求,请求失败,日志里出现severe事件“登录接口异常”,里面带着statusCode=500和retry_count=1,随后页面用空对象渲染用户信息,触发了渲染异常并崩溃。
这段时间线是怎么来的?就是业务日志按规范打出来的:
final log = Logger('auth/login'); log.info('登录请求开始', { 'username': maskedUsername, }); try { final response = await _loginApi.login(username, password); log.info('登录请求成功', {'statusCode': response.statusCode}); } catch (e, st) { log.severe('登录接口异常', e, st, { 'statusCode': e is ApiException ? e.statusCode : -1, 'retry_count': retryCount, }); rethrow; }如果没有接入sentry_logging,你看到的问题就是“订单页空对象崩溃”,可能要去改渲染逻辑。而有了面包屑上下文,你会先看到“登录接口 500”这个前置条件,排查方向立刻翻转到后端。
4.4 场景二:页面渲染崩溃定位
渲染崩溃在 Flutter 里很常见,大多是数据格式和 UI 假设不一致。例如接口新返回了一个空数组,但代码里直接取第一个元素。SentryFlutter 默认接入了 FlutterError,所以这类异常会主动上报。但上报的堆栈只能看到 build 方法,看不到接口返回了什么。
要解决这个问题,需要在接口返回时就留下现场数据:
Logger('order/detail').severe( '订单详情数据格式异常', null, { 'rawData': rawData.length > 200 ? rawData.substring(0, 200) : rawData, 'expectedFields': ['items', 'totalAmount'], }, );只截取前 200 字符是为了避免上报超限和泄露隐私。接下来再发生渲染崩溃时,Sentry Issue 里除了 stack trace,还有接口返回样例和期望字段,定位问题基本不用再去找后端拉日志。
如果想把渲染错误处理得更细,可以自定义FlutterError.onError,在默认逻辑之外追加自己的上报和恢复逻辑。但要注意别重复上报,Sentry 内部已经挂过一份监听,你再挂一个会导致同一次崩溃出现两条重复 Issue。
5. 生产环境监控的调参与噪音治理
5.1 环境、release、平台标签的规范化
监控链路接好了,接下来最影响使用体验的是数据是否干净。我见过不少项目,代码里能上报,但打开面板全是没用的噪音,真正的问题沉在下面看不到。关键先做三件事:环境隔离、release 规范、平台标签。
| 配置项 | 开发环境 | 测试环境 | 生产环境 |
|---|---|---|---|
| environment | development | staging | production |
| sampleRate | 1.0 | 1.0 | 1.0 |
| tracesSampleRate | 1.0 | 0.5 | 0.1 |
| debug | true | false | false |
release字符串一旦不更新,所有版本的崩溃都会堆在同一个 Issue 上,你根本看不出是不是新版本修复了问题。我的建议是在构建脚本里自动生成:取当前版本号和构建号拼出release,不要写死在代码里。
平台标签也要提前约定好。可以监听defaultTargetPlatform,但在鸿蒙场景下可能需要结合工具链能力判断。我是在初始化时统一给 scope 打标签:
Sentry.configureScope((scope) { scope.setTag('platform', 'ohos'); scope.setTag('sdk_version', '1.2.0'); });有了这些标签,面板就能按“生产环境 + 鸿蒙 + 最新版本”筛选,定位问题的范围一下子缩小很多。
5.2 采样率、缓存与离线补偿
生产环境不能无脑全量上报。性能追踪的采样率建议 0.1,百万用户量级一天产生的事件数非常可观,服务端和客户端都会吃力。错误事件不要采样,错误采样会让你漏掉低频但致命的崩溃。
SentryFlutter自带本地缓存机制,上报失败会写入磁盘,下次启动继续尝试。缓存上限用options.maxCacheItems控制,默认值对大多数场景够用。如果你的 App 经常在弱网环境运行,建议显式设一下:
options.maxCacheItems = 100;这个参数不是越大越好。缓存太多会在下次启动时集中上报,导致网络拥塞和电量损耗。我实测 100 条是比较平衡的值,既能覆盖离线往返,又不会造成重启风暴。
5.3 beforeSend 过滤噪音的真实案例
生产环境最大的噪音来源往往不是业务代码,而是三方 SDK 自己抛的错误。某些 SDK 的底层网络探测失败每天都会产生几百条 Issue,看起来吓人,实际上毫无影响。全部忽略也不好,可能掩盖真实问题,更好的做法是分类处理。
options.beforeSend = (event, {hint}) { // 已知无害的三方错误,直接丢弃 if (event.throwable is BadCertificateException) { return null; } // 业务上已经降级的异常,改一个指纹方便聚合 if (event.message?.contains('network timeout') ?? false) { event.fingerprint = ['business-degraded', 'network-timeout']; return event; } // 鸿蒙上部分能力缺失,单独打标方便统计 if (event.tags?['platform'] == 'ohos' && event.throwable is MissingPluginException) { event.fingerprint = ['ohos-missing-plugin', event.throwable.toString()]; return event; } return event; };这里核心思路是:不要一刀切丢弃,而是给不同类型分配不同的聚合策略。真正无害的丢弃,暂时不重要的独立成类,有价值的保留原样。这样几个月下来,Issue 列表里剩下的基本都是值得处理的问题。
5.4 告警规则与团队协作
面板再好看,没人看就是零。告警要接进团队日常使用的协作工具,比如飞书、钉钉或 Slack。Sentry 的 Alert Rule 可以按条件设置:崩溃次数超过阈值、影响用户数超过多少、特定 release 出现新 issue。
我习惯设三类规则:
- 致命路径崩溃率异常:核心下单页面新增 issue 数超过 2 条就告警;
- 静默异常持续增长:某个 fingerprint 在 5 分钟内事件数超过 20 条并且连续出现 10 分钟;
- 版本回归:上个版本没有、这个版本突然出现的 issue。
规则关联 issue 负责人时,尽量用团队群而不是个人。崩溃这种问题的跨端归因往往需要客户端、服务端、测试三方一起看,群公告比私聊效率高得多。
6. 常见问题与避坑实录
6.1 上报没数据?按这个顺序排查
这是接入后最常见的问题。代码改了、跑起来了、面板还是空的,我的排查顺序很固定:先确认 DSN 正确并已生效,再看是否关闭了 debug,再手动上报一条测试消息,接着检查设备网络,最后查 release 和 beforeSend 是否有拦截逻辑。
// 手动验证,正常的话两分钟后面板能看到 Sentry.captureMessage('connectivity check', level: SentryLevel.info);如果手动消息能看到,说明整条链路没问题,问题在特定异常没有触发上报条件。如果手动也看不到,优先怀疑 DSN 没传进去。我踩过一次坑,String.fromEnvironment在flutter build没带参数时拿到的是空字符串,SDK 初始化不报错也不上报,排查了半天。
6.2 日志重复与面包屑泛滥
有时会发现同一个问题在面板里出现多次,或者面包屑一大半是重复日志。常见原因有三个:Logger.root绑定了多个 listener,SentryLogging被 add 了两次,或者build方法里有日志输出。
排查时先把logging的 listener 数量列出来:
Logger.root.listen((record) { // 不要在这层再调用 Sentry 手工上报 });SentryLogging 自己会监听 Logger.root,业务层不要再重复对同一条日志做captureMessage,否则必然重复。开发模式可以开启options.debug = true,控制台会打印 Sentry 内部日志,能看到每条上报的来源。
6.3 鸿蒙 NEXT 上 Crash 堆栈缺失怎么办
鸿蒙 HARBOR 体系下,如果原生层没有完整的 Crash 上报实现,你会遇到 Dart 异常能上报、原生崩溃却缺失堆栈的情况。这不是代码接错了,是平台能力还没补齐。
我的处理策略是分级:先确认崩溃是不是集中在 Dart 层错误,如果是,跟着面包屑和堆栈就能定位。如果确实指向原生层,临时在 main 函数里监控关键原生调用,打点记录成功失败,同时用 HarmonyOS 的设备日志作为参考来源。另外,对比同一份业务代码在 Android 和鸿蒙两端的崩溃率,能帮你判断问题是平台适配导致的还是服务端数据导致的。
判断原生崩溃是否遗漏,最直接的方法是看崩溃事件里有没有contexts下的os信息,以及堆栈中是否出现 Dart VM 之外的符号。如果全是 Dart 符号,大概率是 Dart 层异常,不是原生崩溃。
6.4 发版前必查的 release 字符串
这个坑藏在最后,但影响最大。release 字符串一旦漏改,Sentry 就会把所有版本混在一起。崩溃率看起来每天都在波动,实际上是被旧版本流量影响了。我每次发版前会跑一个脚本,检查当前构建里的 release 是否包含本次发布的版本号,不一致直接 fail。
经验做法是把 release 生成放到 CI 流程里,用构建号自动拼。这样本地调试用的往往是devrelease,上了 CI 才会变成正式 release,既不会把本地调试数据混进生产,也不会出现漏改 version 的情况。
6.5 OOM 导致日志断掉
线上还有一个常见怪象:日志在前半段好好的,后半段突然中断,紧接着系统杀进程。这种多半是内存压力导致的 OOM,不是代码主动崩溃。Flutter 端 OOM 很难抓到完整堆栈,因为系统不会给你机会上报。
我的经验是提前做内存水位监控,在 App 进入后台和收到内存警告时打点。在 Flutter 里可以监听WidgetsBindingObserver.didHaveMemoryPressure,把内存水位作为上下文写到面包屑里。这样即使日志中断,你也能知道崩溃前是否已经出现了内存压力信号,再结合 Flutter 内存优化手段去治理,方向就对了。
另外,isolate 里的异常也要单独处理。Sentry 默认只接管主 isolate,后台 isolate 产生的异常不会自动上报。我给每个后台 isolate 套了一层通用 try-catch,出现异常时手动转发到主 isolate 的 Sentry 通道,避免任务静默失败。这一步不做,你会漏掉大量后台任务的隐性故障。
最后再分享一点个人体会:接入 Sentry 和用好 Sentry,真的是两件事。头几天日志刷屏、Issue 很多很吵,非常正常,关键是你要先建立规范,再逐步优化。把 env、release、platform 这些基础维度定好,把业务日志按面包屑的思维打出来,这套系统才会从“一个上报工具”变成“线上问题的第一道防线”。鸿蒙这块我的建议是先打通 Dart 层链路,别等所有原生能力都补齐才上线,越快看到线上数据,越早知道哪些需求是真实存在的,哪些只是你想象出来的。这套配置和排查方法我落地之后,项目崩溃问题的平均定位时间缩短了至少一半,希望你也能少走弯路。