我最近在一台 RK3568 的开发板上,用 Flutter 给 OpenHarmony 做了一款视力保护提醒 App。功能不算复杂:定时弹提醒、检测连续用眼时长、给出休息建议。但真正让我花掉大半开发时间的,不是界面布局,也不是状态管理,而是错误处理与异常管理。第一版跑起来时,定时器一到点,App 经常直接闪退,没留下任何堆栈信息,仿佛什么都没发生过。后来我彻底梳理了 Dart 层、平台通道、系统服务三个层面的异常链路,才让这个项目真正稳定下来。这篇文章就把我在 Flutter for OpenHarmony 这条路上踩过的异常坑、用到的处理方案,以及一次完整崩溃的排查过程,一次性讲清楚。
1. 视力提醒类开发板应用为什么总在"最不该崩"的时候崩
1.1 提醒类 App 的本质:系统能力调度器
视力保护提醒 App 这类应用,从工程结构上看,本质是一个"系统能力调度器"。它不依赖复杂 UI,但需要在正确的时间点,正确地把设备上的若干系统能力串联起来。拿我的项目举例,核心链路大致是这样:
- 用
Timer.periodic维护用眼时长计时; - 到点后调用系统通知能力,弹出提醒;
- 如果启用了摄像头或传感器检测用眼距离,还得拉起相机/传感器通道;
- 在用户切换前后台时,需要感知生命周期并调整定时策略。
这套链路里,定时器本身只是一个毫秒级精度的调度源,真正的风险全部集中在外部依赖上。通知服务可能没准备好,传感器可能被占用,平台通道可能在前一个页面销毁后被继续调用。任何一个环节出问题,如果代码里没有兜底,异常就会一路冒泡到顶层,最后表现为整个 App 闪退。
我第一版代码里最常见的写法是:
dart复制Timer.periodic(const Duration(minutes: 20), (timer) async {
await notificationService.sendReminder();
});
当时我觉得这写法没什么问题。直到某天真正跑起来,通知服务偶发抛异常,Timer 回调里又没人接住,整个应用直接白屏退出。我才意识到一个问题:提醒类 App 的"提醒"发生在后台、发生在用户没盯着屏幕的时候,这里的异常,恰恰是最容易被忽略、又最致命的那一类。
1.2 OpenHarmony 设备差异放大了异常概率
在 OpenHarmony 上做 Flutter 开发,遇到的情况比 Android 更复杂。Android 至少机型多但系统服务相对统一,而 OpenHarmony 当前的开发板生态很杂:RK3568、RK3588、DAYU200、各种定制板子,各自的系统裁剪程度、驱动完备度、权限策略都可能不一样。
网上经常有人问"RK3568 有许多设备树到底咋选",这其实就是一个信号:设备树选错,驱动加载就不完整;驱动不完整,传感器服务、通知服务这些系统能力就可能是残缺的。我在一块板子上跑得好好的通知通道,换到另一块板子上直接抛异常,原因就是那台设备的通知服务没有正常注册。
这意味着什么?意味着做错误处理时,不能假设系统能力一定存在、一定可用。代码里必须有一套机制,能在"能力缺失"或"服务异常"时优雅降级,而不是直接崩溃。
1.3 分层的异常管理总思路:异常不该在一个地方全处理
经历几次闪退之后,我重新设计了项目里的异常处理架构。核心思路是分层,不把所有异常都塞到某个全局函数里。
- UI 层:只处理"如何把错误呈现给用户"——弹提示、显示重试按钮、或者静默降级。
- 业务层:用统一的
Result/ 错误码模型来表达失败,不在业务代码里到处裸抛字符串。 - 平台层:把
MethodChannel抛出的PlatformException、MissingPluginException转换成业务层可识别的异常类型。
三层的目标不一样:UI 层要体验,业务层要可控,平台层要防泄漏。这样任何一层出问题,都不会直接捅到最外层导致闪退。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Dart 层异常拦截:三层兜底与业务异常建模
2.1 runZonedGuarded:在 main() 里筑第一道墙
Dart 的异常模型和很多语言不太一样。try-catch 只能接住同步代码和显式 await 的异步异常,而全局未捕获异常,最终会走到 Zone 的 handleUncaughtError。如果不做任何处理,这个未捕获异常就会直接导致 isolate 终止,表现就是 App 闪退。
所以我在 main() 里第一件事,就是用 runZonedGuarded 包住整个应用:
dart复制void main() {
runZonedGuarded(() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const VisionGuardApp());
}, (error, stackTrace) {
// 这里接住所有未捕获异常
CrashReporter.instance.reportError(error, stackTrace, 'zone');
});
}
这里有个细节值得说明:WidgetsFlutterBinding.ensureInitialized() 必须放在 runZonedGuarded 的回调里面。因为 Flutter 引擎初始化过程中的一部分异步任务也跑在同一个 Zone 里,放在外面的话,这些异步任务产生的未捕获异常,runZonedGuarded 反而接不住。
runZonedGuarded 相当于给整个应用铺了一张安全网。它的价值不在于替代 try-catch,而在于兜住那些"没想到会抛"的异常——比如平台通道异常、引擎内部回调异常。有了这张网,App 至少不会因为一个未处理异常直接闪退,而是能先把错误记录下来。
2.2 FlutterError.onError 与 PlatformDispatcher.onError:框架层和引擎层的兜底
runZonedGuarded 能接住大部分 Dart 侧的未捕获异常,但 Flutter 框架层自己也会处理一些异常,这些异常不会自动走到 Zone 的兜底逻辑里。典
