适配仓库:https://atomgit.com/oh-flutter/screen_security
适配分支:
ohos-adaptation
这次我适配的是screen_security。它原本已经支持 Android 和 iOS,作用也很好理解:进入登录、支付、证件展示这类敏感页面后,应用可以暂时禁止常规截图和录屏;离开页面时再恢复。Android 端用的是FLAG_SECURE,iOS 端使用安全文本渲染层,但项目里没有鸿蒙实现。
我没有另起炉灶改 Dart API,而是保留原来的enable()和disable(),只在插件底层增加 OHOS 平台代码。最终结果是:关闭防护时页面可以正常被抓取,开启后系统抓到的应用区域会变成黑色;窗口服务里的isPrivacyMode也会从false变成true。这两个结果放在一起,才能说明不是按钮只改了页面文案,而是系统窗口的隐私模式真的生效了。
本文的真机证据覆盖系统截图和窗口隐私状态。OHOS 实现使用系统窗口隐私模式承接屏幕捕获防护,但本次没有单独留存系统录屏文件,因此不把“录屏已通过真机验证”写进结论。
一、最终运行效果
先看真机结果。防护关闭时,系统截图可以完整抓取 Flutter 页面:
图 1:华为畅享 90 Pro Max、HarmonyOS 7.0.0.105 上,防护关闭时可以正常抓取示例页面。
点击Enable Security后,系统抓图中的应用窗口变为黑色;状态栏仍然可见,因为它属于系统窗口,不属于当前 Flutter 应用窗口:
图 2:隐私模式开启后,系统抓图自动遮黑应用窗口内容。
为了排除黑屏或渲染失败,我又对比了同一应用窗口的系统属性。调用前后,isPrivacyMode由false变为true;调用disable()后再恢复为false。
图 3:同一窗口在调用前后发生false -> true的状态变化,关闭防护后恢复为false。
| 验证点 | 实测结果 | 证据 |
|---|---|---|
| 签名 HAP 安装与启动 | 通过,Demo 在真机前台正常运行 | 图 1 |
enable() | isPrivacyMode: false -> true,系统抓图中应用区域变黑 | 图 2、图 3 |
disable() | isPrivacyMode: true -> false,页面恢复正常抓取 | 图 1、图 3 |
| Dart 通道与异常透传 | 26 项测试通过 | 图 7 |
| 系统录屏 | 本次未单独留存录屏文件,不列为实测通过项 | 验证边界 |
二、成果速览
| 项目 | 内容 |
|---|---|
| 库名称与版本 | screen_security 1.1.2 |
| 上游基线 | TAGv1.1.2,提交1700cf3880ecc4e3c63008caa91da79c48ede78a |
| 开源许可证 | MIT |
| 适配仓库 | oh-flutter/screen_security |
| 适配分支 | ohos-adaptation,属于统一命名规范实施前创建的历史分支 |
| 适配 TAG | 尚未发布 |
| 适配提交 | 477879990d220cb517986f85a0f8e9e7fd985399,feat: add OpenHarmony screen security support |
| 新增 OHOS 能力 | 插件注册、Ability 生命周期、窗口隐私模式和权限声明 |
| 保持不变的接口 | ScreenSecurity.enable()、disable()和kidpech_screen_security通道 |
| Flutter OH 实测版本 | 3.41.10-ohos-1.0.1,核对时的最新稳定标签,不是版本号最大的预览标签 |
| 自动化验证 | flutter analyze无问题,26 项 Dart 测试通过;暂无覆盖插件核心行为的 ArkTS 自动化测试 |
| 构建验证 | unsigned HAP 构建成功 |
| 真机结论 | 截图遮黑与isPrivacyMode: false -> true -> false通过;录屏未单独留证 |
图 4:Dart API、MethodChannel、ArkTS 插件和系统窗口隐私模式之间的调用关系。
三、先确认这个库值得适配
动手之前,我先在 Flutter 鸿蒙三方库适配清单中做了去重。2026 年 9 月 7 日检查时,screen_security只出现在待适配清单里,没有出现在“适配中”和“已适配”清单;oh-flutter组织下也没有同名仓库。这一步很重要,因为功能实现完才发现别人已经提交,前面的时间基本就白花了。
原项目的 Dart 层已经把接口封装好了,调用方只需要写:
import'package:screen_security/screen_security.dart';finalscreenSecurity=ScreenSecurity();Future<void>protectSensitiveContent()async{awaitscreenSecurity.enable();}Future<void>restoreNormalCapture()async{awaitscreenSecurity.disable();}再往下看,默认实现通过名为kidpech_screen_security的MethodChannel调用原生端,方法名分别是enableScreenSecurity和disableScreenSecurity。所以鸿蒙端真正要补的是三件事:注册同名通道、接住这两个方法、把开关状态交给鸿蒙窗口 API。
四、本次实测环境
| 项目 | 本次使用情况 |
|---|---|
| 电脑 | Apple Silicon Mac,arm64 |
| Flutter OH | 3.41.10-ohos-1.0.1 |
| Dart | 3.11.5 |
| DevEco Studio | 26.0.0 Release |
| HarmonyOS SDK | API 26 |
| 测试手机 | 华为畅享 90 Pro Max,HarmonyOS 7.0.0.105 |
| 插件 | screen_security 1.1.2 |
这里需要区分“版本号最新”和“稳定版最新”。活动表推荐 Flutterv3.44.9;截至 2026 年 9 月 11 日,Flutter OH 仓库中版本号最大的标签是3.44.9+ohos-0.0.1-canary1,但它带有canary1,属于预览版本。当前最新正式稳定标签仍是3.41.10-ohos-1.0.1,也是本文实际完成静态检查、测试、HAP 构建和真机验证的版本。
因此这里不能只把表格改成3.44.9,否则版本与证据不一致。如果活动最终强制要求v3.44.9,应先用上述 canary SDK 重新完成构建和真机回归,再替换环境信息与测试证据;后续出现3.44.9正式稳定标签时也要重新验证。
五、代码仓库是怎么来的
上游基线是最新稳定版screen_security 1.1.2,TAGv1.1.2指向提交1700cf3880ecc4e3c63008caa91da79c48ede78a。该版本保持enable()和disable()两个公开接口稳定,许可证为 MIT,允许保留版权与许可声明后进行修改和再发布,因此我选择它作为适配起点,而不是更早版本或未发布代码。
我先核对待适配、适配中、已适配清单和oh-flutter组织仓库,再把保留上游历史的代码同步到 AtomGit。最终适配提交47787999的直接父提交就是上述上游基线,当前可复现入口为:
gitclone https://atomgit.com/oh-flutter/screen_security.gitcdscreen_securitygitcheckout 477879990d220cb517986f85a0f8e9e7fd985399gitrev-parse HEADgitshow-s--format='%P'这个仓库早于统一分支规范,远端实际分支是ohos-adaptation,所以本文不把它改写成并不存在的feat/ohos_screen_security_1.1.2。后续库统一使用feat/ohos_<库名>_<版本>。在干净副本中补全 OHOS 插件骨架可使用:
flutter create--template=plugin--platforms=ohos --no-pub.命令只负责生成ohos/工程骨架;通道名、窗口能力、权限和错误处理仍要按原项目接口实现。适配仓库、分支链接和可复现命令已经同时保留,后续代码图、构建图与真机图分别验证实现和结果。
六、让 Flutter 识别 OHOS 插件
第一处改动在pubspec.yaml。原来只注册了 Android 和 iOS,我增加了 OHOS 平台入口:
flutter:plugin:platforms:android:package:dev.kidpech.screen_securitypluginClass:KidpechScreenSecurityPluginios:pluginClass:KidpechScreenSecurityPluginohos:pluginClass:ScreenSecurityPlugin这里的pluginClass必须和 ArkTS 导出的类名一致。少写这一段时,Dart 代码照样能通过静态检查,但运行到鸿蒙设备后找不到插件实现,调用通道就会失败。这类问题看起来像业务方法没写对,实际是插件根本没有注册进引擎。
OHOS 插件还需要自己的工程入口。我新建了ohos/index.ets,只负责导出插件类:
importScreenSecurityPluginfrom'./src/main/ets/components/plugin/ScreenSecurityPlugin';exportdefaultScreenSecurityPlugin;同时补齐oh-package.json5、hvigorfile.ts、build-profile.json5和src/main/module.json5。这些文件看起来零碎,但职责很清楚:它们告诉 OHOS 构建系统这是一个 HAR 模块、入口在哪里、由哪个构建插件处理,以及需要什么系统权限。
七、ArkTS 端怎么接住两个方法
核心代码在ScreenSecurityPlugin.ets。这个类同时实现FlutterPlugin、MethodCallHandler和AbilityAware。
exportdefaultclassScreenSecurityPluginimplementsFlutterPlugin,MethodCallHandler,AbilityAware{privatechannel:MethodChannel|null=null;privateabilityContext:common.UIAbilityContext|null=null;onAttachedToEngine(binding:FlutterPluginBinding):void{this.channel=newMethodChannel(binding.getBinaryMessenger(),'kidpech_screen_security',);this.channel.setMethodCallHandler(this);}onAttachedToAbility(binding:AbilityPluginBinding):void{this.abilityContext=binding.getAbility().contextascommon.UIAbilityContext;}}onAttachedToEngine负责建立通道,通道名称必须和 Dart 端一字不差。onAttachedToAbility则保存当前UIAbilityContext。之所以需要这个上下文,是因为后面要通过它找到应用当前正在显示的窗口。
生命周期也不能只管“连上”,还要管“断开”。插件离开 Flutter 引擎时要取消方法处理器,离开 Ability 时要清空上下文。否则插件被重新挂载后,旧对象还可能留在内存里,问题不一定马上出现,但调试起来很麻烦。
onDetachedFromEngine(binding:FlutterPluginBinding):void{this.channel?.setMethodCallHandler(null);this.channel=null;}onDetachedFromAbility():void{this.abilityContext=null;}两个 Dart 方法来到 ArkTS 后,用一个switch分发即可:
onMethodCall(call:MethodCall,result:MethodResult):void{switch(call.method){case'enableScreenSecurity':this.setWindowPrivacyMode(true,result);break;case'disableScreenSecurity':this.setWindowPrivacyMode(false,result);break;default:result.notImplemented();break;}}这里我没有复制两套开关逻辑,而是统一交给setWindowPrivacyMode。开启和关闭只差一个布尔值,放在一起更不容易出现一边修了、另一边忘记改的情况。
八、真正打开鸿蒙窗口隐私模式
鸿蒙端的关键 API 是setWindowPrivacyMode。调用前先使用window.getLastWindow(context)拿到当前窗口:
privateasyncsetWindowPrivacyMode(enabled:boolean,result:MethodResult,):Promise<void>{constcontext=this.abilityContext;if(context===null){result.error('NO_ABILITY','UIAbility is not available',null);return;}try{constcurrentWindow=awaitwindow.getLastWindow(context);awaitcurrentWindow.setWindowPrivacyMode(enabled);result.success(null);}catch(exception){consterror=exceptionasBusinessError;result.error(error.code.toString(),'Failed to update window privacy mode',error.message,);}}这段代码里有两个容易忽略的地方。第一,Ability 还没准备好时不能硬调窗口 API,所以我先判断上下文是否为空,并把NO_ABILITY返回给 Dart。第二,系统 API 是异步调用,失败后也不能只在 ArkTS 控制台打印一行日志,否则 Flutter 页面不知道发生了什么。通过result.error把错误码和信息传回去,调用方才有机会弹提示或做降级处理。
图 5:核心实现只有一条主线:获取当前窗口,再按参数打开或关闭隐私模式。
九、权限别漏掉
只写 API 还不够,模块需要声明ohos.permission.PRIVACY_WINDOW:
{ "module": { "name": "screen_security", "type": "har", "deviceTypes": ["default", "tablet"], "requestPermissions": [ { "name": "ohos.permission.PRIVACY_WINDOW" } ] } }插件最终会以 HAR 的形式进入示例应用。构建完成后,我直接检查 HAP 内的module.json,可以看到ohos.permission.INTERNET和ohos.permission.PRIVACY_WINDOW都已经合并进去。这样比只看源码更可靠,因为源码里写了权限,不等于最终安装包里一定有。
图 6:从构建产物中检查权限,确认PRIVACY_WINDOW已进入最终模块配置。
十、补齐交付文件并完成自动化检查
除 OHOS 源码外,我还核对了README.md、CHANGELOG.md、许可证、pubspec.yaml平台声明、example/ohos/和示例截图。交付文件必须反映真实仓库状态;目标仓库没有要求的文件不为凑清单虚构,后续若按社区准入规则提交,再以当时规则补齐README.OpenSource等材料。
代码写完后,我先在插件根目录执行:
flutter analyze fluttertestflutter analyze返回No issues found,插件根目录的 Dart 测试共 26 项,全部通过。测试覆盖了通道名称、开启与关闭的方法调用、无参数调用、原生异常向 Dart 端传递,以及多次开关的调用顺序。仓库虽然带有example/ohos/entry/src/ohosTest/模板测试骨架,但它没有断言插件的窗口隐私行为,因此本文不把它计入 ArkTS 功能测试。
图 7:静态检查无报错,26 项 Dart 测试全部通过。
接着进入example构建 OHOS 调试包:
cdexample flutter build hap--debug--no-codesignHvigor 构建成功,生成了build/ohos/hap/entry-default-unsigned.hap。--no-codesign适合检查代码和工程配置能不能正常编译;真机安装仍然要使用 DevEco Studio 配置过签名的 HAP。不要把“unsigned HAP 构建成功”直接写成“真机验证完成”,这是两回事。
图 8:OHOS 示例工程完成构建,Hvigor 正常输出 unsigned HAP。
十一、Demo 通过固定提交接入
另一个应用要复用这次适配,依赖必须指向 AtomGit 上经过验证的完整提交,不能落回 pub.dev 上尚未包含 OHOS 实现的版本,也不要长期依赖会继续移动的分支:
dependencies:screen_security:git:url:https://atomgit.com/oh-flutter/screen_security.gitref:477879990d220cb517986f85a0f8e9e7fd985399执行flutter pub get后,应在pubspec.lock的resolved-ref中确认解析结果仍是477879990d220cb517986f85a0f8e9e7fd985399。分支链接适合查看最新代码,TAG 或完整 commit 才适合作为可复现依赖;当前没有适配 TAG,所以这里锁定 commit。
下面是包含导入、调用、状态展示和错误反馈的最小页面:
import'package:flutter/material.dart';import'package:screen_security/screen_security.dart';voidmain(){runApp(constMaterialApp(home:Scaffold(body:SafeArea(child:ScreenSecurityDemo()),),),);}classScreenSecurityDemoextendsStatefulWidget{constScreenSecurityDemo({super.key});@overrideState<ScreenSecurityDemo>createState()=>_ScreenSecurityDemoState();}class_ScreenSecurityDemoStateextendsState<ScreenSecurityDemo>{final_screenSecurity=ScreenSecurity();bool _enabled=false;String?_error;Future<void>_setEnabled(bool enabled)async{try{if(enabled){await_screenSecurity.enable();}else{await_screenSecurity.disable();}if(!mounted)return;setState((){_enabled=enabled;_error=null;});}catch(error){if(!mounted)return;setState(()=>_error=error.toString());}}@overrideWidgetbuild(BuildContextcontext){returnPadding(padding:constEdgeInsets.all(24),child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[Text(_enabled?'Screen security is ON':'Screen security is OFF'),if(_error!=null)Text('Error:$_error'),FilledButton(onPressed:_enabled?null:()=>_setEnabled(true),child:constText('Enable Security'),),OutlinedButton(onPressed:_enabled?()=>_setEnabled(false):null,child:constText('Disable Security'),),],),);}}这个插件在 Dart 层不创建订阅或控制器,因此没有额外对象需要dispose()。但是隐私模式属于窗口状态,退出敏感流程前仍要显式await screenSecurity.disable();不要只在无法等待异步结果的dispose()中恢复。
十二、真机上怎么判断防护真的生效
我把签名后的调试 HAP 安装到华为畅享 90 Pro Max。应用启动后,红色卡片显示Screen security is OFF,按钮文字是Enable Security。这时系统窗口信息中的isPrivacyMode为false,系统抓图可以看到完整内容,对应图 1。
点击Enable Security后,Flutter 页面内部会切换到开启状态,同时原生插件把窗口隐私模式设置为true。我用窗口服务再次查询,得到:
WindowName: flutter_oh_demo0 isPrivacyMode: true这时候再次通过系统抓图,应用内容区域已经变成黑色,对应图 2。为了避免只拿一张黑图下结论,我把关闭和开启两次窗口查询放在一起对比,图 3 证明状态变化确实落到了系统窗口层。
测试结束后,我又调用了一次disable(),确认isPrivacyMode回到false。这个收尾不能省,因为真实业务通常只需要在敏感页面临时开启。如果退出页面后没有恢复,用户在应用其他页面也无法截图,体验会很差。实际接入时可以在进入敏感流程时调用enable(),离开时在合适的生命周期里调用disable(),同时注意异常和页面跳转。
这轮没有单独留存系统录屏文件。实现采用的窗口隐私模式面向屏幕捕获保护,但当前真机结论只覆盖截图路径;录屏效果需要在目标系统版本上另行开始录制、切换开关并回看成片后才能标记为通过。
十三、提交适配分支
推送前先排除签名材料、本机 SDK 路径和构建产物,再提交实际文件:
gitstatus--shortgitdiff--checkgitadd.metadata CHANGELOG.md README.md pubspec.yaml lib ohos examplegitcommit-s-m"feat: add OpenHarmony screen security support"gitpush-uorigin ohos-adaptation截至 2026 年 9 月 10 日,远端分支可读取,HEAD 为477879990d220cb517986f85a0f8e9e7fd985399。
十四、FAQ:这次适配里最容易踩的几个坑
Q1:调用后为什么没有进入 OHOS 实现?
- 现象:
enable()或disable()没有到达 ArkTS 方法分支,调用可能表现为插件未注册或方法未实现。 - 原因:
pubspec.yaml平台入口、pluginClass、通道名或方法名没有与原接口保持一致。 - 解决方法:注册
ScreenSecurityPlugin,并逐字核对kidpech_screen_security、enableScreenSecurity和disableScreenSecurity。 - 验证结果:26 项 Dart 测试覆盖通道名称、两个方法名和调用顺序,真机开关可以改变窗口隐私状态。
Q2:为什么要返回NO_ABILITY?
- 现象:插件已经连接 Flutter 引擎,但当前还拿不到可用于查询窗口的
UIAbilityContext。 - 原因:引擎注册和 Ability 挂载是两个生命周期阶段,不能假设它们同时完成。
- 解决方法:在
onAttachedToAbility保存上下文,在调用窗口 API 前判空,并在解绑时清空。 - 验证结果:最终源码会在上下文缺失时返回明确的
NO_ABILITY;Dart 测试确认平台异常不会被吞掉。
Q3:源码声明了权限,为什么还要检查 HAP?
- 现象:源码中已经写入
ohos.permission.PRIVACY_WINDOW,但仅凭源码无法证明宿主最终获得了该声明。 - 原因:插件会先构建为 HAR,再由宿主合并配置;中间的工程配置或依赖解析可能影响最终产物。
- 解决方法:构建 HAP 后解包检查最终
module.json,不要只检查插件目录。 - 验证结果:图 6 显示
PRIVACY_WINDOW已进入本次受测 HAP。
Q4:开启后截图为什么只剩黑色?
- 现象:系统截图只保留状态栏,应用内容区域变成黑色。
- 原因:窗口隐私模式阻止系统抓图暴露受保护的应用窗口,这不是 Flutter 渲染失败。
- 解决方法:同时保留开启前页面、开启后抓图和窗口属性,避免只凭黑图下结论。
- 验证结果:图 1、图 2 和图 3 分别证明页面原本正常、抓图被遮黑以及
isPrivacyMode已开启。
Q5:能否把它当成完整的数据防泄漏方案?
- 现象:业务容易把“窗口进入隐私模式”理解成所有复制途径都被阻断。
- 原因:窗口级保护挡不住另一部手机拍摄、已被修改的设备或数据在进入受保护界面之前泄漏。
- 解决方法:把插件作为纵深防护的一层,同时保留身份认证、最小权限、敏感字段脱敏和服务端鉴权。
- 验证结果:本次只确认系统截图遮黑和窗口状态切换;录屏未单独留证,外部拍摄也不在插件能力范围内。
十五、总结
这次适配没有改动screen_security的上层用法,原有 Flutter 代码继续调用enable()和disable()。新增工作集中在 OHOS 插件注册、Ability 生命周期、MethodChannel 方法分发、窗口隐私模式调用和权限声明几个地方。
最后我用四层结果做了确认:Dart 静态检查通过、26 项自动化测试通过、OHOS HAP 构建通过、HarmonyOS API 26 真机上的窗口隐私模式可以在false和true之间切换。开启后系统抓图中的应用区域变黑,关闭后恢复正常。当前仍缺少插件核心行为的 ArkTS 自动化测试和独立录屏证据,因此这两项没有写成已经通过。
十六、参考链接
- 适配仓库
- 适配分支
- Flutter OH 版本标签
- Flutter OH 环境搭建指南
欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter