1. 为什么要在鸿蒙上折腾 screen_protector做过金融、医疗、企业办公类 App 的兄弟应该都有体会防截屏、防录屏、防后台预览这三件事几乎是安全合规的硬指标。Android 侧有FLAG_SECUREiOS 侧有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 / protectDataLeakApp 切到后台时用一张图片盖住当前界面防止任务切换器截图泄露。这个依赖 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 还是 EventChannelscreen_protector原生和 Dart 的通信分两种命令式调用开启/关闭防截屏用MethodChannelDart 发指令原生执行后返回结果。事件式回调截屏发生、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要设成harHarmony ArchivesrcEntry要指向插件的入口。配错了插件不会报错就是静默不加载排查起来很折磨。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): Promisevoid { 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): Promisevoid { 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): Promisevoid { 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部分设备需权限10setWindowPrivacyMode行为稳定11 及以上setWindowPrivacyMode推荐支持动态切换实际写的时候用canIUse或者版本判断做分支别硬编码。另外setWindowPrivacyMode是异步的连续快速调用比如用户狂点开关可能有时序问题建议加个状态锁等上一次调用完成再执行下一次。4.6 真机验证与效果确认验证分三步防截屏开启后按截屏键系统提示当前界面禁止截屏或者截出来是黑屏。截屏监听开启监听后截屏Dart 层回调能收到事件。后台遮罩切到后台任务切换器里看到的是遮罩图而不是真实内容。我实测下来防截屏和后台遮罩在鸿蒙真机上表现稳定截屏监听的触发时机比 Android 稍慢一点点大概几十毫秒但可接受。如果对实时性要求极高得考虑原生侧直接处理不走 Dart 回调。5. 常见问题与排查技巧实录5.1 调用无响应返回 MissingPluginException这是最高频的问题。原因通常是插件没注册成功或者通道名对不上。排查顺序确认ohos/目录结构完整module.json5配置正确。确认configureFlutterEngine里add了插件实例。确认通道名和 Dart 层完全一致包括大小写。确认 Flutter 构建时真的把 ohos 平台编进去了看构建日志。我踩过一次通道名 Dart 侧是screen_protectorArkTS 侧手滑写成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 和 srcEntry5.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 方法比如就返回一个字符串把通道打通再往里填真实逻辑。这样能把通道问题和业务问题分开排查效率高很多。