1. 项目概述与整体思路这个话题说来很有意思。我在做视力保护提醒应用的时候用的技术栈是 Flutter for OpenHarmony——也就是把 Flutter 跑在开源鸿蒙系统上。单从功能上看这类 App 并不复杂一个计时器到点弹提醒再加点统计和设置项。但真正跑起来才发现问题的重心根本不在功能实现而是错误处理。一个视力提醒应用核心价值在于“到了该休息的时候它得可靠地提醒你”。这意味着什么意味着通知必须准时发、声音必须响、界面不能被莫名卡死、后台杀不掉、数据不能越存越乱。任何一个环节出错用户的实际体验就是“到点了没反应”产品信任感瞬间清零。所以错误处理不是后置的收尾工作而是决定这个 App 能不能被长期信任的核心工程。先从整体框架说起。我接到的需求非常直接做一个可以后台运行、每分钟检查一次当前状态、到时间后弹出全屏提醒的视力保护工具。我当时定的技术选型是 Flutter for OpenHarmony原因很简单——开发效率高、热重载方便、一套代码多端适配。但跨平台框架和原生系统之间的能力边界恰恰是错误隐藏得最深的地方。这个 App 涉及的核心模块有四块设置页工作/休息时长、提醒开关、铃声选择计时引擎管理当前状态工作中/休息中/暂停、计算剩余时间通知模块到点后触发提醒、恢复工作时静默统计模块记录每日休息次数、工作时长分布功能清单不复杂但每个模块都可能踩到不同层级的错误。我把它们分成了几类Flutter 框架层的语言/运行时错误、业务逻辑层的状态错误、OpenHarmony 平台能力层的适配错误以及数据持久化层面的 IO 错误。每种错误大爷的打法和处理策略都不一样。从一开始我的设计原则就是不让任何未捕获异常直接打到用户面前。所有可能出现问题的环节都必须有一层兜底逻辑。这样容错能力上来了后期调优和新增功能才不会被存量问题绊住脚。2. 错误分类与处理策略设计2.1 从错误层次拆解处理方案在写第一版代码之前我先画了一张错误地图把所有可能出错的位置标出来。这个习惯是我做跨平台项目最受益的一步——比边写边补救强太多。我的错误地图分四层第一层Dart 语言/框架层。这类错误属于代码本身的 bug比如空指针、类型转换失败、Future 异常。表现特征是触发路径清晰、堆栈信息完整。处理方式靠 try/catch、try/catch 的 Future 分支以及 Dart 的 Zone 捕获全局异常。第二层业务状态层。计时状态机是整个 App 的核心状态之间的非法跳转很容易引发问题。比如用户在倒计时过程中切了后台回来的时候计时器状态已经变成了“暂停”或“失效”这时候就得分清楚到底该恢复还是重置。第三层平台能力层。Flutter 跑在 OpenHarmony 上有一些原生能力接口和插件种比较靠边的兼容性问题。比如通知权限、后台运行限制、电源管理稍有偏差就静默没提示。这部分错误的特点是——Flutter 这层不报错但功能就是没生效。我一开始就狠吃了这个亏。第四层数据持久化层。本地存储读写失败、配置损坏、缓存异常。这类错误平时不怎么出现但一旦出现就是灾难处理不好直接白屏。我把这四层做成了枚举类型在统一错误处理入口中按层分发边写代码边往枚举里补充。后续排错的时候一眼就能看出是哪一层出的问题定位效率提升很多。2.2 统一错误处理入口不要每个方法铺 try/catch如果每个函数都自己写 try/catch代码会非常碎片化而且很容易出现“吞掉异常不记录”的情况——写着写着就忘了打日志出问题的时候根本没法复现。我采用的方式是做一个统一的 ErrorHandler 单例所有可预见错误都进入同一个入口处理。入口里做的事包括三件日志记录、状态恢复、用户通知。class AppErrorHandler { static final AppErrorHandler _instance AppErrorHandler._internal(); factory AppErrorHandler() _instance; // 记录日志 void recordError(AppError error, StackTrace stack) { final entry LogEntry( layer: error.layer, code: error.code, message: error.message, stack: stack.toString(), timestamp: DateTime.now(), ); LogStore.instance.add(entry); } // 恢复错误现场 void recover(AppError error) { switch (error.layer) { case ErrorLayer.businessState: TimerStateMachine.instance.recover(); break; case ErrorLayer.platform: NotificationManager.instance.fallbackNotify(); break; case ErrorLayer.storage: SettingsManager.instance.resetToDefault(); break; default: break; } } // 通知用户 void notifyUser(AppError error) { if (error.shouldShowToUser) { ToastHelper.show(error.userMessage); } } void handle(Object e, StackTrace s) { final appError classify(e, s); recordError(appError, s); recover(appError); notifyUser(appError); } }这里我刻意避免把错误处理逻辑散落在业务代码中。业务代码只负责抛错统一入口负责决策。否则你会发现改处理策略的时候要到处翻代码维护成本成倍上涨。注意全局错误入口并不是让我“禁止局部处理”而是说关键决策要集中。部分场景依然需要局部捕获——比如 Timer 回调内部、异步流里的临时异常这些应该在局部就消化掉而不是全部抛给全局否则全局日志会被无价值的噪音刷屏。3. 核心模块实现与异常管理实操3.1 计时引擎的实现状态机是稳的代码就稳了视力提醒类 App 的计时引擎本质上是一个小状态机空闲 - 工作中 - 休息中 - 空闲穿插暂停和恢复。我实现的版本用Timer.periodic每秒触发一次 tick每 tick 检查当前状态和剩余时间。这里比较容易出错的点在于状态切换的瞬时性。如果用户在倒计时快结束的那一秒切走状态容易造成“状态已经变了但计时器没重置”的问题。我在业务层加了校验切换状态之前先检查当前状态是否合法。class TimerStateMachine { TimerState _state TimerState.idle; int _remainingSeconds 0; Timer? _timer; void startWork() { if (!_canTransitionTo(TimerState.working)) { throw AppError( layer: ErrorLayer.businessState, code: ILLEGAL_STATE_TRANSITION, message: Cannot start work from ${_state.name}, ); } _state TimerState.working; _remainingSeconds workDuration; _startTicker(); } void _tick() { if (_remainingSeconds 0) { _transitionToNextState(); return; } _remainingSeconds--; } }所有非法状态切换都被封装成 AppError 抛出由统一入口记录。这套逻辑的关键不在于“不会出错”而在于“出错一定有据可查”。管理状态机的人最怕的就是含糊状态我见过很多项目因为状态转移没有严格校验最后出了问题都说“不知道它当时在什么状态”。3.2 通知提醒模块平台能力差异的典型坑通知模块是这个 App 里最核心也最容易“静默失败”的部分。Flutter for OpenHarmony 的通知接口和 Android/iOS 上 I 用过的插件有差异调用方式、权限获取的路径都不一样。如果你在调用通知的时候没有做双重确认就极容易出现“代码没报错通知没弹出”的情况。我的做法是造了一个 NotifyManager 封装层内部区分“期望结果”和“实际结果”。class NotifyManager { Futurebool showNotify(ReminderPayload payload) async { try { final result await openHarmonyNotificationChannel.show( title: payload.title, content: payload.content, channelId: eye-care-reminder, ); if (!result.isSuccess) { throw AppError( layer: ErrorLayer.platform, code: NOTIFY_FAILED, message: result.errorMessage ?? Unknown notify error, ); } return true; } catch (e) { // 捕获插件异常后走兜底提醒 return _fallbackNotify(payload); } } }_fallbackNotify是兜底策略直接用应用内全屏页面做提醒不依赖系统通知栏。这套方案很土但确实可靠。视力提醒这种强时效性的应用第一优先级是把用户叫醒哪怕是 App 自己弹出一个全屏界面也比系统通知悄无声息地丢掉强。我强烈建议所有 Flutter for OpenHarmony 开发者把核心提醒能力做成“双通道”——系统通知为主、应用内全屏为辅。系统通知一旦失灵立即切换辅助方案。可靠性永远是这类工具型 App 的第一生命线。3.3 全局异常兜底Dart Zone 也不万能Dart 提供了runZonedGuarded来捕获全局未捕获异常我一开始以为有了它就万事大吉直到实际测试才发现Zone 只能捕获一部分异步错误部分错误依然会直接打到 Flutter engine 层。void main() { runZonedGuarded(() { WidgetsFlutterBinding.ensureInitialized(); runApp(EyeCareApp()); }, (error, stack) { AppErrorHandler().handle(error, stack); }); }这层兜底代码写得很快但真正的经验教训是不要把希望全部寄托在最后的兜底上——在各业务模块内部就要尽量捕获、分类、处理。全局兜底只是防止 App 崩掉的无趣保险真正的容错能力还是来自业务层。另外Flutter 的FlutterError.onError也要配一个收集通道专门处理渲染管线上的异常。渲染异常经常表现为“界面卡住但代码还在跑”很隐蔽。3.4 数据持久化的安全读写视力提醒 App 的持久化数据不多设置项、统计记录、上次提醒时间。我用的是 shared_preferences 的 OpenHarmony 兼容版。麻烦的是OpenHarmony 上多次异常退出可能导致配置写入不完整、文件损坏。我用了一个很朴素的方案写入前先备份旧值写入成功后确认如果发现本体损坏就从备份恢复。Futurevoid saveSettings(AppSettings settings) async { try { await prefs.setString(settings_backup, jsonEncode(_current)); await prefs.setString(settings, jsonEncode(settings)); } catch (e) { // 如果设置写失败但备份写成功了下次读取时自动恢复 AppErrorHandler().handle(e, StackTrace.current); } } AppSettings? loadSettings() { try { final raw prefs.getString(settings); if (raw null) return null; return AppSettings.fromJson(jsonDecode(raw)); } catch (e) { // 本体重构时尝试用备份恢复 final backup prefs.getString(settings_backup); if (backup ! null) { try { return AppSettings.fromJson(jsonDecode(backup)); } catch (_) {} } return null; } }不需要复杂的数据库不需要迁移工具就在读写两侧各做一层保险。这个备份策略虽然简单却在实际运行中救了我好几次——有好几次测试机在后台被强杀下次启动时设置没有丢。4. 实战踩坑实录这些问题真的会弄崩你4.1 后台计时失真问题第一个大坑Flutter 应用在 OpenHarmony 上切入后台后Timer.periodic的 tick 频率会被系统严格限制甚至直接挂起。这让我的计时引擎在后台完全不可靠——用户锁屏五分钟后回来发现倒计时只走了几十秒。排查过程很痛苦一开始我以为是系统调度问题反复折腾才发现是进程被冻了。解决办法是把时间计算的基准从“累计 tick 次数”改成“时间戳差”。void _tick() { final now DateTime.now(); final diffSeconds now.difference(_lastTickTime).inSeconds; if (diffSeconds 0) { _remainingSeconds - diffSeconds; _lastTickTime now; } if (_remainingSeconds 0) { _transitionToNextState(); } }类似的做法我在很多系统级任务管理 App 里见过。不要用自增来数秒要用时间差来算。这条经验适用于任何跨平台定时类应用。4.2 通知权限被拒后的用户体验视力提醒 App 如果连通知权限都没拿到就根本没法用。但 OpenHarmony 上的权限对话体验和 Android 不太一样用户第一次拒绝之后后面再次请求权限的路径比较隐蔽。很多用户点了“拒绝”之后应用就永远不会再弹窗口了他们也找不到设置入口在哪。我一开始的代码是很直白的final granted await notificationPermission.request(); if (!granted) { // 只在这里打个日志没有给用户任何提示 }结果可想而知——有 30% 的用户被权限拒绝后抱怨“根本不提醒”但实际上他们的权限是被自己拒过的。后来我加了一个权限准入检测启动时静默检查权限如果没权限就弹一个半透明的引导页配图讲解怎么开启通知权限点击引导按钮再调一次授权请求如果再次拒绝App 会显示为“低效模式”同时把所有提醒改为应用内全屏这套三重处理下来用户的流失率明显下降。权限体验也属于错误处理的一部分——用户拒绝权限本质上是一种可预期的异常路径你得把这条路径设计得平滑顺畅。4.3 OpenHarmony 平台接口的兼容错误Flutter for OpenHarmony 在部分 API 上有自己的实现差异比如获取设备信息、电池状态、屏幕亮度等。最典型的问题是在模拟器上运行没问题真机上某些 API 的返回值结构不兼容导致崩溃。我在代码里给所有调用平台 API 的地方都加了一层 ResponseEnvelope 包装统一解析结果class PlatformResultT { final bool success; final T? data; final String? errorCode; final String? errorMessage; }这个包装层的核心价值在于平台返回的数据结构如果变了错误信息至少是明确暴露出来的而不是在深层代码里变成一个毫无头绪的 cast error。接手这个项目的后来者只需看包装层就能定位问题不用把堆栈翻个底朝天。4.4 状态恢复应用被系统回收后的自愈OpenHarmony 的内存管理相对紧张应用切入后台一段时间后很可能被系统清理回收。用户再次从桌面点开 App期望的是直接回到之前的界面和状态而不是重新走一遍启动流程。这里需要设计状态恢复机制。我在应用启动时执行一次状态恢复函数读取上一次退出时的快照Futurevoid restoreStateIfNeeded() async { final snapshot await StateSnapshot.load(); if (snapshot null) return; if (snapshot.wasInWork) { final gap DateTime.now().difference(snapshot.lastTickTime); final targetRemaining snapshot.remainingSeconds - gap.inSeconds; if (targetRemaining 0) { TimerStateMachine.instance.restoreWorking(targetRemaining); } else { TimerStateMachine.instance.transitionToBreak(); } } }核心逻辑其实很容易想到但这里最容易漏的是“时间差”的处理退出时剩多少、重新进来时应该剩多少这两者之间隔着一段真空期。如果不对真空期做补偿用户会感受到计时跳变。这个状态自愈机制做了一次大补丁完成以后整个应用在体验上一下子稳了很多后台被杀再回来也不会丢状态了。4.5 setState 在页面销毁后的幽灵崩溃Flutter 开发中肯定遇到过用户已经切走了页面但 Timer 里还在回调setState然后报一个 “setState() called after dispose()” 的异常。这个问题不常有但一旦在错误日志里出现就说明你的页面生命周期管理有漏洞。解决方式非常简单页面销毁时手动把异步操作取消掉。override void dispose() { _tickerSubscription?.cancel(); _progressUpdateController.close(); super.dispose(); }经验上我再补充一个更稳的做法不要直接调 setState先检查mounted。这是所有异步回调里最低成本、最高收益的防崩策略if (mounted) { setState(() { ... }); }5. 常见问题速查表与调试技巧5.1 我排过的 Top 10 错误速查表错误现象根源解决方式后台返回后倒计时不变Timer 被系统冻结改用时间戳差计算剩余时间通知弹了但看不到提醒平台通知权限未授予加权限引导页 应用内全屏兜底设置明明改了重启后恢复旧值shared_preferences 写入失败写前备份旧值启动时自动恢复一些设备后台被杀后状态丢失应用被系统回收快照式状态恢复机制切页面时偶发 setState after dispose异步回调未取消用 mounted 检查 dispose 时取消订阅平台 API 返回值解析报错数据结构不兼容统一 ResponseEnvelope 包装全局路由跳转异常路由栈被清理路由栈重建 恢复栈内关键页面应用启动白屏初始化阶段异常runZonedGuarded 初始化异步注入日志文件无限膨胀日志记录无写满策略设最大条数按时间滚动清理首次安装后权限请求时机不佳用户还没建立信任先引导核心功能延后请求权限这张表是我自己整理的项目 bug 复盘。每个项目都不太一样但这些问题在 Flutter for OpenHarmony 的开发里普遍存在。你可以把它当作一个快速索引踩到类似坑的时候先对着表查一遍定位方向。5.2 日志系统的设计别图简单要能追溯错误处理如果没有日志记录支撑等于把诊断能力废了一半。我在项目里自己写了一个轻量日志模块记录所有入参、异常、关键状态变化。它的组织结构比较简单class LogEntry { final DateTime timestamp; final String layer; final String code; final String message; final String? stack; final MapString, dynamic? context; }记录日志时有三个原则我遵守得很严格日志级别要清晰区分。debug 信息不用线上输出error 必须全量留存。上下文信息必须带。只是存一个xxx failed没有价值必须带上当前状态、参数、调用链。日志要有上限。我是每个会话最多保留 2000 条超过就滚动清理避免存储膨胀导致性能下降。在调试阶段我在代码里加了一个调试页直接读日志库、带搜索和按错误层过滤的功能。上线后如果有用户反馈问题你把日志导出来按 code 搜索几秒钟就能确定问题根源。这个效率比用户在描述“我点了那个按钮然后这样那样”要快十倍。5.3 可观测性Olny 代码不出错还不够在错误处理做到一定深度后我慢慢意识到另一个更重要的事——错误处理不只是写代码应对异常还要让异常可以被监测、被度量。我给视力提醒 App 做了几个简单的业务埋点每轮工作/休息正常结束时上报状态计时异常导致的用户手动重置次数通知模块兜底触发次数启动恢复的自愈失败次数这些指标虽然简单但能真实反映用户是否在使用中被问题卡住。如果你发现某一天的兜底触发数突然飙升说明系统的通知能力出了大问题需要立刻检查。这种主动观测的思路比被动等着用户投诉要可靠得多。6. 关于调试工具与工作流的补充6.1 日志分组与关键字过滤操作 Flutter for OpenHarmony 的项目日志往往会混入框架层和原生层的信息。我在 debug 模式下手动定义了三个日志分组编组名称只用于排查时按关键词过滤fltter engine 的日志会自然带 flutter 标记业务日志统一带EYECARE前缀。排查问题时先过滤EYECARE再按错误码细分。整个过程非常顺畅。adb logcat -s EYECARE:V # 只看业务日志 adb logcat | grep -E EYECARE|Notification # 混合排查6.2 崩溃前的现场恢复工具OpenHarmony 崩溃现场的恢复能力不如预期好我补了一个“崩溃现场快照”机制App 主流程的每一步关键操作都往内存里写一个小快照当检测到启动时颜色配置与快照颜色不一致说明上次流程没走完就可以做恢复处理。这套做法思路类似写文件时的 journal 机制不复杂但很有用。实际跑下来崩溃后的启动流程恢复成功率大幅提升用户反馈也基本没有了。7. 整个项目的处理心得与扩展空间7.1 关于错误处理的整体经验总结回看这次 Flutter for OpenHarmony 视力提醒 App 的开发我要说几个核心收获。第一错误处理不是防御性编程的花架子而是这个应用的可靠性底座。用户要的是“到点必须提醒”任何一环的失误都会导致信任崩塌。我没有把精力浪费在追求零错误上而是把重点放在“出错后如何尽快恢复”和“出错后如何不让用户感知”两者之间做好平衡。第二全局兜底、分层处理、双通道容错、状态快照修复这四个策略是跨平台工具型 App 的通用答案。无论你做什么类型的应用——只要有时钟或定时器逻辑这两个经验就全部适用。只要有数据存储备份恢复就必不可少。只要有通知推送兜底策略就应该天生存在。第三技术以外的细节决定了产品成败。权限引导页、全屏提醒兜底、角标提示、防打扰静默模式这些设计都不是核心技术难点但如果不处理它们整套应用的用户体验就会打折扣。技术上的错误处理对应到产品上就是“遇到意外时产品怎么面对用户”。7.2 后续延伸再把可靠性推进一步做完这个版本之后回头看看我觉得有这几个扩展方向值得去做。把计时精度从分钟级提高到秒级校准。目前的时间戳差方案已经能解决后台冻结问题但如果系统时钟被人为修改还是会对剩余时间计算产生误差。可以做系统时间源漂移检测发现时间跳变时主动提示用户校准。把日志系统升级成错误上报通道。现在日志只在本地存着虽然调试效率已经不错但如果能做统一上报配合错误码统计分析后期维护会更省力。未来我会考虑收集匿名错误摘要用于优先级判断。视力保护其实可以延伸做一些护眼小功能比如屏幕色温自动调节、眼保健操定时引导、久坐提醒等。功能扩大的同时错误处理的地图也要同步扩充。到时候还是同样的方法论分层分类、统一入口、双通道容错、状态恢复兜底。7.3 最后再分享一个调试中的小技巧开发过程中有一个非常实用的技巧在应用的设置页里藏一个“错误模拟器”入口。这个入口只在 debug 构建中出现底层逻辑是手动触发一系列已封装的错误类型——比如模拟一次通知失败、模拟一次存储写入损坏、模拟一次状态机非法跳转。有了这个入口每次修改完错误处理逻辑我就不需要刻意去“复现场景”来验证了。点一下按钮就能进入观众模式看它是否被统一入口正确捕获、是否正确恢复、是否有预期的用户提示。这套办法让我的回归测试效率提升了一大截。如果你正在开发类似 Flutter 或者其他跨平台框架的定时/提醒类应用建议也提前做一个错误模拟器。它能帮你把开发周期中大量不可控的“也许这次也能行”变成可控的“我确实验证过了”。