摘要
OpenHarmony 分为静态权限与动态敏感权限,相册、相机、定位、麦克风等高危权限无法仅靠 module.json5 静态声明,必须代码动态弹窗申请。API Version23 重构 accessToken 权限校验、授权状态查询、权限弹窗调度逻辑,修复低版本重复申请弹窗、拒绝权限无拦截、权限状态缓存错乱、页面销毁仍弹窗等兼容缺陷。旧项目升级 API23 后常出现:多次进入页面重复弹权限、用户拒绝后功能无提示、权限申请阻塞页面渲染、后台申请权限闪退等问题。本文基于 DevEco Studio 适配 OpenHarmony API23 及以上版本,封装全局动态权限工具类,封装单权限、批量权限申请、权限状态判断、拒绝引导四大能力,搭配相机、相册两大实战场景提供完整可运行代码,输出权限申请时机、页面资源联动、交互提示规范,汇总版本升级兼容故障解决方案,为鸿蒙敏感权限开发提供标准化实操模板。
关键词
OpenHarmony;ArkUI;API Version23;动态权限;权限申请;accessToken;媒体权限;相机权限
一、引言
1.1 动态权限开发背景
系统将权限分为普通静态权限、敏感动态权限。INTERNET 网络权限仅需在 module.json5 静态配置;相机 CAMERA、相册 READ_MEDIA_IMAGES、定位 LOCATION、麦克风 MICROPHONE 属于用户隐私敏感权限,必须运行时弹窗获取用户授权,未授权调用对应功能直接抛出异常闪退。 OpenHarmony API Version23 针对权限底层调度引擎完成全面升级,核心变更点:
- 标准化 accessToken 管理,页面独立上下文权限隔离,跨页面权限状态互不干扰;
- 新增精准权限状态枚举:已授权 / 永久拒绝 / 临时拒绝,区分不同拒绝场景;
- 限制后台无页面上下文申请权限,仅前台可见页面可弹出授权弹窗;
- 优化批量权限申请时序,串行弹窗,不会一次性弹出多个权限框;
- 修复权限状态缓存 bug,用户手动在设置修改权限后可实时刷新状态;
- 强化页面销毁终止未完成的权限申请流程,避免悬浮弹窗残留。
大量 API9~11 旧项目升级后,相册 / 相机打开直接崩溃、用户拒绝权限无友好提示、重复弹窗骚扰用户,根源是未做动态权限前置校验,因此掌握 API23 标准动态权限封装规范是多媒体、定位类功能必备技能。
1.2 开发环境与测试场景
开发工具:DevEco Studio 5.0 及以上 适配系统:OpenHarmony API Version23、HarmonyOS NEXT 导入模块:@ohos.accessToken、@ohos.promptAction 静态前置配置:module.json5 声明对应权限 测试场景:相册图片读取前置权限校验、相机拍摄权限申请、批量同时申请相册 + 相机、用户拒绝权限弹窗引导至系统设置
二、API23+ 动态权限核心 API 与版本变更说明
2.1 核心模块导入
ets
import accessToken from '@ohos.accessToken' import common from '@ohos.app.ability.common'2.2 权限状态判断 API
accessToken.checkAccessTokenSync(context, permission)返回值枚举:
- accessToken.GrantStatus.PERMISSION_GRANTED:已授权
- accessToken.GrantStatus.PERMISSION_DENIED:临时拒绝,可再次申请弹窗
- accessToken.GrantStatus.PERMISSION_DENIED_AND_DISABLED:永久拒绝,需跳转系统设置手动开启
2.3 权限申请 API
accessToken.requestPermissions(context, permissionList)异步申请,传入权限字符串数组,返回授权结果集合
2.4 常用敏感权限常量
ets
// 相册图片读取 const READ_IMAGE = "ohos.permission.READ_MEDIA_IMAGES" // 相机拍摄 const CAMERA = "ohos.permission.CAMERA" // 麦克风 const MIC = "ohos.permission.MICROPHONE" // 位置定位 const LOCATION = "ohos.permission.APPROXIMATELY_LOCATION"2.5 API23 废弃与强制约束
- 废弃无校验直接调用相册 / 相机,必须先 checkAccessToken 判断权限状态;
- 禁止后台、弹窗内部申请权限,仅页面主线程、前台上下文允许申请;
- 批量权限串行申请,不支持并行多弹窗同时弹出;
- 永久拒绝状态不可重复申请弹窗,必须提示用户跳转系统设置;
- 页面销毁时终止正在进行的权限申请流程,防止弹窗悬浮。
三、API23 标准基础示例代码
3.1 单权限判断与申请基础逻辑
ets
import accessToken from '@ohos.accessToken' import promptAction from '@ohos.promptAction' async function requestCameraPermission(ctx: common.UIAbilityContext): Promise<boolean> { const perm = "ohos.permission.CAMERA" const status = accessToken.checkAccessTokenSync(ctx, perm) if (status === accessToken.GrantStatus.PERMISSION_GRANTED) { return true } // 发起申请 const res = await accessToken.requestPermissions(ctx, [perm]) if (res[0].grantStatus === accessToken.GrantStatus.PERMISSION_GRANTED) { return true } else if (res[0].grantStatus === accessToken.GrantStatus.PERMISSION_DENIED_AND_DISABLED) { promptAction.showToast({ message: "权限已永久拒绝,请前往设置开启相机权限" }) return false } else { promptAction.showToast({ message: "未授予相机权限,无法打开拍摄" }) return false } }3.2 批量多权限同时申请基础逻辑
ets
async function requestMediaAll(ctx: common.UIAbilityContext): Promise<boolean> { const perms = [ "ohos.permission.CAMERA", "ohos.permission.READ_MEDIA_IMAGES" ] const results = await accessToken.requestPermissions(ctx, perms) // 校验全部权限均授权 let allGrant = true for (let item of results) { if (item.grantStatus !== accessToken.GrantStatus.PERMISSION_GRANTED) { allGrant = false break } } return allGrant }四、三大业务完整实战案例(全兼容 API23)
4.1 实战一:全局动态权限工具类封装(utils/permission_util.ets)
ets
import accessToken from '@ohos.accessToken' import common from '@ohos.app.ability.common' import promptAction from '@ohos.promptAction' // 常用权限常量统一导出 export const PERMISSION = { READ_IMAGE: "ohos.permission.READ_MEDIA_IMAGES", CAMERA: "ohos.permission.CAMERA", MIC: "ohos.permission.MICROPHONE", LOCATION: "ohos.permission.APPROXIMATELY_LOCATION" } class PermissionUtil { private static instance: PermissionUtil private context: common.UIAbilityContext | null = null static getInstance(): PermissionUtil { if (!PermissionUtil.instance) { PermissionUtil.instance = new PermissionUtil() } return PermissionUtil.instance } // 绑定页面上下文,页面aboutToAppear传入getContext(this) setContext(ctx: common.UIAbilityContext) { this.context = ctx } // 检查单个权限是否已授权 checkSingle(perm: string): boolean { if (!this.context) return false const status = accessToken.checkAccessTokenSync(this.context, perm) return status === accessToken.GrantStatus.PERMISSION_GRANTED } // 申请单个权限,返回是否授权成功 async requestSingle(perm: string, tipText: string): Promise<boolean> { if (!this.context) return false // 先校验已有权限 if (this.checkSingle(perm)) return true const resultList = await accessToken.requestPermissions(this.context, [perm]) const result = resultList[0] if (result.grantStatus === accessToken.GrantStatus.PERMISSION_GRANTED) { return true } else if (result.grantStatus === accessToken.GrantStatus.PERMISSION_DENIED_AND_DISABLED) { promptAction.showToast({ message: `${tipText}权限已永久拒绝,请去系统设置开启` }) return false } else { promptAction.showToast({ message: `未授予${tipText}权限,功能无法使用` }) return false } } // 批量申请一组权限,全部授权才返回true async requestBatch(permArr: string[], tip: string): Promise<boolean> { if (!this.context) return false const resultList = await accessToken.requestPermissions(this.context, permArr) let allOk = true for (let item of resultList) { if (item.grantStatus !== accessToken.GrantStatus.PERMISSION_GRANTED) { allOk = false break } } if (!allOk) { promptAction.showToast({ message: `${tip}相关权限未全部授予` }) } return allOk } } export default PermissionUtil.getInstance()4.2 实战二:相册图片选择页面(相册权限前置校验)
ets
import PermissionUtil, { PERMISSION } from '../utils/permission_util' import ImagePickerUtil from '../utils/image_picker' @Entry @Component struct ImagePermissionPage { @State selectUri: ResourceStr = "" aboutToAppear() { // 页面初始化绑定上下文 PermissionUtil.setContext(getContext(this)) } // 打开相册前校验并申请相册权限 async openAlbum() { const hasPerm = await PermissionUtil.requestSingle(PERMISSION.READ_IMAGE, "相册读取") if (!hasPerm) return // 授权成功后调用相册选择 const uri = await ImagePickerUtil.selectSingle() if (uri) this.selectUri = uri } build() { Column({ space: 30 }) { Text("相册权限动态申请演示") .fontSize(22) .fontWeight(FontWeight.Bold) if (this.selectUri) { Image(this.selectUri) .width(200) .height(200) .borderRadius(12) .objectFit(ImageFit.Cover) } else { Text("暂无选中图片") .width(200) .height(200) .backgroundColor("#eee") .textAlign(TextAlign.Center) .fontColor("#999") } Button("打开相册选择图片") .width(280) .height(48) .backgroundColor("#007DFF") .onClick(async () => await this.openAlbum()) } .width("100%") .height("100%") .justifyContent(FlexAlign.Center) .padding(20) } }4.3 实战三:相机拍摄页面(相机权限批量场景)
ets
import PermissionUtil, { PERMISSION } from '../utils/permission_util' import promptAction from '@ohos.promptAction' @Entry @Component struct CameraPermissionPage { aboutToAppear() { PermissionUtil.setContext(getContext(this)) } // 同时申请相机+麦克风(录像需要麦克风) async openCameraRecord() { const permList = [PERMISSION.CAMERA, PERMISSION.MIC] const allGrant = await PermissionUtil.requestBatch(permList, "相机与麦克风") if (!allGrant) return promptAction.showToast({ message: "权限全部授予,可打开录像相机" }) // 此处可调用相机拍摄业务逻辑 } // 仅拍照,只申请相机权限 async openCameraTakePhoto() { const ok = await PermissionUtil.requestSingle(PERMISSION.CAMERA, "相机") if (!ok) return promptAction.showToast({ message: "相机权限已授予,可拍照" }) } build() { Column({ space: 25 }) { Text("相机动态权限申请") .fontSize(22) .fontWeight(FontWeight.Bold) Button("仅拍照(仅相机权限)") .width(280) .height(48) .backgroundColor("#007DFF") .onClick(async () => await this.openCameraTakePhoto()) Button("录像(相机+麦克风批量权限)") .width(280) .height(48) .backgroundColor("#009966") .onClick(async () => await this.openCameraRecord()) } .width("100%") .height("100%") .justifyContent(FlexAlign.Center) .padding(20) } }五、API23+ 动态权限适配与交互优化规范
5.1 上下文绑定规范
- 每个页面
aboutToAppear调用工具 setContext 绑定当前页面上下文,权限校验依赖页面上下文; - 禁止全局复用单一上下文,切换页面必须重新绑定,否则权限状态判断失效;
- Ability 全局上下文不可用于页面权限申请,仅页面 UI 上下文有效。
5.2 权限申请时机规范
- 调用敏感功能(相册、相机、定位)点击按钮时动态申请,不页面加载自动弹窗;
- 批量权限按需申请,拍照仅申请相机,录像才追加麦克风,不一次性申请全部权限;
- 用户永久拒绝后不再重复发起申请弹窗,仅文字提示引导系统设置。
5.3 交互体验规范
- 每次权限申请附带对应业务文字提示,告知用户权限用途;
- 授权失败统一 Toast 提示,不直接崩溃闪退;
- 永久拒绝场景明确告知用户需手动去系统设置开启;
- 权限校验与业务功能强耦合,未授权直接阻断后续逻辑执行。
5.4 性能与内存规范
- 权限校验为同步轻量操作,无性能损耗,功能调用前必执行;
- 页面销毁无需清理权限工具实例,仅下次进入重新绑定上下文即可;
- 未完成的异步权限申请,页面销毁后自动中断,不会残留弹窗。
六、API23 升级高频兼容问题与解决方案
问题 1:打开相册 / 相机直接闪退,无任何弹窗提示 解决:功能调用前增加动态权限校验,module.json5 补充对应权限静态声明。
问题 2:用户拒绝一次权限后,再次点击按钮不再弹出授权框 解决:区分临时拒绝与永久拒绝状态,临时拒绝允许重复申请弹窗;永久拒绝拦截申请,给出设置引导。
问题 3:切换页面后权限状态判断永远返回未授权 解决:每个页面 aboutToAppear 重新调用 setContext 绑定当前页面上下文。
问题 4:同时申请相机 + 相册,两个弹窗重叠弹出 解决:使用工具类 requestBatch 批量串行申请,系统自动顺序弹窗。
问题 5:升级 API23 后旧版 requestPermission 单参数接口编译报错 解决:全部替换为 accessToken 标准 requestPermissions 数组传参写法。
问题 6:后台切回页面自动弹出权限弹窗,干扰操作 解决:仅在用户主动点击按钮时发起权限申请,禁止 aboutToAppear 自动申请。
七、总结
accessToken 动态权限是 OpenHarmony 隐私敏感功能必备前置流程,API Version23 重构权限状态判断、上下文隔离、批量申请底层逻辑,区分临时拒绝与永久拒绝两种拦截场景,解决旧版无校验闪退、重复弹窗、上下文错乱等核心兼容问题。项目统一封装全局权限工具类,提供单权限、批量权限两套申请方法,搭配相册、相机两大高频多媒体场景实战代码,所有功能调用前强制权限校验,保证应用稳定不崩溃、交互友好。