这次调研的起因,其实是一串很自然的连锁反应:团队里有人接到一个需求,要让 Flutter 应用在将来的 OpenHarmony 设备上,也能像 Windows 桌面端那样干一些“后台常驻”的活。翻了一圈 pub.dev,发现最对口的现成方案就是 dart_windows_service_support 这个库,它能在 Dart 侧直接创建、启动、停止 Windows 服务。于是问题就变成了:这个库能不能搬上鸿蒙?
先说结论:不能直接搬,但它的价值恰恰在于“搬不动”的过程。把 dart_windows_service_support 的底层调用链扒开,再对照 OpenHarmony 的后台驻留机制,你会看清一套典型的跨端适配范式——不是 API 一一翻译,而是先把“系统级服务”降维成“应用级后台任务”,再找到平台能力的最佳映射点。这篇文章就是这次调研的完整记录,既讲清楚 Windows 服务到底怎么被 Dart 调起来的,也说清楚 OpenHarmony 后台任务该用什么姿势对接,适合正在做 Flutter 插件适配、以及想在鸿蒙上搞后台能力的开发者参考。
1. 这次调研到底在查什么
1.1 一个 Windows 服务的 Dart 库,为什么会扯上鸿蒙
dart_windows_service_support 本身并不复杂,它的定位就是给 Dart 程序一个“操作 Windows 服务”的入口。Windows 服务是系统级的东西,由 SCM(Service Control Manager,服务控制管理器)统一管理,普通应用想创建服务、修改服务状态,得绕一大圈 Windows API。这个库做的事情,就是把 OpenSCManager、CreateService、StartService、ControlService 这些 Win32 API 用 dart:ffi 封装起来,让 Dart 代码能直接调用。
这样的库在 Windows 场景下确实好用,但一旦到了 OpenHarmony 上,事情就变了。OpenHarmony 不是 Windows,它没有“系统服务”这种由操作系统直接拉起、独立于用户会话的进程模型,有的是移动端常见的 Ability 生命周期和后台任务调度。也就是说,dart_windows_service_support 所依赖的系统能力,在 OpenHarmony 上根本不存在,想要适配,就得重新理解需求本身。
我之所以觉得这个调研有代表性,是因为 dart_windows_service_support 只是“系统资源管理类”插件的一个缩影。现在 Flutter 生态里,Windows 侧还有操作注册表、计划任务、系统服务的各种插件,它们在适配 OpenHarmony 时都会遇到同样的问题:Windows 系统级机制怎么映射到移动端受限的后台运行模型。搞懂这一个库的适配思路,后面再遇到同类插件,路径都是现成的。
1.2 调研的方法和拆解方向
这次调研没有一上来就改代码,而是先做了一轮资料和源码层面的拆解,大致分了四条线:
- 读源码:把 dart_windows_service_support 的完整调用链理清楚,确认它依赖哪些 Win32 API、用了哪些 dart:ffi 绑定,以及它与 win32 包的关系。
- 跑基线:在 Windows 上实际创建、启动、停止一个临时服务,记录行为特征,方便后面和 OpenHarmony 的行为做对照。
- 翻文档:查 OpenHarmony 官方的后台任务开发指南,特别是持续任务、延迟挂起限制、以及相关权限声明。
- 搭工程:用一个最小 Flutter 插件工程测试 OpenHarmony 侧的插件绑定、MethodChannel 和 EventChannel 能力,确认通信机制是通的。
整个过程走下来,最大的感受是:跨端适配最难的不是写代码,而是建立两个平台的能力坐标系。Windows 服务能做什么、OpenHarmony 后台任务能做什么,先把这两张能力表拉出来,映射关系自然就出来了。
2. 拆开看:dart_windows_service_support 的底层机制
2.1 这个三方库是怎么“管”Windows 服务的
dart_windows_service_support 的核心机制,就是通过 dart:ffi 调用 advapi32.dll 里的一组服务管理函数。它首先用 OpenSCManager 打开服务控制管理器,拿到一个 SCM 句柄,然后调 CreateService 创建服务记录,传参包括服务名、显示名、二进制路径、服务类型、启动类型、错误控制方式等等。CreateService 成功之后,还要靠 StartService 真正把服务进程拉起来,用 QueryServiceStatus 查询当前状态,用 ControlService 停止或修改状态,最后用 DeleteService 把服务记录删掉。
这里有个细节值得注意:Windows 服务在被创建时指定的二进制路径,可以是 exe 文件,也可以是带参数的命令行。dart_windows_service_support 通常要求调用方把服务的可执行文件路径传进来,它不关心这个 exe 是不是 Dart 写的,只负责在 SCM 里登记并启动它。这种设计让“Dart 创建 Windows 服务”成了可能,因为服务本体可以用 Dart 写成 exe,而这个库只做管理端的操作。
也就是说,它的本质不是“用 Dart 实现一个服务”,而是“用 Dart 实现一个服务管理器”。这个角色差异在后面适配 OpenHarmony 时非常关键:如果你只是想要一个“后台常驻”效果,其实不需要搞系统服务,只需要一个能够在后台运行的进程或任务就够了。
2.2 Windows 系统服务与移动端后台任务的本质差异
对比 Windows 服务和 OpenHarmony 后台任务,不能只看能力清单,得看这两套机制的设计哲学。Windows 服务是给系统连续性服务的,开机就能自动拉起,崩溃了 SCM 可以按配置重启,它不受用户登录会话影响,也不需要用户打开过应用。移动端的后台任务则完全不同,它首先服务的是电池续航和用户体验,所以系统会严格控制程序在后台能跑多久、能跑什么类型。
我把两边的关键差异整理成了一个表格,方便后面的映射讨论:
| 维度 | Windows 服务 | OpenHarmony 后台任务 |
|---|---|---|
| 生命周期 | SCM 拉起,可开机自启、崩溃自动重启 | 应用内申请,系统统一调度,进程被回收后不会保证拉起 |
| 权限模型 | 需要 SYSTEM 或管理员权限,静态声明 | 需要声明后台运行权限,用户可拒绝或手动限制 |
| 用户可见性 | 无 UI,用户通常不可感知 | 必须有前台提示机制(通知栏/任务卡片) |
| 运行时长 | 没有强限制,可无限常驻 | 受续期机制约束,不同类型任务有差异,系统可强制挂起 |
| 启动方式 | 系统启动或管理员手动触发 | 必须由应用主动拉起,不能跨会话自启 |
| 资源约束 | 默认无功耗约束 | 严格受功耗、温度、资源优先级策略管控 |
这张表一出来,思路就清晰了。dart_windows_service_support 的每个操作,在 OpenHarmony 里几乎都要重新找一个“能力等价物”。CreateService 对应的是申请一个后台任务,StartService 对应的是启动某项持续能力,QueryServiceStatus 对应的是查询任务状态或者监听生命周期回调,而 DeleteService 这种“删除系统服务记录”的操作,在 OpenHarmony 里压根没有对应概念,直接丢弃即可。
3. 映射到 OpenHarmony:后台驻留机制怎么替代
3.1 OpenHarmony 的后台运行能力边界
OpenHarmony 当前主推的是 Stage 模型,应用由 UIAbility 和 ExtensionAbility 组成。早期版本里,ServiceExtensionAbility 可以承载后台服务逻辑,但后来官方路线调整,把重点转向了更细粒度的后台任务管理能力。现在如果要让应用在后台继续做实事,主要看两个方向:一个是长时任务(持续任务),另一个是延迟挂起机制。
长时任务需要先申请 ohos.permission.KEEP_BACKGROUND_RUNNING 权限,然后调用 backgroundTaskManager 相关接口,传入任务类型,比如数据传输、音视频播放、定位等,让系统知道这个应用为什么需要在后台运行。系统会根据任务类型给出对应的存活策略,并且要求应用关联一个可见的前台提醒,典型形式是通知栏任务卡片。如果应用进入后台但没有任何合法任务,系统就会逐步冻结它的 CPU 访问,最后回收进程。
延迟挂起机制则更像是“最后通牒”。应用在进入后台后,可以通过 requestSuspendDelay 申请一小段缓冲时间,用来保存状态、收尾工作、或完成用户正在感知的最后一步操作。这个机制没法无限续期,到期后系统照常挂起进程。理解了这些边界,再去映射 dart_windows_service_support 的能力,就不会出现“想要创建一个系统服务”这种错位需求。
3.2 能力映射:把系统服务降维成应用级后台任务
实际做映射的时候,遵循的是“先翻译需求,再翻译 API”的顺序。dart_windows_service_support 提供的几个核心操作,对应到 OpenHarmony 上的实现策略我整理成了下面这张表:
| dart_windows_service_support 操作 | OpenHarmony 对应方案 | 说明 |
|---|---|---|
| createService | startBackgroundRunning(长时任务) | 申请后台运行能力,携带任务类型与 WantAgent |
| startService | 启动 ExtensionAbility 或直接执行长时任务 | 取决于具体业务:播放、传输、定位等 |
| stopService | pauseBgRunning 或取消任务 | 主动释放后台资源 |
| queryStatus | 监听应用生命周期 + 任务状态回调 | 通过 EventChannel 把状态推给 Dart 侧 |
| deleteService | 无对应概念 | 直接省略 |
这里的关键认知就是“降维”两个字。Windows 服务创建的是一条系统级的常驻记录,而 OpenHarmony 的后台任务是应用内的一次性“申请-使用-释放”流程。两者最终都能让代码在后台跑起来,但前者是系统中心化管理,后者是应用主动申请、系统动态审批。明白了这个层次,你就知道为什么不能直接把 Windows API 换皮成鸿蒙 API,而必须把业务需求重新落一遍。
给一个比较典型的场景:假设业务方想在 Flutter 应用里创建一个“定时检测网络的服务”,Windows 侧的做法是拿 dart_windows_service_support 注册一个系统服务,让它开机自启、静默运行。到了 OpenHarmony 上,这个需求就变成两个问题:第一,能不能用长时任务,网络检测不属于典型的长时任务类型,系统不会长期放纵;第二,如果只是应用在前台期间做周期性检测,那连后台任务都不需要,直接用 Timer 加 WidgetsBindingObserver 控制前后台即可。这种“需求重写”的过程,才是适配调研里最花时间的部分。
3.3 权限与用户体感,要在设计阶段就定下来
OpenHarmony 后台任务相关的权限不是申请了就一劳永逸,用户可以在系统设置里关闭应用的后台活动权限。这就带来一个很现实的问题:哪怕代码完全按官方文档写对了,用户在设置里一关,你的后台任务照样跑不起来。所以适配时不能只想着“怎么拿到权限”,还要想清楚“权限被用户关掉以后应用要怎么表现”。
我在调研中给团队定的原则是:把后台任务当成一种可降级的能力。有权限就正常跑后台,没权限就退化为“仅前台运行”模式,并且在界面上给出提示,让用户自己决定是否去设置里打开开关。这样既符合系统合规要求,也避免用户产生“应用在背后偷跑”的负面感知。另一个体感层面的问题是通知栏常驻提示。OpenHarmony 要求长时任务关联前台提醒,这个提醒怎么设计、文案怎么写、优先级怎么定,都会直接影响用户对一个应用专业度的判断。
4. Flutter 插件的适配路径与代码骨架
4.1 平台通道层的架构:MethodChannel 加 EventChannel
适配 dart_windows_service_support 这类插件,Dart 代码侧尽量不要大改,要把功夫花在平台通道的重构上。原来的实现里,Dart 通过 FFI 直接和 Windows API 对话,到了 OpenHarmony 上,这条链路要改成 Dart 通过 MethodChannel 和 ArkTS 原生代码对话,再由 ArkTS 调用 OpenHarmony 的系统 API。这样 Dart 侧的业务代码基本保留,真正的适配落在原生这一层。
MethodChannel 用于单向的请求-响应,适合 createService、startBackgroundRunning 这类确定性的调用。EventChannel 则负责反向推送,后台任务被系统挂起、任务状态变化、电量受限等情况发生时,由原生侧主动推送事件给 Dart 层。建议在事件设计上直接用字符串标识状态,例如 started、stopped、suspend、blocked,不要塞太复杂的对象,减少解析成本。
4.2 一个可以直接落地的代码骨架
下面这个例子是我在实际调研工程里跑通的最小骨架。Dart 侧定义了一个 service 控制接口,对应原来 dart_windows_service_support 的语义:
class WindowsServiceLike { static const MethodChannel _channel = MethodChannel('dev.flutter.plugins.windows_service_support'); static const EventChannel _events = EventChannel('dev.flutter.plugins.windows_service_support/events'); static Future<bool> startBackground() async { return await _channel.invokeMethod('startBackground') == true; } static Future<bool> stopBackground() async { return await _channel.invokeMethod('stopBackground') == true; } static Stream<String> get statusStream { return _events.receiveBroadcastStream().map((event) => event.toString()); } }原生侧使用 ArkTS 编写插件,在 onAttachedToEngine 里注册 MethodChannel,并实现对应的方法。这里有一个容易踩的坑:OpenHarmony 的 Flutter 插件绑定接口与 Android、iOS 都不完全一样,不要照搬网上 Android 的代码,要使用 ohos 平台的 FlutterPluginBinding 来注册通道。
class WindowsServiceSupportPlugin implements FlutterPlugin { private methodChannel: MethodChannel | null = null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.methodChannel = new MethodChannel( binding.getBinaryMessenger(), 'dev.flutter.plugins.windows_service_support' ); this.methodChannel.setMethodCallHandler((call) => this.handleMethod(call)); } private async handleMethod(call: MethodCall): Promise<Object | undefined> { switch (call.method) { case 'startBackground': return this.startBackgroundTask(); case 'stopBackground': return this.stopBackgroundTask(); default: return null; } } }在这个骨架里,startBackgroundTask 内部再去调用 @ohos.backgroundTaskManager 的长时任务接口,或者根据业务类型拉起对应的 ExtensionAbility。事件通道那边,需要在原生侧拿到 EventSink,然后在后台任务状态变更时调用。
4.3 服务生命周期与前后台切换的处理
后台驻留机制里最容易出问题的不是“怎么申请”,而是“什么时候该申请、什么时候该释放”。应用还在前台显示时,你申请后台任务是没有意义的;应用退到后台以后,再临时申请可能已经错过了最好的时机。我建议的节奏是:在前台拿到用户明确的业务意图后,立刻申请长时任务;在应用回到前台或任务不再需要时,主动调用停止接口,把资源让出来。
同时,Dart 侧要监听前后台切换。可以用 WidgetsBindingObserver,也可以让原生侧通过生命周期回调通知 Dart。我的做法是两个都要,原生侧负责精确的 Application 生命周期状态,Dart 侧负责业务兜底,比如界面上标记“后台任务运行中”,避免用户对整个机制没有感知。还有一个细节:OpenHarmony 的 Flutter engine 在后台存活时可能会有资源冻结,EventChannel 事件不一定能及时送达 Dart,所以关键状态要同时缓存到本地,等应用回到前台后重新同步。
5. 常见问题与排查实录
5.1 我踩过的几个坑
第一个坑是权限声明遗漏。OpenHarmony 的权限不是写进 AndroidManifest 那种方式,而是在 module.json5 里声明,开始时忘了加 KEEP_BACKGROUND_RUNNING,后台任务接口调用直接报错,排查了半天才发现是权限根本没注册。
第二个坑是旧接口的废弃问题。早期版本里 ServiceExtensionAbility 是后台任务的主流实现,但现在官方文档已经把它标记为不推荐,一些示例代码还停留在旧写法上,照抄过去就会在编译期看到警告,运行行为也可能不符合预期。适配时一定要以当前发布版本的官方文档为准,别拿旧示例硬套。
第三个坑是 EventChannel 的事件泄漏。原生侧 EventSink 在插件 detach 时没有置空,重复注册通道导致 Dart 侧收到重复事件。处理办法是在 onDetachedFromEngine 里把 EventSink 置空,并判断绑定状态再发送事件。
第四个坑是模拟器和真机的行为差异。模拟器上后台任务很宽松,怎么跑都不挂;真机上系统回收策略严格,长时任务也会被冻结。所以验证后台驻留逻辑一定要上真机,别迷信模拟器结果。
5.2 排查工具与命令速查
后台类问题不像普通 UI 问题那么直观,代码里一堆回调,状态到底跑到哪一步了,只能靠日志和工具定位。整理一份速查,方便后面复用:
| 场景 | 命令或工具 | 说明 |
|---|---|---|
| 查看应用进程是否存活 | hdc shell ps -ef|grep 包名 | 确认进程有没有被系统回收 |
| 查看 Ability 状态 | hdc shell aa dump -l | 检查相关 UIAbility / ExtensionAbility 是否存在 |
| 查看系统日志 | hilog|grep 包名 | 关注后台任务相关关键字,例如 backgroundTask、suspend |
| 查看后台任务注册情况 | DevEco Studio 的 Background Task Inspector | 可视化查看任务类型与运行时长 |
| 抓崩溃现场 | hdc shell hilog -b crash | 崩溃日志单独过滤 |
实际排查时,最常用的是“进程是否还活着”和“日志里有没有挂起关键字”。如果进程还在,说明只是业务逻辑没跑,问题大概率在 Dart 层或通道层;如果进程没了,那就是系统回收问题,得看是不是长时任务申请失败、任务类型是否匹配、权限是否被关。
5.3 适配决策的几个建议
适配完这个库之后,我沉淀了几个通用判断标准,做同类插件适配时可以拿来直接用:
- 先判断原插件是“系统级操作”还是“应用级操作”。系统级操作在 OpenHarmony 基本找不到直接等价物,应用级操作则多半能映射。
- 后台任务申请不是多多益善,能用前台能力就用前台能力,后台任务是要消耗系统信任的。
- 平台通道的接口设计要面向抽象能力,不要面向平台 API。比如接口定义成 startBackground 而不是 createWindowsService,后续换平台就不用改 Dart 层。
- 用户设置里的“允许后台活动”开关必须在设计里留入口,不要等到用户反馈“后台跑不了”再去被动处理。
6. 这次调研留下的一些思考
真实跑完整个调研,我最深的感受是:跨端适配项目的难点,从来不是某一个 API 怎么调用,而是两套平台机制在根子上的设计差异。Windows 服务是为“系统可靠性”设计的,OpenHarmony 的后台任务是为“用户体验与设备续航”设计的,这两种取向直接决定了能力边界。所以在动手之前把机制层面吃透,比写多少行适配代码都重要。
另外有一点很实际:Flutter 插件在 OpenHarmony 生态里的适配,现在还在快速演进期,官方接口变动频率不算低。建议适配时把原生层和 Dart 层完全解耦,接口对着抽象定义,平台实现各自维护。就算将来 OpenHarmony 的后台策略调整,Dart 侧的业务代码也不需要跟着一起动。这个库只是起点,顺着这套“机制对应机制、权限对应权限、体验对应体验”的适配框架,后面做再多的跨端插件调研,都会从容很多。