ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flutter鸿蒙适配:screen_protector防截屏插件开发实战

Flutter鸿蒙适配:screen_protector防截屏插件开发实战 1. 为什么要在鸿蒙上折腾 screen_protector做过金融、医疗或者企业内部应用的朋友应该都有体会防截屏这个需求看起来不起眼但真到了合规审查那一步它就是硬指标。我去年接手一个保险类的 Flutter 项目客户明确要求保单详情页、理赔材料上传页必须禁止截屏和录屏同时应用切到后台时要把界面内容遮住防止在任务切换器里被偷看。当时第一反应就是找现成的插件screen_protector这个包在 Android 和 iOS 上口碑一直不错API 简单ScreenProtector.preventScreenshotOn()一行就能搞定。问题出在鸿蒙上。项目要适配 OpenHarmonyFlutter 的鸿蒙分支虽然已经能跑起来大部分插件但screen_protector并没有官方的鸿蒙实现。你去 pub.dev 上看它的平台支持列表里只有 android 和 ios鸿蒙这边是空白的。这意味着什么意味着你在鸿蒙设备上调用preventScreenshotOn()它要么静默失败要么直接抛MissingPluginException反正就是不起作用。对于一个要过合规审查的项目来说这是致命的。所以这篇文章要聊的就是怎么把screen_protector这个插件从零适配到 OpenHarmony 上。我会把整个适配过程拆开揉碎从鸿蒙侧的窗口防截屏机制讲起到 Flutter 插件的 MethodChannel 桥接怎么写再到实际调试时踩过的坑全部摊开来说。适合两类人看一是正在做 Flutter 鸿蒙化适配的开发者二是想了解鸿蒙原生能力怎么和 Flutter 插件体系对接的技术人。哪怕你之前没写过鸿蒙的 ArkTS 代码跟着思路走也能明白整个链路的运作方式。2. 先搞清楚鸿蒙侧到底提供了什么能力2.1 窗口级防截屏的核心 API鸿蒙在防截屏这件事上给的方案比 Android 要直接。Android 那边是给 Window 设置FLAG_SECURE鸿蒙这边是在窗口层面调用setWindowPrivacyMode。这个 API 挂在window模块下属于系统能力ohos.window的一部分。具体来说你需要先拿到当前应用的主窗口实例然后调用它的setWindowPrivacyMode(isPrivacyMode: boolean)方法。传true就是开启隐私模式此时系统会禁止截屏、录屏同时在多任务界面里该应用的缩略图会变成空白或者模糊状态。传false就是关闭。这个 API 从 API 9 开始提供目前主流的鸿蒙设备都支持。有一点要注意setWindowPrivacyMode是异步方法返回 Promise。你在调用的时候必须处理它的异步结果不能调完就不管了。我见过有人在 ArkTS 侧直接同步调用然后返回结果给 Flutter结果 Flutter 那边拿到的是 undefined排查了半天才发现是 Promise 没 await。2.2 隐私模式的生效范围与限制隐私模式不是全局开关它是针对窗口的。一个应用可能有多个窗口比如主窗口、悬浮窗、子窗口你需要明确对哪个窗口开启。对于 Flutter 应用来说通常只有一个主窗口所以直接拿window.getLastWindow()或者window.getMainWindow()就行。但这里有个坑鸿蒙的隐私模式在某些设备上对系统级截屏比如电源键音量键生效但对某些第三方录屏应用可能不完全拦截。这不是 API 的问题而是系统权限层面的差异。所以如果你的合规要求特别严格除了开启隐私模式还应该在应用层做一些额外的检测比如监听截屏事件鸿蒙提供了screenCaptureMonitor相关能力双管齐下。另外隐私模式开启后应用自身的截图功能也会被限制。如果你的应用里有“保存当前页面为图片”这种功能需要先临时关闭隐私模式截完再打开。这个切换逻辑要设计好不然用户会发现截图功能莫名其妙失效了。2.3 Flutter 鸿蒙插件的架构约定Flutter 在鸿蒙上的插件体系和 Android/iOS 类似也是通过 MethodChannel 做通信。但鸿蒙侧的插件工程结构不太一样它用的是 ArkTS 写的 HAR 包或者源码依赖。一个标准的 Flutter 鸿蒙插件通常包含这几部分Dart 侧的接口定义也就是lib/目录下的代码负责暴露 API 给 Flutter 应用调用。鸿蒙侧的 ArkTS 实现通常在ohos/目录下包含EntryAbility或者独立的Ability以及 MethodChannel 的处理器。插件的配置文件pubspec.yaml里面要声明鸿蒙平台的支持。关键点在于Flutter 鸿蒙化之后插件的注册机制和 Android 不太一样。Android 是靠GeneratedPluginRegistrant自动注册鸿蒙这边需要在EntryAbility的onCreate或者onWindowStageCreate里手动注册插件。如果你漏了这一步MethodChannel 就通不了调用任何方法都会报MissingPluginException。3. 动手改造 screen_protector 的完整流程3.1 拉取源码与工程结构梳理第一步肯定是把screen_protector的源码 clone 下来。这个包是开源的结构很清晰screen_protector/ lib/ screen_protector.dart screen_protector_method_channel.dart screen_protector_platform_interface.dart android/ ios/ pubspec.yamlDart 侧的代码基本不用大改因为它的设计本身就是平台无关的通过ScreenProtectorPlatform这个抽象类来分发到不同平台。我们需要做的是新增一个鸿蒙平台的实现并且在pubspec.yaml里声明鸿蒙支持。pubspec.yaml里要加这么一段flutter: plugin: platforms: android: package: com.example.screen_protector pluginClass: ScreenProtectorPlugin ios: pluginClass: ScreenProtectorPlugin ohos: package: com.example.screen_protector pluginClass: ScreenProtectorPlugin注意ohos这个平台标识这是 Flutter 鸿蒙分支约定的。pluginClass对应的是鸿蒙侧 ArkTS 的类名后面我们会实现它。3.2 Dart 侧的平台接口扩展Dart 侧需要新增一个MethodChannelScreenProtector的鸿蒙实现或者直接复用现有的 MethodChannel 实现。实际上screen_protector的 MethodChannel 实现是通用的它只关心 channel name 和方法名不关心底层是 Android 还是鸿蒙。所以最简单的方式是让鸿蒙平台也走同一个 MethodChannel。在screen_protector_method_channel.dart里MethodChannel的 name 是screen_protector。鸿蒙侧只要注册同名的 channel就能接收到 Dart 侧的调用。方法名分别是preventScreenshotOn、preventScreenshotOff、preventScreenRecordOn、preventScreenRecordOff、isScreenshotPrevented等。这些方法名在 Dart 侧已经定义好了鸿蒙侧照着实现就行。有一个细节要注意screen_protector在 Android 上把防截屏和防录屏合并成了一个开关因为FLAG_SECURE同时管这两件事。但鸿蒙的setWindowPrivacyMode也是同时管截屏和录屏的。所以鸿蒙侧的实现可以简单地把preventScreenshotOn和preventScreenRecordOn都映射到setWindowPrivacyMode(true)Off则映射到false。这样行为就和 Android 保持一致了。3.3 鸿蒙侧 ArkTS 插件的编写这是整个适配的核心部分。我们需要在ohos/目录下创建一个 ArkTS 类实现FlutterPlugin接口。这个接口是 Flutter 鸿蒙化框架提供的位于ohos/flutter_ohos包里。先看类的骨架import { FlutterPlugin, FlutterPluginBinding, MethodCall, MethodCallHandler, MethodChannel, MethodChannelHandler } from ohos/flutter_ohos; import window from ohos.window; export class ScreenProtectorPlugin implements FlutterPlugin, MethodCallHandler { private channel: MethodChannel | null null; private windowStage: window.WindowStage | null null; onAttachedToEngine(binding: FlutterPluginBinding): void { this.channel new MethodChannel(binding.getBinaryMessenger(), screen_protector); this.channel.setMethodCallHandler(this); } onDetachedFromEngine(binding: FlutterPluginBinding): void { if (this.channel) { this.channel.setMethodCallHandler(null); this.channel null; } } onMethodCall(call: MethodCall, result: MethodChannelHandler): void { switch (call.method) { case preventScreenshotOn: this.setPrivacyMode(true, result); break; case preventScreenshotOff: this.setPrivacyMode(false, result); break; case preventScreenRecordOn: this.setPrivacyMode(true, result); break; case preventScreenRecordOff: this.setPrivacyMode(false, result); break; case isScreenshotPrevented: this.isPrivacyModeOn(result); break; default: result.notImplemented(); break; } } private async setPrivacyMode(enable: boolean, result: MethodChannelHandler): Promisevoid { try { const win await window.getLastWindow(getContext(this)); await win.setWindowPrivacyMode(enable); result.success(true); } catch (err) { result.error(PRIVACY_MODE_ERROR, err.message, null); } } private async isPrivacyModeOn(result: MethodChannelHandler): Promisevoid { try { const win await window.getLastWindow(getContext(this)); const isOn await win.isWindowPrivacyMode(); result.success(isOn); } catch (err) { result.error(PRIVACY_MODE_ERROR, err.message, null); } } }这段代码有几个关键点需要展开说。onAttachedToEngine是插件被注册到 Flutter 引擎时调用的我们在这里创建 MethodChannel 并设置处理器。binding.getBinaryMessenger()拿到的是消息通道和 Android 侧的BinaryMessenger概念一致。setPrivacyMode方法里window.getLastWindow(getContext(this))是获取当前窗口实例。这里传的 context 是 ArkTS 的上下文在插件里可以通过getContext(this)拿到。拿到窗口后调用setWindowPrivacyMode注意要 await因为它是异步的。isWindowPrivacyMode是查询当前隐私模式状态的 API同样返回 Promise。这个 API 在部分鸿蒙版本上可能不存在如果你的目标设备 API 版本较低需要做兼容处理比如用 try-catch 包住查不到就返回一个默认值。3.4 插件注册与 EntryAbility 的配合插件写好了还得注册到 Flutter 引擎里。鸿蒙侧的 Flutter 应用通常有一个EntryAbility它继承自FlutterAbility。在EntryAbility的onCreate或者configureFlutterEngine方法里需要手动注册插件import { FlutterAbility, FlutterEngine } from ohos/flutter_ohos; import { GeneratedPluginRegistrant } from ../plugins/GeneratedPluginRegistrant; import { ScreenProtectorPlugin } from screen_protector; export default class EntryAbility extends FlutterAbility { configureFlutterEngine(flutterEngine: FlutterEngine): void { super.configureFlutterEngine(flutterEngine); GeneratedPluginRegistrant.registerWith(flutterEngine); flutterEngine.getPlugins().add(new ScreenProtectorPlugin()); } }GeneratedPluginRegistrant是 Flutter 自动生成的它会注册所有声明了鸿蒙支持的插件。但如果你不想改pubspec.yaml或者自动生成有问题也可以像上面这样手动 add。手动 add 的好处是可控坏处是每加一个插件都要改一次。这里有个容易忽略的点configureFlutterEngine的调用时机。它是在 Flutter 引擎初始化时调用的如果你在onCreate里注册插件可能会因为引擎还没准备好而失败。所以推荐在configureFlutterEngine里做注册。4. 调试过程中踩过的坑与解决方案4.1 MissingPluginException 的排查思路这是最常见的问题调用preventScreenshotOn()直接抛异常提示找不到插件。排查步骤我总结了一个顺序排查项检查方法常见原因pubspec.yaml 是否声明 ohos查看 flutter.plugin.platforms 下有没有 ohos漏写或拼写错误插件是否注册在 configureFlutterEngine 里打日志忘记 add 或 GeneratedPluginRegistrant 没生成channel name 是否一致Dart 侧和 ArkTS 侧对比大小写不一致或拼写错误方法名是否匹配对比 switch case方法名多了前缀或后缀引擎是否初始化完成在 onFlutterEngineCreated 后调用过早调用导致 channel 未建立我遇到过一次排查了半天发现是pubspec.yaml里ohos写成了ohos多了个空格YAML 解析后平台名不对导致插件根本没被识别。这种低级错误在紧张开发时特别容易犯建议用 IDE 的 YAML 校验插件。4.2 隐私模式不生效的设备差异有些鸿蒙设备上setWindowPrivacyMode(true)调用成功了返回也是 true但实际截屏还是能截到内容。这种情况通常和设备的系统版本有关。API 9 刚引入这个能力时部分设备的实现不完整尤其是早期的平板设备。我的处理方式是做版本判断和降级。如果检测到系统版本低于某个阈值就在应用层加一层遮罩。具体做法是监听应用的生命周期当应用切到后台时用一个全屏的模糊视图盖住内容。这个遮罩用 Flutter 侧实现就行不需要鸿蒙原生参与。虽然体验上不如系统级隐私模式流畅但至少能挡住任务切换器里的缩略图。另外鸿蒙的隐私模式对多窗口场景的支持也有差异。如果你的应用支持分屏或者悬浮窗隐私模式可能只对主窗口生效悬浮窗的内容还是会被截到。这种场景下要么禁用分屏要么对每个窗口单独设置隐私模式。4.3 异步调用的时序问题setWindowPrivacyMode是异步的但 Dart 侧的preventScreenshotOn()返回的是Futurevoid。如果你在 Dart 侧不 await 就直接进行下一步操作可能会遇到隐私模式还没生效就执行了敏感操作的情况。比如这样的代码就有问题ScreenProtector.preventScreenshotOn(); // 立即展示敏感内容 showSensitiveContent();正确的写法是await ScreenProtector.preventScreenshotOn(); showSensitiveContent();鸿蒙侧的result.success(true)是在await win.setWindowPrivacyMode(enable)之后调用的所以 Dart 侧 await 返回时隐私模式已经生效了。这个时序保证很重要尤其是在页面跳转的场景下如果隐私模式没生效就跳到了敏感页面中间会有一个短暂的空窗期。4.4 与 Flutter 页面生命周期的配合Flutter 的页面生命周期和鸿蒙的 Ability 生命周期不是完全对齐的。比如用户在 Flutter 侧 push 了一个新页面鸿蒙侧的窗口并没有变化隐私模式的状态是保持的。但如果你在dispose里调用了preventScreenshotOff()而新页面也需要防截屏就会出现空窗。我的建议是不要在单个页面的dispose里关闭隐私模式而是在应用级别管理。用一个计数器或者状态标记记录当前有多少个页面需要防截屏。当计数器从 0 变 1 时开启从 1 变 0 时关闭。这样多个页面切换时不会频繁开关也避免了空窗期。这个逻辑可以封装成一个 Dart 侧的ScreenProtectorManager内部维护一个int _lockCountacquire()时加一并判断是否需要开启release()时减一并判断是否需要关闭。用起来就像锁一样谁需要谁申请用完释放。5. 适配后的效果验证与测试要点5.1 功能验证清单适配完成后不能只测一个场景就完事。我整理了一个验证清单每次改完插件都跑一遍调用preventScreenshotOn()后系统截屏电源键音量下是否被拦截有没有提示“当前界面不支持截屏”。录屏应用能否录到内容录出来的画面是否是黑屏或空白。应用切到后台任务切换器里的缩略图是否被遮挡。调用preventScreenshotOff()后截屏功能是否恢复正常。多次开关切换是否有状态不同步的情况。应用被杀掉后重新启动隐私模式是否回到默认关闭状态。在分屏模式下隐私模式是否对两个窗口都生效。其中第 6 点容易被忽略。隐私模式是窗口级别的应用进程被杀后窗口销毁重新启动时是新的窗口默认不开启隐私模式。所以你的应用启动逻辑里如果默认就需要防截屏记得在启动完成后立即调用开启。5.2 性能影响评估setWindowPrivacyMode本身的开销很小就是设置一个窗口标志位不会引起重绘或者布局变化。但频繁调用可能会有性能损耗尤其是每秒调用几十次的情况。所以前面提到的计数器方案不仅解决了空窗问题也避免了频繁调用。实测下来在鸿蒙设备上连续调用 100 次setWindowPrivacyMode总耗时在 50ms 以内平均每次 0.5ms 左右。这个开销基本可以忽略。但如果你在build方法里调用那就另当别论了build可能每秒执行多次累积起来还是会有影响。所以切记不要在build里做隐私模式的开关操作。5.3 与其他插件的兼容性Flutter 鸿蒙化项目里通常不会只用一个插件。如果你同时用了flutter_secure_storage、local_auth这类涉及安全能力的插件要注意它们和screen_protector之间是否有冲突。目前我遇到的情况是local_auth在调用系统指纹验证时会短暂地把应用切到后台如果此时隐私模式是开启的验证界面的缩略图会被遮挡这是符合预期的。但有些设备上指纹验证界面本身也会被隐私模式影响导致验证界面显示异常。解决办法是在调用local_auth之前临时关闭隐私模式验证完成后再开启。这个切换逻辑要放在local_auth的调用前后用 try-finally 保证即使验证失败也能恢复隐私模式。6. 一些实操心得与后续扩展思路整个适配做下来最大的感受是鸿蒙的窗口能力比 Android 要清晰setWindowPrivacyMode这个 API 的设计很直观没有 AndroidFLAG_SECURE那么多历史包袱。但鸿蒙的 Flutter 插件生态还在完善中很多插件的鸿蒙实现需要自己动手。好在 Flutter 的插件架构足够灵活MethodChannel 这层抽象让跨平台适配的工作量可控。如果你也在做类似的适配我的建议是先把鸿蒙原生的能力摸清楚写一个最小的 ArkTS demo 验证 API 是否可用然后再往 Flutter 插件里集成。不要一上来就改插件代码那样出了问题很难定位是原生 API 的问题还是插件桥接的问题。后续如果要做更细粒度的控制比如只防截屏不防录屏鸿蒙目前没有提供分开的 API只能自己想办法。一个思路是用screenCaptureMonitor监听截屏事件截屏发生时立即清空敏感内容或者弹窗提示。但这种方式是事后补救不如系统级拦截彻底。另一个思路是在渲染层面做文章比如用XComponent或者自定义渲染让截屏拿到的画面是空白的。这个方案复杂度高适合对安全性要求极高的场景。还有一个值得关注的方向是鸿蒙 PC 版。随着鸿蒙 PC 的推进桌面端的防截屏需求也会出现。桌面端的窗口管理和移动端差异很大setWindowPrivacyMode在 PC 上是否同样有效需要实际测试。如果 PC 端不支持可能需要走系统级的屏幕捕获限制那是另一套机制了。最后分享一个小技巧在调试隐私模式时可以用鸿蒙的hdc工具抓取当前窗口的状态命令是hdc shell hidumper -s WindowManagerService -a -a输出里会包含窗口的隐私模式标志位。这样不用反复截屏测试直接看状态就行效率高很多。
返回列表