适用环境:HarmonyOS Next(API 20+)|ArkTS / ArkUI|仅 Phone 真机支持

一、功能描述

打开相册翻看照片,最尴尬的场景莫过于:你正低头看手机,旁边的人悄悄凑过来瞄了一眼——工资单、聊天记录、私密照片就这样暴露了。

传统做法是手动锁屏、切后台,体验很割裂。HarmonyOS 提供了系统级的防窥保护能力,本文介绍如何把这一能力接入到一个自研相册应用里,实现:

  • 自动感知:系统通过前置摄像头 + 人脸识别判断"当前注视屏幕的是不是机主",当检测到他人正在窥屏时,实时通知应用;
  • 即时遮蔽:应用收到 HIDE 状态后,全屏蒙层盖住所有照片/视频内容,只露出一句"检测到他人窥屏,内容已隐藏";
  • 自动恢复:他人离开、机主回到屏幕前,收到 PASS 状态后自动撤掉蒙层,内容恢复可见;
  • 优雅降级:设备不支持、系统开关没开时,功能静默关闭,绝不影响正常浏览;
  • 多页面复用:照片页与视频页各自独立订阅,互不干扰。

整个能力零配置成本(用户无需在 App 内设置),且完全由系统开关控制——用户可以在"设置 > 隐私与安全 > 防窥保护"里一键开关,App 只负责"响应",不负责"开启",边界非常清晰。

请添加图片描述请添加图片描述

二、原理介绍

2.1 整体架构:系统感知 → 应用响应

防窥保护属于 Device Security Kit(设备安全服务),核心是 @kit.DeviceSecurityKit 下的 dlpAntiPeep 模块。它的分工是:

┌─────────────────────────────────────────────────┐
│  系统侧(Device Security Kit)                     │
│  前置摄像头 + 人脸 + 姿态融合                        │
│  → 判断当前是否"他人注视屏幕"                         │
│  → 产出 DlpAntiPeepStatus(HIDE / PASS)           │
└──────────────────────────┬──────────────────────┘
                           │ 事件回调 on('dlpAntiPeep')
                           ▼
┌─────────────────────────────────────────────────┐
│  应用侧(ArkUI)                                   │
│  订阅状态 → 更新 @State isPeeking                 │
│  → 条件渲染全屏遮罩层                                │
└─────────────────────────────────────────────────┘

关键点:感知逻辑完全在系统侧,App 拿不到任何原始图像/生物特征数据,只收到一个"该不该隐藏"的布尔语义状态。这既保证了隐私(数据不出设备),也保证了安全(App 无法伪造或绕过)。

2.2 核心 API 一览

API作用说明
dlpAntiPeep.isDlpAntiPeepSwitchOn()查询系统防窥开关是否打开返回 Promise<boolean>,未开启则不订阅
dlpAntiPeep.on('dlpAntiPeep', cb)订阅防窥状态变化cb: (status: DlpAntiPeepStatus) => void
dlpAntiPeep.off('dlpAntiPeep', cb)取消订阅必须在页面销毁时调用,防止野回调
DlpAntiPeepStatus.HIDE需要隐藏内容他人正在窥屏
DlpAntiPeepStatus.PASS可以显示内容当前只有机主
canIUse('SystemCapability.Security.DlpAntiPeep')设备能力探测调用前务必先检查

2.3 两个状态值的语义

防窥只给两个状态,语义非常直白:

  • HIDE(隐藏):系统判断当前屏幕被他人注视,应用应当隐藏敏感内容;
  • PASS(通过):当前是机主本人在看,应用可以正常显示内容。

⚠️ 注意:它不是"检测到人脸"就触发,而是"检测到非机主的人在窥屏"才触发 HIDE。机主自己看着是 PASS,不会误触发。

2.4 前置条件

  1. 系统开关:用户必须在系统"设置 > 隐私与安全 > 防窥保护"中开启该功能,且设备已录入人脸;
  2. 权限声明module.json5 中声明 ohos.permission.DLP_GET_HIDE_STATUS
  3. 设备能力canIUse('SystemCapability.Security.DlpAntiPeep') 为真;
  4. API 版本:API 20+,仅 Phone 形态支持。

下面进入实战。

三、实战实现

3.1 声明权限

entry/src/main/module.json5requestPermissions 中添加:

{
  "name": "ohos.permission.DLP_GET_HIDE_STATUS",
  "reason": "$string:dlp_anti_peep_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

该权限为系统授予(system_grant)+ 用户授权语义,reason 字段用于上架说明用途,when: "inuse" 表示仅在使用期间需要。

3.2 状态定义与回调

在页面组件中定义一个 @State 布尔量,并用箭头函数属性保存回调引用(on/off 必须用同一个引用,匿名函数会导致无法取消订阅):

import { dlpAntiPeep } from '@kit.DeviceSecurityKit';

@State private isPeeking: boolean = false;

// 回调必须存成成员属性,on/off 使用同一引用
private antiPeepCallback = (status: dlpAntiPeep.DlpAntiPeepStatus): void => {
  if (status === dlpAntiPeep.DlpAntiPeepStatus.HIDE) {
    this.isPeeking = true;   // 他人窥屏 → 显示遮罩
  } else {
    this.isPeeking = false;  // 机主在 → 撤掉遮罩
  }
};

⚠️ 为什么必须存成员属性?dlpAntiPeep.off('dlpAntiPeep', cb) 要求传入与 on完全相同的回调引用。如果写成 on('dlpAntiPeep', () => {...}),你手里根本没有这个引用,off 时就取消不掉,页面销毁后回调仍会被调用(此时修改已销毁组件的 @State,轻则无效,重则报错)。

3.3 启动监听:先查开关,再订阅

订阅之前先确认系统开关是打开的,避免无谓监听:

private startAntiPeep() {
  try {
    dlpAntiPeep.isDlpAntiPeepSwitchOn().then((isOpen: boolean) => {
      if (isOpen) {
        dlpAntiPeep.on('dlpAntiPeep', this.antiPeepCallback);
      }
    }).catch((err: Error) => {
      // 开关未开 / 设备不支持,静默降级
      Logger.error('[PhotoBrowserPage] 检查防窥开关失败:', err.message);
    });
  } catch (e) {
    Logger.error('[PhotoBrowserPage] 启动防窥监听失败:', e);
  }
}

调用时机放在 aboutToAppear 末尾(此时页面状态已就绪):

aboutToAppear() {
  // ... 加载数据、初始化避让区等 ...
  this.startAntiPeep();
}

3.4 解除监听:与启动对称

页面销毁时必须取消订阅,这是防窥(以及一切 on 类监听)最容易漏的一步:

aboutToDisappear() {
  try {
    dlpAntiPeep.off('dlpAntiPeep', this.antiPeepCallback);
  } catch (e) {
    Logger.error('[PhotoBrowserPage] 取消防窥监听失败:', e);
  }
  // ... 其他清理(窗口监听、分享监听等) ...
}

3.5 条件渲染遮罩层

isPeeking@State,状态一变 ArkUI 自动重算 build()。在页面 Stack 的最上层放一个条件分支:

// 防窥保护蒙层(放在 Stack 最后 = 最高层级)
if (this.isPeeking) {
  Column() {
    Image($r('app.media.ic_setting_privacy'))
      .width(48)
      .height(48)
      .fillColor('#88FFFFFF')
    Text('检测到他人窥屏,内容已隐藏')
      .fontSize(14)
      .fontColor('#CCFFFFFF')
      .margin({ top: 12 })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#E6000000')   // 90% 不透明度黑底,彻底遮住下层
  .justifyContent(FlexAlign.Center)
}

几个细节:

  1. 层级:放在 Stack 子节点的最后,天然压在所有照片/视频之上;
  2. 不透明度#E6000000(约 90% 黑)而不是纯黑,隐约透出"内容还在",避免用户误以为 App 卡死;
  3. 提示文案:明确告知"内容已隐藏"而非"出错了",减少误解;
  4. 不可点击穿透:遮罩本身没有 onClick,也不会拦截下层手势——窥屏期间用户点什么都没反应,等 PASS 后自动恢复。

3.6 设置页:状态展示 + 引导开启

除了被动响应,还在设置页做了两个增强:

展示当前系统开关状态

private async checkAntiPeepStatus(): Promise<void> {
  try {
    this.antiPeepSwitchOn = await dlpAntiPeep.isDlpAntiPeepSwitchOn();
  } catch (e) {
    this.antiPeepSwitchOn = false;
  }
}
// 开关开启时绿色对勾,未开启时黄色警示
if (this.antiPeepSwitchOn) {
  Row({ space: 4 }) {
    SymbolGlyph($r('sys.symbol.checkmark_circle')).fontSize(16).fontColor(['#34C759'])
    Text('防窥保护已开启').fontSize(14).fontColor('#34C759')
  }
} else {
  Row({ space: 4 }) {
    SymbolGlyph($r('sys.symbol.exclamationmark_triangle')).fontSize(16).fontColor(['#FF9500'])
    Text('防窥保护未开启').fontSize(14).fontColor(['#FF9500'])
  }
}

一键跳转到系统隐私设置(用户点了才跳,不主动打扰):

private async openPrivacySettings(): Promise<void> {
  try {
    const ctx = getContext(this) as common.UIAbilityContext;
    await ctx.startAbility({
      bundleName: 'com.huawei.hmos.settings',
      abilityName: 'com.huawei.hmos.settings.MainAbility',
      uri: 'privacy_settings'
    }).catch((err: Error) => {
      Logger.error('[SettingsPage] 打开隐私设置失败:', err.message);
      promptAction.showToast({ message: '无法打开系统设置' });
    });
  } catch (e) {
    promptAction.showToast({ message: '无法打开系统设置' });
  }
}

说明:API 23 起官方提供了 dlpAntiPeep.requestAntiPeepOptions(context) 可拉起系统防窥设置弹窗,体验更顺;这里用 startAbility 跳转"隐私与安全"是更通用的兜底方案,低版本也能用。

四、工程化细节

4.1 多页面复用,各自管理

本应用照片页(PhotoBrowserPage)和视频页(VideoListView)都需要防窥,采用各页面独立订阅而非全局单例:

页面订阅时机解除时机遮罩内容
PhotoBrowserPageaboutToAppearaboutToDisappear照片遮罩
VideoListViewaboutToAppearaboutToDisappear视频遮罩(同时暂停所有视频)

为什么不用全局单例?防窥遮罩是页面级 UI(蒙层盖在哪个页面上),放在各自页面里,渲染与生命周期天然对齐;全局单例反而要额外管理"当前哪个页面可见"。

4.2 降级策略清单

场景行为
canIUse 为 false(设备不支持)不订阅,功能静默关闭
isDlpAntiPeepSwitchOn() 返回 false不订阅,设置页提示"未开启"
on() 抛异常catch 住,记日志,不影响浏览
未录入人脸系统开关本身开不了,同上降级

4.3 常见坑

原因解法
取消订阅无效,销毁后仍回调回调用了匿名函数,off 拿不到同一引用回调定义为成员属性
模拟器上收不到任何事件防窥依赖前置摄像头 + 人脸识别,模拟器无此能力真机调试,开发期可忽略
遮罩一闪而过把"检测到人脸"当 HIDE 了只有 HIDE 状态才遮,PASS
设置页状态与实际不符用户从系统设置改完回来没刷新页面 onForeground 时重新 checkAntiPeepStatus()

五、总结

防窥保护的接入模式可以概括为四步,任何敏感页面都能照搬

aboutToAppear
  ↓
isDlpAntiPeepSwitchOn 查系统开关
  ↓ (开)
on('dlpAntiPeep', 成员回调) 开始监听
  ↓
HIDE → isPeeking=true → 条件渲染遮罩
PASS → isPeeking=false → 撤掉遮罩
  ↓
aboutToDisappear
  ↓
off('dlpAntiPeep', 同一回调) 停止监听
技术点实现方式关键 API
能力探测系统能力检查canIUse('SystemCapability.Security.DlpAntiPeep')
开关查询异步 PromisedlpAntiPeep.isDlpAntiPeepSwitchOn()
状态监听成员回调 + 事件订阅dlpAntiPeep.on/off('dlpAntiPeep')
内容遮蔽@State 驱动条件渲染if (this.isPeeking) 全屏蒙层
引导开启跳转系统设置startAbility / requestAntiPeepOptions

核心收获

系统感知,应用响应——生物特征数据不出设备,App 只消费一个布尔语义状态;
引用一致性——on/off 必须同一回调引用,成员属性是 ArkTS 下的标准解法;
静默降级——不支持/未开启时零影响,功能"能开则开";
页面级生命周期——遮罩与订阅都绑定页面,aboutToDisappear 必须 off


技术栈:HarmonyOS Next | ArkTS | ArkUI | DeviceSecurityKit | dlpAntiPeep
关键词:HarmonyOS | 防窥保护 | 隐私安全 | dlpAntiPeep | 人脸识别 | 状态订阅

如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流~

Logo

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

更多推荐