在这里插入图片描述

适配仓库: 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:隐私模式开启后,系统抓图自动遮黑应用窗口内容。

为了排除黑屏或渲染失败,我又对比了同一应用窗口的系统属性。调用前后,isPrivacyModefalse 变为 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
上游基线TAG v1.1.2,提交 1700cf3880ecc4e3c63008caa91da79c48ede78a
开源许可证MIT
适配仓库oh-flutter/screen_security
适配分支ohos-adaptation,属于统一命名规范实施前创建的历史分支
适配 TAG尚未发布
适配提交477879990d220cb517986f85a0f8e9e7fd985399feat: 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';

final screenSecurity = ScreenSecurity();

Future<void> protectSensitiveContent() async {
  await screenSecurity.enable();
}

Future<void> restoreNormalCapture() async {
  await screenSecurity.disable();
}

再往下看,默认实现通过名为 kidpech_screen_securityMethodChannel 调用原生端,方法名分别是 enableScreenSecuritydisableScreenSecurity。所以鸿蒙端真正要补的是三件事:注册同名通道、接住这两个方法、把开关状态交给鸿蒙窗口 API。

四、本次实测环境

项目本次使用情况
电脑Apple Silicon Mac,arm64
Flutter OH3.41.10-ohos-1.0.1
Dart3.11.5
DevEco Studio26.0.0 Release
HarmonyOS SDKAPI 26
测试手机华为畅享 90 Pro Max,HarmonyOS 7.0.0.105
插件screen_security 1.1.2

这里需要区分“版本号最新”和“稳定版最新”。活动表推荐 Flutter v3.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,TAG v1.1.2 指向提交 1700cf3880ecc4e3c63008caa91da79c48ede78a。该版本保持 enable()disable() 两个公开接口稳定,许可证为 MIT,允许保留版权与许可声明后进行修改和再发布,因此我选择它作为适配起点,而不是更早版本或未发布代码。

我先核对待适配、适配中、已适配清单和 oh-flutter 组织仓库,再把保留上游历史的代码同步到 AtomGit。最终适配提交 47787999 的直接父提交就是上述上游基线,当前可复现入口为:

git clone https://atomgit.com/oh-flutter/screen_security.git
cd screen_security
git checkout 477879990d220cb517986f85a0f8e9e7fd985399
git rev-parse HEAD
git show -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_security
        pluginClass: KidpechScreenSecurityPlugin
      ios:
        pluginClass: KidpechScreenSecurityPlugin
      ohos:
        pluginClass: ScreenSecurityPlugin

这里的 pluginClass 必须和 ArkTS 导出的类名一致。少写这一段时,Dart 代码照样能通过静态检查,但运行到鸿蒙设备后找不到插件实现,调用通道就会失败。这类问题看起来像业务方法没写对,实际是插件根本没有注册进引擎。

OHOS 插件还需要自己的工程入口。我新建了 ohos/index.ets,只负责导出插件类:

import ScreenSecurityPlugin from './src/main/ets/components/plugin/ScreenSecurityPlugin';

export default ScreenSecurityPlugin;

同时补齐 oh-package.json5hvigorfile.tsbuild-profile.json5src/main/module.json5。这些文件看起来零碎,但职责很清楚:它们告诉 OHOS 构建系统这是一个 HAR 模块、入口在哪里、由哪个构建插件处理,以及需要什么系统权限。

七、ArkTS 端怎么接住两个方法

核心代码在 ScreenSecurityPlugin.ets。这个类同时实现 FlutterPluginMethodCallHandlerAbilityAware

export default class ScreenSecurityPlugin
  implements FlutterPlugin, MethodCallHandler, AbilityAware {
  private channel: MethodChannel | null = null;
  private abilityContext: common.UIAbilityContext | null = null;

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(
      binding.getBinaryMessenger(),
      'kidpech_screen_security',
    );
    this.channel.setMethodCallHandler(this);
  }

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.abilityContext =
      binding.getAbility().context as common.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) 拿到当前窗口:

private async setWindowPrivacyMode(
  enabled: boolean,
  result: MethodResult,
): Promise<void> {
  const context = this.abilityContext;
  if (context === null) {
    result.error('NO_ABILITY', 'UIAbility is not available', null);
    return;
  }

  try {
    const currentWindow = await window.getLastWindow(context);
    await currentWindow.setWindowPrivacyMode(enabled);
    result.success(null);
  } catch (exception) {
    const error = exception as BusinessError;
    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.INTERNETohos.permission.PRIVACY_WINDOW 都已经合并进去。这样比只看源码更可靠,因为源码里写了权限,不等于最终安装包里一定有。

在这里插入图片描述

图 6:从构建产物中检查权限,确认 PRIVACY_WINDOW 已进入最终模块配置。

十、补齐交付文件并完成自动化检查

除 OHOS 源码外,我还核对了 README.mdCHANGELOG.md、许可证、pubspec.yaml 平台声明、example/ohos/ 和示例截图。交付文件必须反映真实仓库状态;目标仓库没有要求的文件不为凑清单虚构,后续若按社区准入规则提交,再以当时规则补齐 README.OpenSource 等材料。

代码写完后,我先在插件根目录执行:

flutter analyze
flutter test

flutter analyze 返回 No issues found,插件根目录的 Dart 测试共 26 项,全部通过。测试覆盖了通道名称、开启与关闭的方法调用、无参数调用、原生异常向 Dart 端传递,以及多次开关的调用顺序。仓库虽然带有 example/ohos/entry/src/ohosTest/ 模板测试骨架,但它没有断言插件的窗口隐私行为,因此本文不把它计入 ArkTS 功能测试。

在这里插入图片描述

图 7:静态检查无报错,26 项 Dart 测试全部通过。

接着进入 example 构建 OHOS 调试包:

cd example
flutter build hap --debug --no-codesign

Hvigor 构建成功,生成了 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.git
      ref: 477879990d220cb517986f85a0f8e9e7fd985399

执行 flutter pub get 后,应在 pubspec.lockresolved-ref 中确认解析结果仍是 477879990d220cb517986f85a0f8e9e7fd985399。分支链接适合查看最新代码,TAG 或完整 commit 才适合作为可复现依赖;当前没有适配 TAG,所以这里锁定 commit。

下面是包含导入、调用、状态展示和错误反馈的最小页面:

import 'package:flutter/material.dart';
import 'package:screen_security/screen_security.dart';

void main() {
  runApp(
    const MaterialApp(
      home: Scaffold(
        body: SafeArea(child: ScreenSecurityDemo()),
      ),
    ),
  );
}

class ScreenSecurityDemo extends StatefulWidget {
  const ScreenSecurityDemo({super.key});

  
  State<ScreenSecurityDemo> createState() => _ScreenSecurityDemoState();
}

class _ScreenSecurityDemoState extends State<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());
    }
  }

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.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: const Text('Enable Security'),
          ),
          OutlinedButton(
            onPressed: _enabled ? () => _setEnabled(false) : null,
            child: const Text('Disable Security'),
          ),
        ],
      ),
    );
  }
}

这个插件在 Dart 层不创建订阅或控制器,因此没有额外对象需要 dispose()。但是隐私模式属于窗口状态,退出敏感流程前仍要显式 await screenSecurity.disable();不要只在无法等待异步结果的 dispose() 中恢复。

十二、真机上怎么判断防护真的生效

我把签名后的调试 HAP 安装到华为畅享 90 Pro Max。应用启动后,红色卡片显示 Screen security is OFF,按钮文字是 Enable Security。这时系统窗口信息中的 isPrivacyModefalse,系统抓图可以看到完整内容,对应图 1。

点击 Enable Security 后,Flutter 页面内部会切换到开启状态,同时原生插件把窗口隐私模式设置为 true。我用窗口服务再次查询,得到:

WindowName: flutter_oh_demo0
isPrivacyMode: true

这时候再次通过系统抓图,应用内容区域已经变成黑色,对应图 2。为了避免只拿一张黑图下结论,我把关闭和开启两次窗口查询放在一起对比,图 3 证明状态变化确实落到了系统窗口层。

测试结束后,我又调用了一次 disable(),确认 isPrivacyMode 回到 false。这个收尾不能省,因为真实业务通常只需要在敏感页面临时开启。如果退出页面后没有恢复,用户在应用其他页面也无法截图,体验会很差。实际接入时可以在进入敏感流程时调用 enable(),离开时在合适的生命周期里调用 disable(),同时注意异常和页面跳转。

这轮没有单独留存系统录屏文件。实现采用的窗口隐私模式面向屏幕捕获保护,但当前真机结论只覆盖截图路径;录屏效果需要在目标系统版本上另行开始录制、切换开关并回看成片后才能标记为通过。

十三、提交适配分支

推送前先排除签名材料、本机 SDK 路径和构建产物,再提交实际文件:

git status --short
git diff --check
git add .metadata CHANGELOG.md README.md pubspec.yaml lib ohos example
git commit -s -m "feat: add OpenHarmony screen security support"
git push -u origin ohos-adaptation

截至 2026 年 9 月 10 日,远端分支可读取,HEAD 为 477879990d220cb517986f85a0f8e9e7fd985399

十四、FAQ:这次适配里最容易踩的几个坑

Q1:调用后为什么没有进入 OHOS 实现?

  • 现象: enable()disable() 没有到达 ArkTS 方法分支,调用可能表现为插件未注册或方法未实现。
  • 原因: pubspec.yaml 平台入口、pluginClass、通道名或方法名没有与原接口保持一致。
  • 解决方法: 注册 ScreenSecurityPlugin,并逐字核对 kidpech_screen_securityenableScreenSecuritydisableScreenSecurity
  • 验证结果: 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 真机上的窗口隐私模式可以在 falsetrue 之间切换。开启后系统抓图中的应用区域变黑,关闭后恢复正常。当前仍缺少插件核心行为的 ArkTS 自动化测试和独立录屏证据,因此这两项没有写成已经通过。

十六、参考链接

欢迎加入CPF-Flutter 鸿蒙社区:https://atomgit.com/CPF-Flutter

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐