摘要
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 重构权限状态判断、上下文隔离、批量申请底层逻辑,区分临时拒绝与永久拒绝两种拦截场景,解决旧版无校验闪退、重复弹窗、上下文错乱等核心兼容问题。项目统一封装全局权限工具类,提供单权限、批量权限两套申请方法,搭配相册、相机两大高频多媒体场景实战代码,所有功能调用前强制权限校验,保证应用稳定不崩溃、交互友好。