1. 为什么要在鸿蒙上折腾 screen_protector
做过金融、医疗、企业办公类 App 的兄弟应该都有体会,防截屏、防录屏、防后台预览这三件事,几乎是安全合规的硬指标。Android 侧有FLAG_SECURE,iOS 侧有UITextField的isSecureTextEntry那套取巧方案,Flutter 生态里把这些封装得比较顺手的就是screen_protector这个插件。它做的事情很聚焦:开启防截屏、监听截屏事件、应用切后台时用一张遮罩图盖住界面,防止系统在任务切换器里泄露内容。
问题来了,当项目要往 OpenHarmony(也就是大家常说的鸿蒙)上迁移时,这个插件默认是不认鸿蒙的。Flutter 官方对 OpenHarmony 的支持走的是社区维护的flutter_flutter分支和ohos平台通道,很多纯 Dart 逻辑能跑,但一旦涉及平台原生能力(Platform Channel),就得自己补鸿蒙侧的 ArkTS 实现。screen_protector恰好就是这种"一半 Dart、一半原生"的插件,Android 和 iOS 的实现都在,唯独缺ohos目录。
所以这篇东西要聊的,就是怎么把screen_protector适配到 OpenHarmony 上,让防截屏、防泄露这套能力在鸿蒙设备上真正跑起来。适合两类人看:一类是正在做 Flutter 鸿蒙化迁移、被平台插件卡住的开发者;另一类是想搞清楚 Flutter 插件在鸿蒙上到底怎么落地、Platform Channel 在 ArkTS 侧怎么写的人。哪怕你之前没碰过鸿蒙原生开发,跟着思路走也能把整个链路理清楚。
我先把结论摆前面:适配的核心工作量不在 Dart 层,而在鸿蒙侧的窗口管理和 ArkTS 插件注册。Dart 层几乎不用动,真正要写的是ohos目录下的 Ability 生命周期钩子、窗口隐私模式的调用,以及截屏事件的监听。下面按我实际踩过的顺序,一层层拆。
2. 适配前的整体设计与思路拆解
2.1 先搞清楚 screen_protector 到底依赖哪些原生能力
在动手之前,得先把这个插件的原生依赖摸清楚,不然适配就是盲人摸象。我把它的能力拆成三块:
- 防截屏(preventScreenshotOn):本质是给当前窗口打一个"隐私模式"标记,系统层面禁止截屏和录屏。Android 是
WindowManager.LayoutParams.FLAG_SECURE,鸿蒙对应的是窗口的隐私模式设置。 - 截屏监听(screenshotListener):当用户触发截屏时,原生层捕获这个事件并回调给 Dart。Android 靠
ContentObserver监听媒体库变化,鸿蒙侧有对应的截屏事件订阅机制。 - 后台遮罩(preventScreenshotOn / protectDataLeak):App 切到后台时,用一张图片盖住当前界面,防止任务切换器截图泄露。这个依赖 Ability 的前后台生命周期回调。
这三块能力,前两块是"系统级开关",第三块是"生命周期 + UI 覆盖"。理解了这一点,适配的边界就清楚了:我们要在鸿蒙侧提供一个能接收 Dart 调用、能操作窗口、能监听生命周期、能回调事件的 ArkTS 模块。
2.2 为什么选择补 ohos 目录而不是重写插件
有人可能会想,干脆自己写个纯 Dart 的替代方案行不行?不行。防截屏这件事必须落到系统窗口层,Dart 层没有权限碰。那为什么不 fork 一个新插件?因为screen_protector的 Dart API 已经被项目大量引用,重写意味着改一堆调用点,迁移成本高。
最经济的做法是:保持 Dart 层接口不变,在插件目录下新增ohos平台实现。Flutter 的插件机制本身就是按平台分目录的,android/、ios/、ohos/各管各的,只要在pubspec.yaml里声明好平台,Flutter 构建时会自动把对应平台的实现编进去。这样既复用了现有 Dart 逻辑,又把改动收敛在鸿蒙侧。
这里有个关键点:OpenHarmony 的 Flutter 插件目录约定和 Android/iOS 不完全一样。鸿蒙侧用的是ohos/目录,里面是标准的 HarmonyOS 工程结构(entry、oh-package.json5、module.json5等),插件注册走的是FlutterPlugin接口的鸿蒙实现。这个结构如果搞错,插件根本不会被加载。
2.3 平台通道的设计:MethodChannel 还是 EventChannel
screen_protector原生和 Dart 的通信分两种:
- 命令式调用(开启/关闭防截屏)用
MethodChannel,Dart 发指令,原生执行后返回结果。 - 事件式回调(截屏发生、App 前后台切换)用
EventChannel,原生主动往 Dart 推事件。
这个划分在鸿蒙侧要原样保留,因为 Dart 层的代码已经按这个约定写死了。我们要做的是在 ArkTS 侧建两个对应的通道,方法名、事件名必须和 Dart 层完全对齐,一个字母都不能错,否则就是"调用无响应"的经典坑。
提示:通道名(channel name)在 Dart 和原生两侧是硬约定,建议直接从 Dart 源码里把字符串抠出来,别凭记忆写。
2.4 鸿蒙窗口隐私模式的选型考量
鸿蒙的窗口隐私能力,不同 API 版本叫法和用法有差异。早期版本用的是setWindowPrivacyMode,新版本在window模块里提供了更规范的隐私模式接口。选型时要考虑两点:一是项目要兼容的最低 API 版本,二是目标设备的实际系统版本。
我的建议是以项目实际要覆盖的最低 API 版本为准,用条件判断做兼容,而不是一上来就用最新接口。因为鸿蒙设备碎片化虽然没 Android 那么夸张,但企业内部分发的设备系统版本往往偏保守,用太新的接口会直接编译不过或者运行时抛异常。
3. 核心细节解析与实操要点
3.1 插件目录结构怎么搭
先看目录。在screen_protector插件根目录下,和android/、ios/平级,新建ohos/。里面至少要有这些:
ohos/ ├── entry/ │ └── src/main/ │ ├── ets/ │ │ └── components/plugin/ │ │ └── ScreenProtectorPlugin.ets │ └── module.json5 ├── oh-package.json5 └── build-profile.json5ScreenProtectorPlugin.ets是核心,负责实现FlutterPlugin接口、注册通道、处理方法和事件。module.json5里要声明这个模块是给 Flutter 用的,oh-package.json5里要写清楚依赖。
这里最容易踩的坑是module.json5的配置。鸿蒙的模块声明和 Android 的AndroidManifest.xml思路类似但字段完全不同,type要设成har(Harmony Archive),srcEntry要指向插件的入口。配错了插件不会报错,就是静默不加载,排查起来很折磨。
3.2 FlutterPlugin 接口的鸿蒙实现要点
鸿蒙侧的 Flutter 插件要实现FlutterPlugin接口,核心是三个方法:
onAttachedToEngine:插件被挂载到 Flutter 引擎时调用,在这里创建 MethodChannel 和 EventChannel,注册方法处理器。onDetachedFromEngine:插件卸载时调用,清理通道和监听器,防止内存泄漏。getUniqueClassName:返回插件唯一类名,鸿蒙侧用来标识插件实例。
onAttachedToEngine里拿到的FlutterPluginBinding对象,能拿到binaryMessenger,这是通道通信的命脉。MethodChannel 和 EventChannel 都基于它创建。
onAttachedToEngine(binding: FlutterPluginBinding): void { this.methodChannel = new MethodChannel( binding.getBinaryMessenger(), "screen_protector" ); this.methodChannel.setMethodCallHandler({ onMethodCall: this.onMethodCall.bind(this) }); this.eventChannel = new EventChannel( binding.getBinaryMessenger(), "screen_protector/stream" ); this.eventChannel.setStreamHandler(new ScreenProtectorStreamHandler()); }注意通道名screen_protector和screen_protector/stream,这两个必须和 Dart 层一致。我见过有人把 stream 通道名写成screen_protector_stream,结果截屏监听死活不触发,查了半天。
3.3 防截屏的窗口操作细节
防截屏的核心是操作当前 Ability 的窗口。在 ArkTS 里,通过window.getLastWindow(context)拿到当前窗口,然后调用隐私模式接口。
import window from '@ohos.window'; async function setPrivacyMode(enable: boolean): Promise<void> { const context = getContext(this) as common.UIAbilityContext; const win = await window.getLastWindow(context); await win.setWindowPrivacyMode(enable); }setWindowPrivacyMode(true)开启后,系统会禁止对该窗口截屏和录屏,任务切换器里也会自动打码。这个接口是异步的,必须await,不然可能出现"调用了但没生效"的时序问题。
注意:
getLastWindow拿的是当前栈顶窗口,如果 App 有多个窗口(比如悬浮窗),要确保操作的是主窗口,否则防截屏可能只对某个子窗口生效。
3.4 截屏事件监听的实现路径
鸿蒙侧监听截屏,思路和 Android 不同。Android 是监听媒体库变化,鸿蒙更推荐用系统提供的截屏事件订阅。在 ArkTS 里可以通过@ohos.multimedia.image或者系统事件订阅机制来捕获。
实际适配时,我采用的是监听系统截屏事件 + 回调 Dart的方式。在插件初始化时注册监听,截屏发生时通过 EventChannel 往 Dart 推一个事件。Dart 层收到后触发用户注册的回调。
这里有个细节:截屏事件的回调是异步的,而且可能在 App 处于后台时触发。要确保 EventChannel 的 StreamHandler 在插件生命周期内一直有效,别在onDetachedFromEngine之前就把监听器注销了。
3.5 后台遮罩与生命周期绑定
后台遮罩依赖 Ability 的前后台回调。鸿蒙的UIAbility有onForeground和onBackground两个生命周期钩子。当 App 进入后台时,往 Flutter 界面盖一张遮罩图;回到前台时移除。
实现上有两种思路:一种是在 ArkTS 侧直接操作窗口加一层原生遮罩,另一种是通过 EventChannel 通知 Dart 层,让 Dart 用 Flutter Widget 盖一层。我选的是后者,因为 Dart 层控制遮罩样式更灵活,而且和现有screen_protector的 Dart 逻辑一致。
但这里有个坑:App 切后台时,Flutter 引擎可能被挂起,Dart 层的 UI 更新不一定及时。所以更稳妥的做法是原生侧先盖一层纯色遮罩兜底,同时通知 Dart 层,双保险。这个细节在官方文档里不会写,是实际测试时发现的——某些设备切后台瞬间截图,Dart 层还没反应过来,内容就泄露了。
4. 实操过程与核心环节实现
4.1 环境准备与依赖确认
动手前先把环境理清楚。Flutter 侧要用支持 OpenHarmony 的flutter_flutter分支,鸿蒙侧要有 DevEco Studio 和对应 SDK。版本这块我不写死具体号,因为迭代太快,你按官方仓库 README 里推荐的组合来就行。
确认三件事:
flutter doctor能识别到 ohos 工具链。- 项目里
screen_protector的版本,确认 Dart 层 API 没大改。 - 目标设备的 API 版本,决定用哪套窗口隐私接口。
我建议先在真机上跑通一个最小 Demo,别一上来就往主项目里塞。Demo 里就放一个按钮,点一下开防截屏,再点一下关,能验证通道通不通。
4.2 Dart 层需要改什么
好消息是,Dart 层基本不用改。screen_protector的 Dart 代码通过Platform.isAndroid、Platform.isIOS判断平台,鸿蒙上这两个都是 false,会走到默认分支。我们要做的是在 Dart 层加上对 ohos 的识别。
Flutter 在鸿蒙上运行时,Platform.operatingSystem返回的是ohos。所以在插件的 Dart 入口处,把平台判断补上:
if (Platform.isAndroid) { // Android 实现 } else if (Platform.isIOS) { // iOS 实现 } else if (Platform.operatingSystem == 'ohos') { // 走 MethodChannel,和 Android 共用通道逻辑 }实际上因为通道名一致,鸿蒙侧可以直接复用 Android 的 Dart 调用逻辑,只是原生实现换成 ArkTS。这一步改动量很小,但必须做,否则 Dart 层会因为找不到平台实现而抛MissingPluginException。
4.3 ArkTS 插件完整实现
核心文件ScreenProtectorPlugin.ets的结构:
import { FlutterPlugin, FlutterPluginBinding, MethodChannel, MethodCall, MethodCallHandler, EventChannel } from '@ohos/flutter_ohos'; export class ScreenProtectorPlugin implements FlutterPlugin, MethodCallHandler { private methodChannel: MethodChannel | null = null; private eventChannel: EventChannel | null = null; private privacyEnabled: boolean = false; onAttachedToEngine(binding: FlutterPluginBinding): void { this.methodChannel = new MethodChannel(binding.getBinaryMessenger(), 'screen_protector'); this.methodChannel.setMethodCallHandler(this); this.eventChannel = new EventChannel(binding.getBinaryMessenger(), 'screen_protector/stream'); this.eventChannel.setStreamHandler(new ScreenProtectorStreamHandler()); } async onMethodCall(call: MethodCall, result: MethodChannel.Result): Promise<void> { switch (call.method) { case 'preventScreenshotOn': await this.setPrivacyMode(true); result.success(true); break; case 'preventScreenshotOff': await this.setPrivacyMode(false); result.success(true); break; case 'startScreenshotListener': this.startListener(); result.success(true); break; case 'stopScreenshotListener': this.stopListener(); result.success(true); break; default: result.notImplemented(); } } private async setPrivacyMode(enable: boolean): Promise<void> { const context = getContext(this) as common.UIAbilityContext; const win = await window.getLastWindow(context); await win.setWindowPrivacyMode(enable); this.privacyEnabled = enable; } onDetachedFromEngine(binding: FlutterPluginBinding): void { this.methodChannel?.setMethodCallHandler(null); this.eventChannel?.setStreamHandler(null); this.methodChannel = null; this.eventChannel = null; } getUniqueClassName(): string { return 'ScreenProtectorPlugin'; } }方法名preventScreenshotOn、preventScreenshotOff这些,必须和 Dart 层MethodChannel.invokeMethod里传的字符串完全一致。我建议直接去 Dart 源码里搜invokeMethod,把方法名列表抄下来对照。
4.4 插件注册与 module.json5 配置
插件写完了要注册,否则 Flutter 引擎不知道它的存在。鸿蒙侧的注册在EntryAbility或者专门的插件注册文件里:
import { ScreenProtectorPlugin } from './components/plugin/ScreenProtectorPlugin'; export default class EntryAbility extends FlutterAbility { configureFlutterEngine(flutterEngine: FlutterEngine): void { super.configureFlutterEngine(flutterEngine); flutterEngine.getPlugins().add(new ScreenProtectorPlugin()); } }module.json5里要确保type是har,并且srcEntry指向正确的入口。这个文件配错的表现是:编译通过,运行时不报错,但插件方法调用返回notImplemented。排查时优先看这里。
4.5 参数与兼容性处理
窗口隐私模式在不同 API 版本上的行为有差异。我做了个兼容层:
| API 版本 | 隐私模式接口 | 备注 |
|---|---|---|
| 9 及以下 | setWindowPrivacyMode | 部分设备需权限 |
| 10 | setWindowPrivacyMode | 行为稳定 |
| 11 及以上 | setWindowPrivacyMode | 推荐,支持动态切换 |
实际写的时候用canIUse或者版本判断做分支,别硬编码。另外,setWindowPrivacyMode是异步的,连续快速调用(比如用户狂点开关)可能有时序问题,建议加个状态锁,等上一次调用完成再执行下一次。
4.6 真机验证与效果确认
验证分三步:
- 防截屏:开启后按截屏键,系统提示"当前界面禁止截屏"或者截出来是黑屏。
- 截屏监听:开启监听后截屏,Dart 层回调能收到事件。
- 后台遮罩:切到后台,任务切换器里看到的是遮罩图而不是真实内容。
我实测下来,防截屏和后台遮罩在鸿蒙真机上表现稳定,截屏监听的触发时机比 Android 稍慢一点点,大概几十毫秒,但可接受。如果对实时性要求极高,得考虑原生侧直接处理,不走 Dart 回调。
5. 常见问题与排查技巧实录
5.1 调用无响应,返回 MissingPluginException
这是最高频的问题。原因通常是插件没注册成功,或者通道名对不上。排查顺序:
- 确认
ohos/目录结构完整,module.json5配置正确。 - 确认
configureFlutterEngine里add了插件实例。 - 确认通道名和 Dart 层完全一致,包括大小写。
- 确认 Flutter 构建时真的把 ohos 平台编进去了(看构建日志)。
我踩过一次,通道名 Dart 侧是screen_protector,ArkTS 侧手滑写成screenProtector,结果就是静默失败,查了两小时。
5.2 防截屏开了但没效果
先确认setWindowPrivacyMode真的被调用了,加日志。如果调用了没效果,大概率是操作了错误的窗口。getLastWindow在某些场景下拿到的不是主窗口,要改成通过window.getTopWindow或者从 Ability 上下文直接拿主窗口。
还有一种情况是设备本身不支持隐私模式,这种只能降级处理,用后台遮罩兜底。
5.3 截屏监听不触发
鸿蒙的截屏事件订阅需要权限或者特定配置。检查module.json5里有没有声明相关权限。另外,监听器要在插件 attach 之后注册,detach 之前注销,生命周期错位会导致监听失效。
5.4 后台遮罩闪现真实内容
前面提过,Dart 层响应有延迟。解决办法是原生侧先盖一层纯色遮罩,等 Dart 层准备好再切换。这个兜底逻辑很关键,尤其是金融类 App,切后台瞬间泄露内容是要出事的。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| MissingPluginException | 插件未注册/通道名错 | 检查注册和通道名 |
| 防截屏无效 | 窗口对象错/设备不支持 | 换主窗口/降级兜底 |
| 截屏监听不触发 | 权限缺失/生命周期错位 | 补权限/调整注册时机 |
| 后台遮罩延迟 | Dart 层响应慢 | 原生侧先盖兜底遮罩 |
| 编译报错找不到模块 | module.json5 配置错 | 检查 type 和 srcEntry |
5.6 几个独家避坑心得
第一,别在 Dart 层做平台判断的硬编码。用Platform.operatingSystem == 'ohos'比用版本号判断靠谱,因为鸿蒙的版本号规则和 Android 不一样。
第二,通道通信加超时。鸿蒙侧某些接口(比如窗口操作)在极端情况下会卡住,Dart 层invokeMethod最好加超时,避免 UI 卡死。
第三,测试要覆盖冷启动。有些问题只在 App 冷启动后第一次调用时出现,热重载测不出来。我遇到过一次冷启动后防截屏失效,原因是插件注册时机早于窗口创建,加了延迟初始化才好。
第四,多设备验证。鸿蒙设备之间窗口行为有细微差异,至少测两台不同型号的真机,别只信模拟器。
6. 适配后的扩展与维护建议
适配完screen_protector只是第一步。这套 Platform Channel + ArkTS 插件的模式,可以复用到其他需要鸿蒙原生能力的 Flutter 插件上,比如文件选择、权限管理、设备信息读取。核心套路是一样的:Dart 层保持接口,鸿蒙侧补ohos/目录,实现FlutterPlugin,对齐通道名。
维护上要注意,OpenHarmony 的 Flutter 支持还在快速迭代,flutter_ohos的 API 可能变。建议把鸿蒙侧的适配代码单独抽成一个 patch 或者 fork 分支,方便后续跟着上游更新。别直接改主仓库,不然升级插件时冲突能让你怀疑人生。
另外,防截屏这类安全能力,建议在 CI 里加自动化测试用例,至少覆盖"开启后截屏被拦截"和"后台遮罩生效"两个场景。安全功能最怕的就是某次重构悄悄失效,等出事才发现。
我个人在实际项目里的体会是,鸿蒙化迁移的难点从来不是 Dart 代码,而是这些边边角角的原生适配。screen_protector这种插件看着小,但涉及窗口、生命周期、事件通道,把它的适配链路走通一遍,后面再遇到别的插件心里就有底了。最后分享一个小技巧:适配新插件时,先写一个最小可运行的 ArkTS 方法(比如就返回一个字符串),把通道打通,再往里填真实逻辑。这样能把"通道问题"和"业务问题"分开排查,效率高很多。