去年把一套 Flutter 应用往 OpenHarmony 上移植,做的是“视力保护提醒”这类工具型 App。功能本身不复杂:定时提醒休息、前置摄像头估算观看距离、统计当日护眼数据。但真正让我加班到后半夜的,不是界面适配,而是错误处理与异常管理。Flutter 在 Android/iOS 上的异常体系已经比较成熟,可一旦接上 OpenHarmony 这个相对较新的生态,平台通道、权限、后台任务的坑是一个接一个。这篇文章就把我在实战中踩过的坑、拆过的方案、写过的代码整理出来,给准备在 OpenHarmony 上用 Flutter 做应用的开发者一些参考。
这篇文章不是教你怎么写业务逻辑,而是聚焦在错误处理上:护眼提醒 App 里有哪些高危异常场景、全局异常捕获怎么搭、错误怎么上报、用户怎么提示、真机问题怎么排查。按照这套思路落地,基本能把线上异常压掉一大半。
1. 项目背景与错误处理整体设计思路
1.1 这个护眼 App 到底要管哪些“错误”
先盘一下项目里真正可能出现异常的地方,不要等报了 bug 再临时抱佛脚。护眼提醒 App 虽然功能不多,但每个模块都踩在系统能力的边界上:
| 功能模块 | 典型异常 | 严重程度 | 影响范围 |
|---|---|---|---|
| 通知提醒 | 通知权限被关闭、通知创建失败 | 高 | 提醒功能直接失效 |
| 定时调度 | 系统清理进程、重启后提醒丢失、时区变化 | 高 | 用户收不到任何提醒 |
| 摄像头测距 | 无摄像头、相机被占用、权限拒绝、弱光帧异常 | 中 | 距离检测降级 |
| 本地存储 | 数据库损坏、磁盘空间不足 | 高 | 护眼记录丢失 |
| 网络上报 | 请求失败、超时、队列积压 | 低 | 数据不同步 |
| 平台通道 | 插件未链接、参数序列化失败 | 高 | 整个功能不可用 |
这里的核心思路是:先按模块和严重程度把异常归类,再决定每类异常采用什么处理策略。有的要做到“用户无感知恢复”,有的要做降级处理,有的必须中断操作并引导用户去系统设置页。千万别所有异常都走同一个提示模板,那用户体验会很差,排查问题也会很痛苦。
1.2 错误处理方案选型:分层捕获 + 统一兜底
当时团队内部讨论过两种方案。第一种是传统的“哪里出错就在哪里 try-catch”,优点是直接,缺点是代码里到处是捕获块,漏网之鱼很多,而且不同开发者的错误处理风格不一致。第二种就是下面要讲的“分层捕获 + 统一兜底”,也是我最终采用的方案。
code复制Dart 业务代码(针对可预期错误主动捕获)
↓
Flutter 框架层(FlutterError.onError)
↓
平台派发层(PlatformDispatcher.instance.onError)
↓
异步 Zone 层(runZonedGuarded)
↓
全局错误上报 + 本地日志落盘
为什么这么分层?因为 Flutter 的异常来源其实是多条线的,单靠一层根本兜不住:
- 业务代码里的同步异常和异步异常:用 try-catch 和捕获 Future 错误,但无法保证每个人写的代码都不漏。
- Flutter 框架内部的异常:比如 build 期间抛错、布局越界,这些会交给 FlutterError.onError。
- 平台引擎侧的异常:例如 MethodChannel 调度过程中出现的异常,会经过 PlatformDispatcher.onError。
- 第三方插件包里的异步异常:这些未必从 Flutter 框架的管道走,得靠 runZonedGuarded 做最后的兜底。
这种分层设计的好处是:每一类异常都有明确归属,同时统一汇总进一个 ErrorReporter,输出结构清晰的错误日志,而不是散落在各地。
1.3 为什么在 OpenHarmony 上这套设计尤其重要
说实话,Android/iOS 上 Flutter 的异常处理已经很成熟,网上大把方案。但 OpenHarmony 不一样,这个生态还比较早期,很多 Flutter 插件并没有官方适配版本,需要自己封装 Platform Channel 甚至修改原生工程。
这种环境下,插件本身可能就是你最大的异常源。比如一些插件在 Android 上运行正常,在 OpenHarmony 上因为原生侧 API 没实现,直接抛 MissingPluginException;或者原生侧返回了空值,Dart 这边一旦按非空类型处理就崩。如果没有统一的捕获和分级策略,这种问题会非常难查。
我当时的做法是:把“平台通道异常”单独列为一类,并在封装插件时统一 catch,转换成自定义的业务异常码。这样上层逻辑永远不直接面对 MissingPluginException 或 PlatformException,而是面对一组我们定义的错误码,例如 NotifyPermissionDenied、CameraInitFailed、ReminderCreateFailed。这对后续排查和用户提示都友好得多。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心异常场景拆解与处理要点
2.1 通知与权限类异常:最容易出问题的一环
护眼提醒 App 的第一核心能力就是通知,而通知恰恰是权限敏感度最高的模块。在 OpenHarmony 上,通知权限的申请与 Android 不太一样,需要在 module.json5 里声明权限,再通过系统的 abilityAccessCtrl 请求动态授权。
Flutter 侧获取权限状态,需要走 MethodChannel,因为目前还没有太成熟的 OpenHarmony 通知插件。首先要处理的就是“权限状态查询失败”的情况。我当时封装了一个方法:
dart复制Future<bool> isNotificationEnabled() async {
try {
final bool? enabled = await _channel.invokeMethod('isNotificationEnabled');
return enabled ?? true;
} on MissingPluginException {
// 插件没接上,不能直接判定为“没权限”,要按模块降级处理
Log.w('通知能力插件未注册,走降级逻辑');
return true;
} on PlatformException catch (e) {
Log.w('通知状态查询异常,code=${e.code}, message=${e.message}');
return true;
}
}
这里有个经验:权限查询失败时,我倾向于返回“可用”,而不是返回“不可用”。因为如果底层接口突然抛错,我们不知道真实状态,返回不可用会导致所有用户被强制关掉通知功能;返回可用的话,后续发送通知时会自动失败,至少那时候还能拿到更准确的错误码。
但发送通知失败时,就不能这么保守了。用户看不到提醒,护眼功能就等于废了。所以发送通知失败要立即走“轻提示 + 跳转设置页”的策略,引导用户到系统设置里开启通知。
2.2 定时提醒与后台运行异常:跨平台要额外小心
护眼提醒离不开定时器。一开始在 Flutter 里用 Timer.periodic 写了个每 20 分钟提醒的逻辑,结果测试时发现锁屏半小时后提醒就完全消失了。原因很直接:Flutter 的 Dart 层 Timer 依赖应用进程存活,进程被系统回收后瞬间失效。
所以要在 OpenHarmony 上实现可靠提醒,必须走系统级能力。在 OpenHarmony 上一般是两个选择:WorkScheduler 或者 reminderAgentManager。我最终用的是 reminderAgentManager,它更像系统的闹钟/提醒服务,能在应用不运行的情况下把提醒发出来。
但这个方案同样带来新的异常场景:
- 创建提醒时参数错误:比如时间格式不合法、重复规则冲突,原生侧会返回错误码。
- 应用更新后旧提醒与新逻辑不兼容。
- 系统重启后提醒数据需要重新注册。
- 用户改了系统时间或时区,导致下一次提醒时间计算错误。
我的处理方式是:在 Flutter 侧维护一个“提醒模型”并持久化到本地数据库,每次启动时对比数据库里的提醒记录和系统侧的注册状态,发现不一致就重新注册。同时,每次创建系统提醒都包裹 try-catch,把错误码和回调时间记录下来,打点统计。
这个做法虽然啰嗦,但解决了一个很核心的问题:系统侧提醒“悄悄失败”的时候,至少应用侧在下次启动时能发现并自动修复,而不是让用户永远收不到提醒。
2.3 摄像头距离检测异常:硬件和解码层面都要兜住
视力保护 App 的第二个亮点功能是距离检测,用前置摄像头估算眼睛和屏幕的距离。这个模块我刚接完摄像头就遇到了三个问题:
- OpenHarmony 上没有现成的 Flutter camera 插件,我直接封装了原生的 CameraKit。
- 相机初始化不是瞬时完成的,需要异步等待,中间任何一步失败都要回滚。
- 相机一旦被其他应用占用,初始化会直接失败,而且错误信息比较隐晦。
针对相机这类硬件资源,我设计了一个简单的状态机:
code复制idle → opening → opened → failed
↘ error/degraded
初始化过程中只要出现异常,就把状态切到 degraded,而不是直接崩溃。降级模式下不提供摄像头测距,但保留基础的定时休息提醒功能。用户看到的是“距离检测暂不可用,已切换到定时模式”,而不是“应用已停止运行”。
dart复制enum EyeCareMode { timerOnly, distanceDetect }
class CameraControllerWrapper {
EyeCareMode _mode = EyeCareMode.timerOnly;
Future<void> init() async {
try {
await _openCamera();
_mode = EyeCareMode.distanceDetect;
} on CameraPermissionDenied {
// 权限被拒绝,静默降级
_mode = EyeCareMode.timerOnly;
} on CameraInitTimeout {
// 初始化超时,释放资源
_release();
_mode = EyeCareMode.timerOnly;
} catch (e) {
Log.e('摄像头初始化未知异常: $e');
_mode = EyeCareMode.timerOnly;
}
}
}
这个设计保证核心提醒功能不依赖摄像头模块,摄像头挂了顶多少一个功能,但护眼提醒的主流程还在。
3. 实操实现:异常捕获、上报与恢复机制
3.1 全局异常捕获三层护甲
全局捕获是错误处理的地基。我在 main.dart 里同时挂了三个监听,分别处理不同类型的问题:
dart复制Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 第一层:Flutter 框架异常
FlutterError.onError = (FlutterErrorDetails details) {
FlutterError.presentError(details);
ErrorReporter.report(
details.exception,
details.stack,
stage: 'flutter',
module: 'framework',
);
};
// 第二层:平台派发异常
PlatformDispatcher.instance.onError = (Object error, StackTrace? stack) {
ErrorReporter.report(error, stack, stage: 'platform', module: 'engine');
return true; // 关键点:返回 true 表示该错误已处理,防止直接崩溃
};
// 第三层:异步 Zone 兜底
await runZonedGuarded(() async {
runApp(const EyeCareApp());
}, (Object error, StackTrace stack) {
ErrorReporter.report(error, stack, stage: 'zone', module: 'async');
});
}
为什么要同时挂三个?因为它们的职责完全不同:
FlutterError.onError捕获的是 Flutter 框架层构建、布局、绘制过程中出现的异常,不设置它,一个布局错误就可能让整个页面白屏。PlatformDispatcher.instance.onError捕获的是引擎侧分发异常,尤其是 MethodChannel 调用过程中的错误。注意回调返回true这个细节,如果不返回 true,Flutter 引擎会认为异常未被处理,直接终止应用。runZonedGuarded主要兜底那些没有在业务代码里显式捕获的异步错误,比如某个第三方插件内部Future抛出的异常。
很多初学者只挂了第一层,结果遇到平台通道异常依然崩溃。三层全部挂上之后,真正“冷崩溃”的概率会低非常多。
3.2 错误上报与本地日志设计
错误捕获到之后不能只打日志。线上用户不会主动把日志发给你,所以必须设计一套自动上报机制。我在项目里定义了一个统一的错误模型:
dart复制class ErrorEvent {
final DateTime time;
final String stage; // zone/platform/flutter/business
final String module; // notification/camera/reminder/storage/network
final String code; // 自定义错误码,比如 PERMISSION_DENIED
final String message;
final String? stack;
final Map<String, dynamic> extras; // 附加信息,如设备型号、系统版本
Map<String, dynamic> toJson() => {
'time': time.toIso8601String(),
'stage': stage,
'module': module,
'code': code,
'message': message,
'stack': stack,
'extras': extras,
};
}
上报之前先落盘。我在 getApplicationSupportDirectory() 下建了一个日志目录,按天分文件,每条错误以 JSON 行格式追加写入。日志文件会控制大小和保留天数,超过 7 天的自动删除,避免用户存储被无限制占用。
上报策略上,我没有搞复杂的实时上报,而是做了个“离线队列 + 空闲重试”:
dart复制Future<void> reportWithRetry(ErrorEvent event) async {
await _queueDao.insert(event);
_tryFlush();
}
Future<void> _tryFlush() async {
if (!await _network.isAvailable()) return;
final pending = await _queueDao.takeBatch(20);
final ok = await _api.upload(pending);
if (ok) {
await _queueDao.delete(pending);
} else {
// 下次启动再重试,重试次数过多就丢弃最旧的数据
await _queueDao.markRetry(pending);
}
}
这个队列设计有一个容易被忽略的点:最大重试次数和最大队列长度必须做限制。否则网络长期不可用时,错误数据越积越多,不仅占存储,还会在网络恢复后的短时间内把流量打满。我设置的是队列上限 200 条,超过部分丢弃最老数据。
3.3 用户提示策略与自动恢复实现
不是所有错误都要弹窗,也不是所有错误都要静默。我把错误提示分成了三个等级,做成了一套映射:
| 错误等级 | 处理策略 | 典型场景 |
|---|---|---|
| SILENT | 静默恢复,记录日志 | 网络上报失败、日志写入失败 |
| LIGHT | SnackBar 非阻断提示 | 摄像头测距不可用,已切换定时模式 |
| BLOCK | 对话框阻断 + 引导操作 | 通知权限被关闭,必须跳设置页 |
具体实现时,我写了一个 ErrorDisplayHelper,根据错误码返回对应的处理策略:
dart复制class ErrorPolicy {
final bool needReport;
final bool needUserNotify;
final String? notifyMessage;
final bool needJumpSettings;
}
ErrorPolicy resolvePolicy(ErrorEvent event) {
switch (event.code) {
case 'PERMISSION_NOTIFICATION_DENIED':
return ErrorPolicy(
needReport: true,
needUserNotify: true,
notifyMessage: '通知权限已关闭,无法发送护眼提醒,请前往设置开启。',
needJumpSettings: true,
);
case 'CAMERA_INIT_FAILED':
return ErrorPolicy(
needReport: true,
needUserNotify: true,
notifyMessage: '距离检测暂不可用,已切换到定时提醒模式。',
needJumpSettings: false,
);
default:
return ErrorPolicy(needReport: true, needUserNotify: false);
}
}
这样做的目的是把“异常恢复”做进业务逻辑里,而不是让异常最终变成一句冷冰冰的“发生错误”。护眼提醒这种工具型 App,用户要的是“即使某个功能坏了,其他功能还能继续用”,这种可恢复性设计非常关键。
4. 常见问题与排查经验实录
4.1 真机上怎么抓崩溃日志
OpenHarmony 的调试工具是 hdc,不是 adb。一开始我习惯性地敲 adb logcat,结果什么日志都抓不到。正确姿势是用 hilog:
bash复制# 抓取所有日志并输出到本地文件
hdc shell hilog -z 10M > hilog.log
# 只保留包含 flutter 或 fatal 关键字的行
hdc shell "hilog | grep -E 'flutter|FATAL'"
我踩过最深的坑是:App 在 OpenHarmony 真机上冷启动必崩,但 Flutter 侧怎么都复现不了。后来把 hilog 拉下来才发现,崩溃发生在引擎初始化阶段,和 Dart 代码无关,是原生侧缺少某个依赖库。这种问题必须看原生日志才能定位,纯靠 Flutter 层的异常捕获根本没有蛛丝马迹。
所以经验是:先分清是 Dart 层异常还是引擎层崩溃。如果是引擎层,FlutterError 那套基本失灵,必须抓 hilog。建议项目里保留一条文档记录常用的 hdc 命令,团队里每个人都要会用。
4.2 MethodChannel 的坑:序列化和空返回
在 OpenHarmony 上封装插件的时候,最常见的问题就是 MethodChannel 参数类型和返回值类型不一致。比如原生侧返回的是一个 Kotlin/TS 对象,但 Flutter 侧只接收 bool,这时候会报类型转换错误。
另外一个更隐蔽的坑是:原生侧方法在未实现某个分支时,直接返回空值。Flutter 侧如果按非空处理,就会抛 Null check operator used on a null value。我在代码里已经专门做了防御:
dart复制final bool? enabled = await _channel.invokeMethod('isNotificationEnabled');
return enabled ?? true; // 空值兜底
还有一类是 MissingPluginException。这个问题的背后往往不是运行时 bug,而是工程配置问题,比如插件没有在 pubspec.yaml 里声明、原生工程没有重新构建。遇到这种情况先别急着改业务代码,先把工程配置检查一遍,往往是 low-level 问题。
4.3 通知权限第一次请求失败的典型原因
我测试时发现,第一次安装 App 后请求通知权限,经常返回失败。排查下来有三个主要原因:
module.json5里没声明通知权限。- 请求权限的 context 传递错误,拿的是应用级 context 而不是界面 context。
- 用户在弹窗上误点了“拒绝”,之后系统不再弹窗,导致永远无法授权。
第一种和第二种都好解决,声明补上、context 换对就行。第三种最麻烦,因为 App 自己无法强制触发系统弹窗,只能在界面引导用户去设置页手动开启。所以我在权限拒绝后,会先查一次 shouldShowRequestPermissionRationale,如果系统表示不能再弹窗,就跳设置页;如果还能弹,就再次发起请求。
dart复制final shouldShow = await _channel.invokeMethod('shouldShowRationale');
if (shouldShow == true) {
await _channel.invokeMethod('requestPermission');
} else {
openSettings();
}
这个细节很值得记下来,因为 OpenHarmony 的权限弹窗策略和 Android 类似,但很多开发者第一次接触时会忽略“不再询问”这种状态。
4.4 模拟器和真机行为不一致的几个场景
护眼提醒 App 在模拟器上跑得很顺畅,一到真机就各种诡异问题。我遇到的典型不一致包括:
- 模拟器没有真正的前置摄像头权限,摄像头打开会直接失败,但模拟器未必抛异常,可能只是回调卡住。
- 模拟器的系统和真机版本有差异,
reminderAgentManager在模拟器上可能不支持。 - 后台任务的调度差异非常大,模拟器锁屏后进程可能不会被清理,导致提醒异常发现不了。
所以我给团队立了一个规矩:涉及权限、提醒调度、摄像头的功能,测试用例必须标注“真机必测”,模拟器只是开发自测用,不能当验收环境。同时,在 Flutter 侧做特性检测:
dart复制class DeviceCapability {
static Future<bool> hasCamera() async { /* 调用原生能力检测 */ }
static Future<bool> hasReminderAgent() async { /* 调用原生能力检测 */ }
}
这样某些功能在模拟器上可以直接隐藏,不用跑一遍报错流程,既省时间也降低误导。
5. 实际项目中的几点额外心得
最后的最后,再啰嗦几句实际感触。
错误处理这个事,前期最容易被人忽略,因为新功能开发时永远有比“异常怎么办”更吸引人的需求。但一个护眼提醒 App 能不能在用户手机里活过第一周,靠的绝对不是 UI 有多炫,而是它在权限出问题、后台被清、摄像头被占用时,能不能保持体面。我见过太多工具类 App,一次崩溃就导致用户卸载,这个成本远比加几行错误处理逻辑高得多。
这次在 OpenHarmony 上做 Flutter 应用的经历,让我对“跨端适配”的理解也更深了一层。真正的适配不只是把界面跑起来,而是要把平台差异带来的异常行为全部考虑到。Flutter 本身能帮你屏蔽大部分 UI 差异,但底层系统能力、权限模型、后台调度这些,没有任何框架能替你兜底,必须自己一层一层去接、去试、去修。
如果你正在做类似的方向,我的建议很直接:把错误处理当成一等公民来设计,而不是开发末期补丁。先把全局捕获、统一日志、错误分级、降级状态机这四件事做好,后面的开发效率和线上稳定性都会轻松很多。
