你可能觉得视力保护提醒App这种项目没什么技术含量无非是定个时、发个通知、调一下光线传感器。我在OpenHarmony设备上做这个Flutter实战项目的前两周也是这么想的直到第三次真机测试时发现一个诡异现象锁屏五分钟后重新打开所有提醒都消失了。App还活着、界面还能滑、日志也正常但任何一个定时任务都不再触发。这个现象直接把我拽进了错误处理与异常管理的深水区。排查了一整天才确定根因不在Dart代码逻辑而是OpenHarmony对后台任务有自己的一套调度策略任务被挂起后Flutter侧的Timer并没有感知整个提醒链路处于半死状态。更麻烦的是这种故障不抛异常、不闪退、日志不报错完全静默。这类项目的真正难点从来不在UI画得漂不漂亮而在于从传感器读数到提醒通知一条链路上任何一个环节悄悄失效用户得到的结果都是两个字没用。本篇是Flutter for OpenHarmony视力保护提醒App实战的异常管理篇我把项目里踩过的坑、设计过的错误处理机制、以及最后沉淀下来的排查方法论一次性讲清楚。适合两类读者一是正在把已有Flutter应用向OpenHarmony上移植的开发者二是准备从零做OpenHarmony跨端应用、希望少走弯路的人。1. 为什么视力保护提醒App的异常管理比业务逻辑更耗精力1.1 从20-20-20法则说起功能越简单异常越致命视力保护App最常见的就是20-20-20法则提醒每用眼20分钟看远方20秒。听起来就是一个Timer加一个通知代码量不超过一百行。但如果要把体验做到真的能用事情完全不是这样。真实链路会变成获取当前屏幕使用状态、读取光线传感器数值、判断用户是否正在注视屏幕、检查是否已经进入过休息状态、决定要不要触发提醒、发送本地通知、把本次提醒结果写进本地记录、最后刷新统计页面。任何一步失败用户都收不到提醒。这里有个本质区别普通App的失败是可被用户感知并重试的。列表加载失败页面有重试按钮登录失败有错误提示。但提醒类App的失败是不可见的。用户不会知道本来该提醒我休息但系统没提醒他只会觉得这个App没用然后默默卸载。所以在这种项目里静默失败比闪退还可怕——闪退至少有用户反馈静默失败连反馈都没有。1.2 Flutter与OpenHarmony双栈的特殊复杂度这个项目天然是双技术栈。Flutter负责界面渲染和业务状态OpenHarmony负责传感器、通知、后台调度这些系统能力两者通过MethodChannel和EventChannel通信。这意味着一次简单的判断距离并提醒动作要跨两个技术栈完成。Dart侧逻辑写得再严谨OpenHarmony侧某个API因为权限不足、设备差异、资源占用等因素返回了一个完全没预料到的结果异常照样产生。而且OpenHarmony的设备生态跨度很大传感器型号不统一系统版本行为不一致厂商定制策略更多样。模拟器跑通不等于真机可用一台真机可用不等于所有真机可用。我在这类项目里体会最深的一点错误处理不是上线前补的防御工事而是架构阶段就要设计的骨架。如果你把异常管理当时项目都写完了再回来加那基本等于把整栋楼的地基挖了重浇。2. 先摸清家底OpenHarmony环境中Flutter异常的完整分类2.1 Dart层异常业务代码里最容易被漏掉的部分Dart层的同步异常最好处理try-catch直接接住就行。真正麻烦的是异步异常。Future里的异常可以用catchError或者try-catch接到但回调函数里的异常特别容易漏。举个例子我在做休息状态监听时最开始写过这样的代码// 错误写法异步回调里的异常外层catch根本接不住 try { restMonitor.onRestTriggered((duration) { throw StateError(unexpected rest state); }); } catch (e) { // 这里永远执行不到 }Dart的异常捕获是基于调用栈的回调是稍后才执行的外层调用早就返回了。正确的做法是在回调内部处理restMonitor.onRestTriggered((duration) { try { _handleRestTriggered(duration); } catch (e, stack) { _reportError(rest_trigger, e, stack); } });Stream的监听同理。没有挂onError监听的Stream异常会进入未处理状态在debug模式下直接导致zone错误release模式下可能被吞掉。我排查过一个bug光线传感器的EventChannel数据流偶尔抛出一个类型转换错误因为没有onErrorDart侧完全不知道但内存里飘着一个已损坏的订阅状态后续数据全部丢失。2.2 平台通道异常PlatformException只是冰山一角MethodChannel调用OpenHarmony原生能力时最常见的异常确实是PlatformException。但OpenHarmony侧返回的错误格式不一定规范。有的接口返回的是带code的完整错误对象有的只返回一个笼统的字符串有的在失败时直接返回null——而null在Dart侧可能被当成成功但无数据处理继续往下走然后在业务层制造一个更难排查的空值错误。EventChannel的断连问题更要命。当应用进入后台、通道被系统回收、或者传感器被其他高优先级任务抢占时事件流会静默断开。Dart侧不会有任何回调、任何异常、任何提示。我是在测试环境发现光线传感器数据突然停了的时候才意识到这个问题的。后来加了心跳机制原生侧每五秒推一个心跳事件Dart侧十五秒没收到心跳就判定通道异常触发重连。2.3 生命周期与状态异常Flutter on OpenHarmony特有的时序问题OpenHarmony对Flutter应用的生命周期管理有自己的逻辑跟Android不完全一样。WidgetsBindingObserver里的didChangeAppLifecycleState在部分OpenHarmony设备上触发时机会有延迟甚至出现前后台状态跳变从paused直接到resumed中间的inactive和hidden被跳过。这个时序问题直接影响提醒调度。我在恢复计时器时依赖生命周期回调结果发现有时候App已经在前台了计时器还没恢复。后来改用双源校验生命周期回调只作为参考真正决定计时器状态的是一系列互相校验的时间戳和活动标记。这里我给个排查建议所有生命周期回调入口都打时间戳日志把App从启动到锁屏再到恢复的完整事件流打印出来。你会惊讶地发现常态表现和你的直觉完全不同。3. 异步异常捕获实战三层保险别指望一个try-catch走天下3.1 第一层异步入口统统做规范化收口我在这类项目里最大的改变是给所有异步入口函数定了一个统一约定不直接抛异常而是返回一个带状态的结果对象。这样做的好处是业务代码逻辑统一不会在任何一个异步点漏掉异常。sealed class LoadResultT { const LoadResult(); } class LoadSuccessT extends LoadResultT { const LoadSuccess(this.data); final T data; } class LoadFailureT extends LoadResultT { const LoadFailure(this.code, this.message, [this.detail]); final String code; final String message; final Object? detail; }调用方长这样FutureLoadResultRestStat loadRestStat() async { try { final stat await _repo.fetch(); return LoadSuccess(stat); } on TimeoutException { return const LoadFailure(timeout, 查询超时请稍后重试); } on PlatformException catch (e) { _saveLog(rest_stat, platform_error, e); return LoadFailure(platform_${e.code}, e.message ?? 系统能力暂不可用); } catch (e, stack) { _saveLog(rest_stat, unknown_error, e, stack); return const LoadFailure(unknown, 发生未知错误); } }你会发现所有异常都变成了数据流UI层只需要判断LoadResult是成功还是失败失败时展示哪个message逻辑清晰得接近无脑。代价是要为每个接口写一点点样板代码但这个成本在排查问题时会成倍赚回来。异常转数据之后你可以在日志里留下完整的错误码、原始异常和堆栈而不是让异常穿越层级然后消失。3.2 第二层Zone兜底与FlutterError重定向try-catch能覆盖掉大部分业务异常但总有漏网之鱼。比如某个第三方组件内部异步回调里的异常、某个平台通道的底层错误、某个我没考虑到的边界触发。这层要用runZonedGuarded做全局兜底。void main() { runZonedGuarded(() { WidgetsFlutterBinding.ensureInitialized(); FlutterError.onError (details) { _handleFlutterError(details); }; runApp(const EyeCareApp()); }, (error, stack) { _handleFlutterError(FlutterErrorDetails( exception: error, stack: stack, library: app_zone, )); }); }_handleFlutterError里做的事很关键写日志、尝试上报、但不影响当前UI。这样至少保证异常发生了我们有记录而不是让异常直接触发红屏或者闪退。这个做法还有个额外好处release模式下FlutterError.onError默认行为是把错误打印到控制台但用户在真机上根本看不到控制台。重定向之后错误会被写进本地文件配合用户反馈就能定位问题。3.3 第三层ErrorWidget.builder做最后一道视觉兜底如果某个组件在build阶段抛了异常Flutter会用它内置的ErrorWidget代替组件渲染默认是一个灰底红条。这个在开发阶段还行给用户看到就太粗暴了。我替换成了统一风格的错误占位ErrorWidget.builder (details) { _saveLog(widget_error, details.exception, details.stack); return const Material( child: Center( child: Padding( padding: EdgeInsets.all(16), child: Text( 这个区域出了点问题我们已记录原因, textAlign: TextAlign.center, ), ), ), ); };注意ErrorWidget.builder只兜住build阶段的异常事件回调、异步操作里的异常它管不了。所以这三层是层层包裹关系第一层处理业务异步流第二层处理全局漏网异常第三层处理组件渲染异常。三层都齐了才敢说异常处理体系基本成型。4. 传感器与硬件调用最容易出现静默故障的地方4.1 传感器数据的可信度校验视力保护App离不开传感器光线传感器、距离传感器、可能还有加速度传感器。这些硬件数据有一个共性不是每次读数都可信。光线传感器在部分设备上返回的是lux值但不同厂商的数值范围差异巨大。有的设备在室内正常光线下返回50有的返回5000。更麻烦的是有些传感器在弱光环境下会返回0或一个极小的值如果直接拿来判断环境过暗然后疯狂提醒用户开灯体验会非常糟糕。距离传感器的坑更多。部分设备在无法测量距离时返回-1这个值如果进入判断逻辑用户明明正常坐姿也会被判定为距离过近。我是用过滤策略解决的连续读三次数去掉无效值取有效值的中位数作为本次判断依据。如果连续五次都没有有效值就判定传感器不可用自动降级。4.2 权限与可用性降级把失败变成可理解的策略传感器权限被用户拒绝这件事几乎是必然发生的。谁都不喜欢一个App突然要读这么多传感器数据。这里的关键不是代码怎么处理异常而是产品策略怎么设计。我最终采用的是软降级方案距离传感器不可用就只做时长提醒光线传感器不可用就用固定默认亮度策略权限被拒绝设置页给用户清晰的说明距离提醒需要传感器权限未授权时该功能不可用而不是弹一个系统风格的报错框。实现上很简单class DistanceMonitor { DistanceMonitor(this._sensor); Futurebool isUsable() async { try { final hasPermission await _sensor.checkPermission(); if (!hasPermission) { return false; } final status await _sensor.getStatus(); return status SensorStatus.available; } on PlatformException catch (e) { _saveLog(sensor, check_failed, e); return false; } catch (e) { _saveLog(sensor, check_unknown, e); return false; } } }所有异常都收敛成一个bool值。上层业务拿到false就降级逻辑非常干净。4.3 平台通道的错误码映射让问题可诊断最开始我的做法是把OpenHarmony侧的错误字符串直接透传给Dart层。结果就是排查问题时看到一堆毫无规律的乱码根本不知道错误来自哪个模块。后来我设计了一套错误码规范模块-层级-码值。比如SCN_LIGHT_1001表示传感器模块光线传感器权限拒绝NTF_SEND_2002表示通知模块发送超时。OpenHarmony侧把错误码和附加信息拼成一个Map// OpenHarmony侧(ArkTS)返回给Dart的结构示意 { code: SCN_LIGHT_1001, message: light sensor permission denied }Dart侧统一解析这个结构落到日志和用户提示里。这套规范让排查效率提了一个量级。你不需要再猜错误是什么意思只看前缀就能锁定模块范围。5. 提醒调度与后台任务的异常战场5.1 定时任务的悬挂与恢复提醒类App最核心的机制是定时调度。我用Timer.periodic做轮询调度每个30秒检查一次时间是否到达下一个提醒节点。这个方案比单个Timer更稳因为单Timer在系统挂起后一旦失效就彻底没戏了轮询Timer至少可以在App恢复活跃后第一时间发现该提醒的没提醒。但真正的挑战是OpenHarmony的后台策略。应用进入后台或锁屏后系统可能暂停应用进程的某些活动。我在生命周期回调里做了恢复逻辑在resumed时重新校准所有计时器override void didChangeAppLifecycleState(AppLifecycleState state) { if (state AppLifecycleState.resumed) { _rescheduleAllTimers(); } } void _rescheduleAllTimers() { final now DateTime.now(); if (_nextRestAt ! null now.isAfter(_nextRestAt!)) { _triggerRestReminder(); } }这里有个细节不能直接把Timer重置为20分钟应该重新计算当前时间到原定提醒节点的差值。这样用户只是短暂切走App回来后如果已经超过提醒时间立刻触发提醒而不是重新计时。5.2 通知发送的失败重试与幂等通知是提醒链路的最终出口。通知发送失败的原因很多用户关闭了通知权限、系统请勿打扰模式、当天通知配额已满、应用在后台时通知受限。如果发送失败就放弃链路上所有努力都白费了。我加了一个重试和幂等机制。每个提醒都有一个reminderId发送成功后记录该ID已发送发送失败则进入待重试队列下次App启动或网络恢复时重试。核心原则是同一个reminderId只能发成功一次避免用户收到重复提醒。这个设计解决了两个问题。一是网络或系统短暂故障导致的失败不再丢失提醒二是重复触发不会轰炸用户。5.3 从静默失败到可感知降级前面强调过静默失败是提醒类App最大的敌人。我的应对方式是做一个健康状态自检功能放在设置页的显眼位置。App启动时跑一次自检检查通知权限是否开启、距离传感器是否可用、光线传感器是否可用、后台运行权限是否正常。每个状态项直接展示在设置页用正常或不可用表示并给出原因说明。这样即使某个功能因为系统原因失效用户也看得清清楚楚。这块的代码核心就是一个状态聚合器class HealthReport { final bool notificationEnabled; final bool distanceSensorUsable; final bool lightSensorUsable; final bool backgroundAllowed; } FutureHealthReport generateHealthReport() async { final results await Future.wait([ _notificationService.checkEnabled(), _distanceSensor.isUsable(), _lightSensor.isUsable(), _backgroundService.isAllowed(), ]); return HealthReport( notificationEnabled: results[0] as bool, distanceSensorUsable: results[1] as bool, lightSensorUsable: results[2] as bool, backgroundAllowed: results[3] as bool, ); }让异常可见、让降级可见用户至少知道发生了什么事。这比任何隐藏的容错逻辑都重要。6. 崩溃防御与问题复盘不追求不崩追求崩得明白6.1 日志链路设计关键节点打卡式记录我见过太多项目把日志当回事的时候已经是线上事故之后了。这个项目里我从第一天就要求所有关键节点写日志。不是简单的进函数打一条而是结构化的事件流。每个提醒生命周期都有固定日志点提醒计划生成、定时器校准、传感器数据读取、提醒条件判断、通知发送请求、发送结果回调。任何一步异常顺着这次提醒的唯一ID就能把整条链路串起来。日志存储上我用的是分级策略debug环境输出到控制台release环境写本地文件文件按天切割保留最近七天。注意一个细节日志写入本身也要做异常保护。文件写失败、磁盘满、权限被收回这些异常不能反过来把日志系统搞崩。日志IO全部在独立的事件循环里执行主流程不等待也不感知。6.2 真机矩阵与逃生舱设计OpenHarmony设备差异带来的异常靠单元测试和模拟器永远测不出来。我最后整理了一个真机测试矩阵至少覆盖三台不同厂商的设备分别评估它们的传感器数据表现、后台策略差异、生命周期回调时机。每次发版前用这个矩阵做冒烟验证成本不大但能挡掉大多数设备相关异常。还有一个经验是逃生舱。每类传感器功能都做了一个远程配置开关后端配置中心可以随时关闭某个功能的开关。比如某批设备突然出现距离传感器误报可以通过配置直接停掉距离提醒而不是让用户在误报轰炸中把App卸载。这个机制上线后救过我至少两次。最后补充一个实用技巧在开发阶段给App加一个隐藏的异常记录页面把所有错误事件按时间倒序列出来。真机测试时随手翻一翻很多偶发异常当场就能发现不需要等用户反馈。做完这个项目我对异常管理的理解有了很大变化。以前写普通App异常处理的核心目的是防止闪退现在做这种强依赖系统能力的应用异常管理其实就是产品逻辑的一部分。用户视力保护的效果直接取决于提醒链路每个环节的可靠性。与其说我们在写业务代码不如说我们在设计一套即便某个环节失败、整体依然体面可用的系统。如果你正在做Flutter on OpenHarmony相关的应用祝你少踩一些我踩过的坑也希望这些经验能帮你把错误处理这层地基打得更牢。