团队接到适配任务那天,我第一反应是去Flutter社区翻了一圈,发现secure_application这个包在GitHub上维护比较活跃,覆盖Android、iOS、Web、Windows等多个平台,唯独没有OpenHarmony的官方支持。这个场景很典型:银行App要上鸿蒙生态,Flutter层业务代码不想重写,但隐私保护这种原生强相关的能力绕不开系统差异。于是我们做了一个决定——在Flutter侧沿用secure_application的使用方式,在OpenHarmony侧自研适配层,顺带把五个平台的隐私保护机制完整拉通对比了一遍。这篇文章就是这次适配的完整记录,里面包含了我实际踩过的坑、验证过的方案,以及最终收敛下来的设计思路。
1. secure_application到底干了什么:一个"遮罩层+生命周期"的组合拳
1.1 不是拦截截图,而是遮挡内容
先说一个很多刚接触这个库的人容易产生的误解:secure_application不是用来"禁止截屏"的。它解决的核心问题,是应用切换到后台后在任务切换器、最近任务列表、Alt+Tab这些位置暴露敏感内容。
用户打开钱包App看了一眼余额,切出去回个微信,再回来的时候,刚才的余额数字还挂在最近任务卡片上。这个画面被同事瞄到,或者被系统自动截图,就是一次隐私泄露。secure_application的思路很简单:在应用进入后台的瞬间,往最上层盖一块"遮罩",让系统能抓到的那一帧画面里只有纯色遮罩,没有业务内容。用户回到前台时再移除遮罩,或者进入锁定状态要求重新验证。
这个方案叫"遮挡"而不是"拦截",优势在于它不需要平台级的高权限,只要拿到生命周期事件和窗口对象就能实现。代价是必须保证"遮罩动作"和"后台事件"之间的时序足够快,否则系统已经抓取到敏感帧,再补遮罩就晚了。
1.2 状态机:解锁、自动锁定、二次验证
secure_application在Flutter侧暴露的API非常精简,核心就是包裹你的App根组件:
SecureApplication( onLock: () { // 进入锁定状态时,清理敏感数据或跳转验证页 _showUnlockPage = true; }, onUnlock: () { _showUnlockPage = false; }, lockWhenInactive: true, // 应用失去焦点立即锁 lockDelay: 1000, // 或者延迟1秒后再锁,用于避免频繁切前的误触 child: MaterialApp( // 你的业务App ), )它内部维护了一个状态机,核心是三个状态:unlocked(正常展示)、locked(需要用户验证)、hidden(被遮罩遮挡)。实际运行中有两个触发维度:
- 前后台切换:应用进入后台,无论是否锁定,都立即盖上遮罩。
- 锁定计时:后台停留时间超过设定阈值,状态从"待机遮挡"升级为"锁定",回到前台时要求输入密码或指纹。
把这两个维度拆开非常重要。很多App只做了后台遮挡,没有做锁定,用户切后台超过5分钟再回来,应用还停在之前的敏感页面上,等于给捡到手机的人留了一扇门。secure_application把这套状态机内置好了,Flutter侧只需要关心业务层的锁定跳转逻辑。
1.3 Flutter层与原生层的分工
这个库在Dart侧做的事情,是通过AppLifecycleListener或WidgetsBindingObserver监听Flutter引擎的生命周期事件,然后切换Dart层的Widget树。但问题在于:Flutter的AppLifecycleState只是引擎层面的状态映射,它并不保证原生窗口已经被系统处理。
举个例子,Android上Flutter引擎收到paused状态,通常是在Activity.onPause之后,此时系统已经开始准备最近任务卡片了。如果只在Dart层切Widget,原生窗口的内容有概率被系统提前抓帧。所以成熟的应用必须同时做两层:
- 原生层:在系统生命周期回调里,立刻向窗口添加原生遮罩View,保证系统抓帧时看到的已经是遮罩。
- Flutter层:在AppLifecycle状态变化后,切换Dart层业务页面,比如弹起解锁页。
这也是secure_application这个库在Android上性能稳定、但其他平台效果参差不齐的根本原因——Android的原生通道做得扎实,桌面和Web侧就相对弱一些。适配OpenHarmony时,必须把原生层补上,不能只依赖Dart层。
2. Android与iOS:移动端隐私保护的两条技术路线
2.1 Android:FLAG_SECURE太重,用View叠加实现轻量保护
Android平台提供了一刀切的方案,窗口参数FLAG_SECURE。设置之后,系统不会在截屏、录屏、最近任务预览里显示窗口内容,也能阻止部分投屏工具捕获画面。
window.setFlags( WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE );但FLAG_SECURE的代价在实际业务中很让人头疼。第一,应用内所有区域都无法截图,测试同学报bug想截个图都截不了;第二,部分机型上开启FLAG_SECURE会禁用某些系统能力,比如Android 14之后的预测性返回动画、以及部分厂商的侧边栏、小窗能力;第三,这个开关是全局的,没法做到"只在敏感页面开启"而不影响其他页面。除非你动态地在一个页面里反复加flag、清flag,那又可能产生闪烁和时序问题。
secure_application在Android上采用的是更可控的"遮罩View叠加"方案。它通过Application.ActivityLifecycleCallbacks监听所有Activity的生命周期,在onPause或onStop时,向当前Activity的DecorView父容器中添加一个全屏不透明的纯色View,在onResume或onStart时移除。
这个方案的关键点在于View添加的层级和时机。我建议添加到WindowManager的下一个层级,而不是直接加到DecorView的addView里,因为DecorView内部可能存在Dialog、PopupWindow等兄弟窗口层级更高的场景。实际项目中我把遮罩View的LayoutParams设置成match_parent,背景色取自主题的windowBackground或启动页颜色,然后通过WindowManager.addView添加,这样系统在绘制任何上层窗口之前,遮罩就已经存在。
2.2 iOS:基于scene的快照掩码和原生窗口覆盖
iOS和Android的设计逻辑不同。iOS默认不提供类似FLAG_SECURE的公共API,但系统在应用进入后台时会做一件事:对当前UIWindow进行快照,供应用切换器展示。这个快照就是隐私泄露的根源。
iOS上有几个处理思路:
第一,UIApplicationExitsOnSuspend设为YES。App进入后台后直接退出进程,切换器里没有快照可显示。副作用明显:用户每次切换回来都要冷启动,体验极差。
第二,监听UIApplicationDidEnterBackgroundNotification或scenePhase变化,在进入后台的瞬时把窗口的rootViewController替换成一个纯色页面。系统抓快照时,抓到的已经是这个纯色页面。这种做法不会影响App进程存活,切换器里显示的是干净的遮罩画面。
第三,iOS 13之后引入的Scene生命周期,需要同时关注willResignActive和didEnterBackground。在实际测试中我发现,scenePhase从active变为inactive可能发生在控制中心下拉、来电横幅弹出、Face ID验证弹窗等场景,这些时候如果立刻上遮罩,会让体验显得很神经质。所以时序上建议:inactive阶段只做"预备信号",真正切遮罩放在background阶段,同时配合UIApplication.shared.beginBackgroundTask保证遮罩动作有足够时间完成。
另外补充一个容易被忽略的点:iOS上的键盘窗口(UIInputWindowController)层级高于正常的UIWindow。如果你在密码输入页面切后台,键盘可能残留在快照里。遮罩窗口的windowLevel要设置成高于键盘的层级,或者直接用一个UIWindow覆盖在UIWindowLevel.Alert之上。
2.3 移动端适配三个容易被忽略的细节
我在适配过程中总结了三个移动端通用的注意事项:
第一个是遮罩背景色与App启动屏保持一致。很多团队直接用白色遮罩,但App启动屏是深色主题,用户切后台再回来时,会看到一帧白屏闪烁,观感非常差。正确做法是从原生主题里读取windowBackground或启动页的背景色,然后把这个颜色同步给遮罩层。
第二个是部分国产ROM的后台机制差异。Android上Activity.onStop是确定进入后台的可靠信号,但部分ROM会在下拉通知栏时触发onStop,或者在某些激进的内存管理策略下跳过onResume直接进入onStart。如果遮罩逻辑只挂在onPause/onResume上,就可能在通知栏下拉时误触遮罩。我建议在Android上同时监听onStop/onRestart作为兜底,并设置一个短暂的延迟(比如300ms)来过滤掉这种短暂失焦。
第三个是"系统抓帧时机"的不确定性。不同Android版本中,最近任务卡片抓帧的时机并不完全一致。Android 11及之前通常是在onStop之后抓取;Android 12及之后,系统可能在任何一帧窗口内容变化时更新卡片预览。这意味着遮罩必须尽快上屏,任何Dart层的异步操作都来不及。这也是为什么我坚持要在原生层同步添加遮罩,而不是等Flutter引擎回调之后再用platform channel通知原生层。
3. Web与桌面:想被"看到"都得先过浏览器/窗口系统这一关
3.1 Web:Visibility API加CSS遮罩,纯前端也能锁
Web端没有"最近任务"的概念,但浏览器的标签页预览、任务栏悬停预览、以及操作系统层面的应用切换器,都会暴露页面最后渲染的内容。所以Web端的隐私保护逻辑依然是"切后台时给页面盖一层遮罩"。
核心API是document.visibilityState和visibilitychange事件。当用户切走标签页或最小化窗口时,visibilityState会变成hidden。注意这个事件的触发时机已经比较晚了,浏览器可能会在标签页失焦但窗口还可见时保持document.visibilityState === "visible"。因此我还会监听window.blur和window.focus作为补充,只要有任何一个信号表明"用户看不到这个页面了",就触发遮罩。
遮罩实现方式很简单,在HTML根节点上动态插入一个全屏固定定位的div:
window.addEventListener('blur', () => { const cover = document.createElement('div'); cover.style.position = 'fixed'; cover.style.inset = '0'; cover.style.zIndex = '99999'; cover.style.backgroundColor = '#f5f5f5'; document.body.appendChild(cover); }); window.addEventListener('focus', () => { document.querySelectorAll('div[data-privacy-cover]').forEach(el => el.remove()); });在Flutter Web里,要拿到DOM元素,通常需要借助dart:js_interop或者package:web。我的做法是封装一个WebPrivacyService,把上面这段JS逻辑通过script标签注入,然后在Dart层通过visibilitychange事件决定是否隐藏业务组件。
Web端这个方案有个天然缺陷:它只能防"切换器预览"和"标签页图标模糊",防不了用户在当前可见页面上直接截图。如果产品要求的是"禁止截屏",Web端基本没有可靠方案,只能做水印溯源。这点需要提前跟产品对齐预期。
3.2 Windows:任务视图截图与窗口显示属性
Windows桌面端的隐私保护和移动端有一个明显区别:Windows的"最近任务"更丰富,有Win+Tab任务视图、Alt+Tab切换器、任务栏悬停预览、甚至一些录屏软件。系统通过DWM(Desktop Window Manager)从窗口缓冲区获取缩略图,这部分内容并不完全受普通的窗口Z序控制。
我在Windows上验证了两条技术路线。
第一条是Win32 APISetWindowDisplayAffinity。传WDA_EXCLUDEFROMCAPTURE之后,窗口内容会从所有捕获API(包括PrintWindow、BitBlt、部分录屏工具)中排除,任务视图和Alt+Tab缩略图也会变成空白。这个API在不同Windows 10/11版本上行为不完全一致,部分版本上会导致Remote Desktop显示黑窗口,需要在发布前覆盖这两类场景做回归测试。
第二条路线是"失去焦点时显示一个置顶遮罩窗口"。Flutter Windows应用可以通过flutter_window拿到HWND,然后监听WM_KILLFOCUS。收到消息后,创建一个覆盖整个工作区的无边框置顶窗口,背景不透明。收到WM_SETFOCUS后再销毁遮罩窗口。
两条路线对比下来,我更推荐第二条。原因是它和控制逻辑完全解耦:遮罩窗口由原生C++层维护,Flutter侧只通过MethodChannel收到"窗口失焦/聚焦"两个事件,然后决定是否进入锁定状态。这样即使系统抓帧时机出现偏差,遮罩窗口也已经在原生层就位了。
3.3 macOS与Linux:从窗口管理器到X11/Wayland
macOS相对简单。NSWindow有一个sharingType属性,设置为NSWindowSharingNone后,窗口内容不会出现在系统的截屏、录屏和切换器缩略图中。代价和Android的FLAG_SECURE类似,会禁用用户主动截屏。如果只想做切换器保护,可以用NSWindowSharingReadOnly,允许截屏但防止窗口采集。
设置方式在Flutter macOS Runner里可以挂一个插件,在NSApplicationDelegate中拿到NSWindow后修改属性。这是在应用层做的,不需要额外权限。
Linux这边比较分裂。X11环境下,可以叠加一个窗口或者用_NET_WM_STATE_HIDDEN状态来让窗口不被任务栏预览。Wayland环境下,协议层还在演进,各桌面环境的窗口捕获规则不一致,最稳妥的方案还是"聚一个Top-Level窗口盖在业务窗口之上"。但Wayland对Top-Level窗口的定位、堆叠都有更多限制,实际适配成本比X11高不少。如果目标用户主要是Ubuntu+GNOME或KDE,建议直接做一个"极小化前自动盖窗、聚焦后移除"的逻辑,不要依赖特定协议特性。
4. OpenHarmony适配:从UIAbility生命周期到窗口隐私模式
4.1 鸿蒙化Flutter的运行框架:Flutter容器与UIAbility
OpenHarmony上的Flutter不是直接跑在系统上的。当前主流的运行方案是:通过OpenHarmony侧的Flutter容器,把Flutter engine编译为鸿蒙so库,然后用ArkTS编写一个承载页面,往里面嵌入FlutterView。应用的"原生入口"是一个UIAbility,Flutter只是UIAbility页面内部的渲染容器。
这个架构决定了适配secure_application时,原生层的工作需要拆成两部分:一部分在UIAbility的生命周期回调里处理,另一部分在承载FlutterView的ArkTS组件里处理。UIAbility负责感知应用级前后台,ArkTS组件负责实际切换窗口内容或遮罩状态。
4.2 切后台的时机:onBackground/onForeground回调怎么拿
OpenHarmony的UIAbility提供了明确的生命周期回调:onBackground()表示Ability进入后台,onForeground()表示回到前台。这两个回调对应Android的onStop/onRestart,在时序上比Flutter引擎的AppLifecycleState更可靠,是适配流程里最关键的信号源。
需要注意,OpenHarmony的页面栈模型里,UIAbility可能包含多个页面(stage模型的windowStage.loadContent)。如果应用里有多个Ability实例、或者使用了子窗口,每个窗口的前后台状态可能不同。我在实际项目中的做法是:在UIAbility的onBackground/onForeground里统一标记应用级状态,同时通过windowStage.getMainWindow()拿到主窗口实例,把前后台事件通过通道分发到Flutter侧。
在Flutter侧,我建了一个统一的PrivacyChannel,原生层往Dart层发两个事件:onBackground和onForeground。Dart层收到后,直接触发secure_application的锁定/解锁逻辑。这样Flutter业务代码不用感知平台差异,和Android/iOS的使用方式保持一致。
4.3 窗口层遮罩的落地:ArkTS原生窗口覆盖
拿到生命周期之后,最核心的问题就是"遮罩层怎么加"。OpenHarmony上有两条路:
第一条路是设置窗口实例的隐私模式。OpenHarmony的窗口对象提供窗口隐私模式开关,开启后窗口内容不会出现在系统截屏、录屏和最近的卡片快照中。这个能力在语义上最接近Android的FLAG_SECURE,但实现细节存在版本差异,API名称在不同OpenHarmony版本中可能不同。我建议看你当前SDK里Window类的接口文档,找到setPrivacyMode或类似能力。这个方案优点是省事,缺点和FLAG_SECURE一样:开了之后整个应用窗口都无法截屏,影响测试环节。
第二条路是ArkTS层手动添加遮罩。在UIAbility的onBackground回调里,往承载页面的组件树根部插入一个全屏Stack,盖住FlutterView:
@Entry @Component struct FlutterPage { @State isBackground: boolean = false; build() { Stack() { // Flutter容器 FlutterView() .width('100%') .height('100%') // 遮罩层 if (this.isBackground) { Column() { Text('内容已隐藏') .fontSize(16) .fontColor(Color.Gray) } .width('100%') .height('100%') .backgroundColor('#F5F5F5') } } } onBackground() { this.isBackground = true; } onForeground() { this.isBackground = false; } }我最终选择的是"隐私模式开关 + ArkTS遮罩"双保险:隐私模式保证系统侧抓帧永远不会出现敏感内容,ArkTS遮罩保证用户回到前台时有一个明确的"锁定提示页"可以承接验证流程。
4.4 不加原生代码的替代思路:Flutter层自绘遮罩
如果原生侧无法快速配合,还有一个纯Dart的替代方案:在Flutter的AppLifecycleListener回调中,当状态变为hidden或paused时,把业务Widget树切换为遮罩组件。
AppLifecycleListener( onHide: () { setState(() { _obscured = true; }); }, onShow: () { setState(() { _obscured = false; }); }, )这个方案的问题在于时机不可控。Flutter引擎的onHide事件源自主机平台的窗口焦点变化,中间隔了引擎桥接层,原生窗口内容被抓帧时,Dart层的遮罩可能还没渲染完成。我实测在部分OpenHarmony设备上,切后台后最近任务卡片仍可能暴露前一帧的业务画面。所以这个方案只能作为兜底,不能作为主方案。
5. 五平台隐私保护机制横向对比
5.1 机制差异对比表
整套适配做完后,我把五个平台的机制拉了一张对比表,方便你从全局理解各自的设计哲学:
| 对比维度 | Android | iOS | Web | Windows | OpenHarmony |
|---|---|---|---|---|---|
| 后台事件来源 | onPause/onStop | didEnterBackground/scenePhase | visibilitychange/blur | WM_KILLFOCUS | UIAbility.onBackground |
| 系统级快照保护 | FLAG_SECURE | 快照掩码/NSWindowSharingNone | 无 | SetWindowDisplayAffinity | 窗口隐私模式 |
| 遮罩方案 | WindowManager加View | 覆盖UIWindow | 插入固定定位div | 创建置顶遮罩窗口 | ArkTS组件插入遮罩 |
| 自动锁定粒度 | 精确到秒级 | 精确到秒级,受后台时间限制 | 页面级 | 精确到秒级 | 依赖Ability生命周期时机 |
| 生物识别解锁 | BiometricPrompt | LocalAuthentication | WebAuthn | Windows Hello | UserAuth能力 |
| 原生开发依赖 | 中 | 中 | 无 | 高 | 高 |
| 方案侵入性 | 较低 | 较低 | 低 | 较高 | 取决于隐私模式接口版本 |
这张表里最直观的结论是:Linux/Windows这类桌面平台,方案侵入性普遍偏高,因为"窗口"本身就是系统级的资源,你没有太多上层抽象可以用。而Web平台虽然实现成本最低,但防护强度也最弱——系统级截屏根本拦不住。
5.2 适配成本与风险等级
结合我这次的实际投入,给一个成本评估供参考:
- Android:一次性适配成本最低,secure_application已经封装好Activity生命周期监听,Clion里拉代码就能跑。风险点主要是国产ROM差异。
- iOS:适配成本中等。需要处理Scene生命周期和系统后台任务时间窗口,且App Store审核时如果发现误用后台任务,可能被打回。
- Web:开发简单但验证成本高,因为浏览器行为碎片化,Safari对blur事件的触发时机和Chrome不一致。风险点在于该平台防护强度天然弱,需要产品确认预期。
- Windows:适配成本最高。Win32 API和DWM交互的边界情况很多,我在Win11上就遇到过分屏场景下遮罩窗口没有正确覆盖另一个屏幕的问题。
- OpenHarmony:本身生命周期模型清晰,适配成本在于生态工具不成熟,比如DevEco Studio的模拟器对窗口隐私模式支持不完整,必须真机验证。
如果把风险按"手机端被借看"" 桌面端被偷拍"“录屏软件抓取”这几种威胁建模,我的建议是:移动端用遮罩+锁定双保险,桌面端优先用系统API把窗口缓冲区保护起来,Web端做到页面内遮挡就足够了。
5.3 锁定时长的策略设计
secure_application这种库使用上最容易翻车的其实是"锁定策略"。锁定过于激进,用户切出去回个消息都要重新验证,体感很差;锁定过于宽松,又等于没保护。我最终采用的策略如下:
- 切后台时间小于5秒:回到前台时只移除遮罩,不弹验证页。
- 切后台时间在5秒到2分钟之间:回到前台时弹"继续会话"确认页,不要求生物识别。
- 切后台时间超过2分钟:回到前台时强制验证,验证通过后恢复业务页面。
这个阈值不是拍脑袋定的,而是根据业务场景里"临时回复消息"和"长时间离开工位"两种行为的分布来定的。如果是金融类App,可以把阈值收紧到30秒;如果是工具类App,可以放宽到5分钟。
值得注意的是,锁定时长判断不能只依赖Flutter侧记录时间戳。原生层切后台和回前台的事件可能因为系统调度原因有延迟,如果两条事件的到达顺序乱了,Dart侧算出的"后台停留时长"可能为负。我会在原生层也记录一个时间戳,Dart层以两个时间戳的最大值为准。
6. 实测中踩过的坑
6.1 遮罩闪烁:切换后台动画没结束时遮罩就移除
第一个坑出现在Android和OpenHarmony上,现象是"切后台再切回来时,页面会闪一下敏感内容"。
排查链路是这样的:起初我以为遮罩移除太早,但查看日志后发现Dart层的onShow事件和遮罩移除几乎同时发生,而原生层动画还在收尾。系统先在后台恢复了窗口内容,然后再播放恢复动画,用户在这中间看到了一帧未遮罩的画面。
解法是把"隐藏遮罩"和"业务内容恢复"拆成两步:先移除原生遮罩,但让Dart层业务页面保持模糊或纯色,等WidgetsBinding.addPostFrameCallback的下一帧渲染完成后再恢复真实内容。在OpenHarmony上,我还给ArkTS遮罩组件的移除加了一个animateTo延时,确保动画帧完成后再销毁遮罩节点。
6.2 生命周期事件丢失:Flutter引擎与系统前后台不同步
这个坑在OpenHarmony第3代API版本上非常明显。具体表现是:UIAbility已经触发了onBackground,但Flutter引擎的AppLifecycleState仍然停留在resumed,导致Dart层完全没有收到"已进入后台"的事件。
原因是Flutter引擎在OpenHarmony上的生命周期映射走的是独立通道,和UIAbility的生命周期回调不是强绑定关系。某些场景下引擎的bridge初始化较晚,UIAbility先进入了后台,引擎侧才注册监听,中间这段事件就丢了。
解法生命周期回调不走Flutter引擎,而是通过MethodChannel主动推送到Dart层。我在UIAbility里把onBackground/onForeground写死为原生事件源,使用EventChannel主动下发。这样即使Flutter引擎的AppLifecycleState不准确,Dart层的SecureApplication也能收到可靠的信号。
6.3 输入法弹窗导致的遮罩误判
这个坑主要出现在Web端和Windows端。Web端我一开始同时监听了blur事件,结果用户点击浏览器里的DevTools,整个页面切后台,遮罩直接盖上来,调试过程极其痛苦。Windows端更离谱,某些输入法候选窗口弹出时会让主窗口失焦,WM_KILLFOCUS被触发,原生遮罩窗口瞬间盖住整个应用。
针对输入法场景,我加了过滤规则:收到失焦信号后不立即上遮罩,而是先走一个300ms的延迟。如果300ms内收到"重新聚焦"事件(比如输入法候选窗关闭后焦点回到主窗口),就取消遮罩。如果300ms内没有恢复聚焦,再触发真正的后台保护。
这个方式的代价是系统抓帧可能在这一小段窗口期内抓走一帧画面。但我实测下来,浏览器标签页切换的抓帧时机一般不会精确到300ms内,Android和OpenHarmony也可以通过这个延迟过滤掉大量误触场景。如果产品要求极高安全性,可以把延迟降到0,代价就是误触率上升,需要产品接受。
6.4 在OpenHarmony上的测试:DevEco Studio模拟器与真机差异
最后一定要单独说OpenHarmony的测试环境。DevEco Studio的模拟器在常规UI开发和Flutter容器调试上已经比较成熟,但在"隐私模式"和"最近任务卡片"这两块,模拟器行为与真机差异很大。
模拟器上开启窗口隐私模式后,切后台再回来看卡片,可能仍然是正常的缩略图,因为模拟器的图形栈没有完整模拟DWM的抓帧流程。但真机上,隐私模式的效果是可以直观看到卡片变空白。我在这个差异上吃过亏:以为代码没生效,反复查了一个下午,最后拿到真机上验证才发现功能是好的。
OpenHarmony真机上验证时,建议覆盖以下场景清单:
- 切后台后立刻切回来,观察最近任务卡片是否显示敏感内容。
- 切后台停留10秒以上再切回,验证自动锁定和验证页逻辑。
- 在最近任务列表里左右滑动,切换应用,确认遮罩层没有被系统提前销毁。
- 横屏/竖屏切换、分屏模式下前后台切换,确认遮罩层和窗口尺寸适配正常。
- 使用系统录屏功能录一段完整流程,确认录屏内容中敏感页面始终被遮罩覆盖。
6.5 一个收敛成功的设计:统一PrivacyChannel
适配完OpenHarmony后,我把五平台的生命周期事件统一成了一个PrivacyChannel设计,Dart层只依赖两个事件:onBackground和onForeground,外加一个可选的onLockRequired。原生层各自实现事件源映射,Dart层通过secure_application的状态机来处理业务。
这个设计带来的直接好处是,后续再有新平台(比如未来鸿蒙PC版)接入时,只需要在原生层做生命周期映射,Dart层完全不用动。如果再往后要做车载、手表这类形态,也只需要扩展事件源,不需要改业务代码。
我的建议是不要过度依赖当前库自带的生命周期监听,把它当做一个业务层状态机工具来用,原生层的生命周期事件单独管理。这样一旦遇到平台版本升级导致API变化,影响面可以控制在原生适配层内,而不是波及整个Flutter业务层。
这套方案上线后跑了半年,稳定性比预期好。唯一的遗憾是OpenHarmony侧的窗口隐私模式接口在部分低版本设备上拿不到完整能力,最终只能退回到ArkTS遮罩方案。如果你现在也在做类似的移植,建议先确认目标设备的系统版本,再决定是依赖系统隐私模式接口还是直接上自定义遮罩。测隐私保护功能,一定要用真机,模拟器永远发现不了键盘、分屏、悬浮窗这些边界情况。