HarmonyOS应用《左右相册》 隐私防窥实战:用 dlpAntiPeep 让相册“感知“正在被偷瞄
适用环境: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 前置条件
- 系统开关:用户必须在系统"设置 > 隐私与安全 > 防窥保护"中开启该功能,且设备已录入人脸;
- 权限声明:
module.json5中声明ohos.permission.DLP_GET_HIDE_STATUS; - 设备能力:
canIUse('SystemCapability.Security.DlpAntiPeep')为真; - API 版本:API 20+,仅 Phone 形态支持。
下面进入实战。
三、实战实现
3.1 声明权限
在 entry/src/main/module.json5 的 requestPermissions 中添加:
{
"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)
}
几个细节:
- 层级:放在
Stack子节点的最后,天然压在所有照片/视频之上; - 不透明度:
#E6000000(约 90% 黑)而不是纯黑,隐约透出"内容还在",避免用户误以为 App 卡死; - 提示文案:明确告知"内容已隐藏"而非"出错了",减少误解;
- 不可点击穿透:遮罩本身没有
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)都需要防窥,采用各页面独立订阅而非全局单例:
| 页面 | 订阅时机 | 解除时机 | 遮罩内容 |
|---|---|---|---|
| PhotoBrowserPage | aboutToAppear | aboutToDisappear | 照片遮罩 |
| VideoListView | aboutToAppear | aboutToDisappear | 视频遮罩(同时暂停所有视频) |
为什么不用全局单例?防窥遮罩是页面级 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') |
| 开关查询 | 异步 Promise | dlpAntiPeep.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 | 人脸识别 | 状态订阅
如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流~
更多推荐

所有评论(0)