OpenHarmony 权限动态申请封装(API Version23 + 适配版)

摘要

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 针对权限底层调度引擎完成全面升级,核心变更点:

  1. 标准化 accessToken 管理,页面独立上下文权限隔离,跨页面权限状态互不干扰;
  2. 新增精准权限状态枚举:已授权 / 永久拒绝 / 临时拒绝,区分不同拒绝场景;
  3. 限制后台无页面上下文申请权限,仅前台可见页面可弹出授权弹窗;
  4. 优化批量权限申请时序,串行弹窗,不会一次性弹出多个权限框;
  5. 修复权限状态缓存 bug,用户手动在设置修改权限后可实时刷新状态;
  6. 强化页面销毁终止未完成的权限申请流程,避免悬浮弹窗残留。

大量 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 废弃与强制约束

  1. 废弃无校验直接调用相册 / 相机,必须先 checkAccessToken 判断权限状态;
  2. 禁止后台、弹窗内部申请权限,仅页面主线程、前台上下文允许申请;
  3. 批量权限串行申请,不支持并行多弹窗同时弹出;
  4. 永久拒绝状态不可重复申请弹窗,必须提示用户跳转系统设置;
  5. 页面销毁时终止正在进行的权限申请流程,防止弹窗悬浮。

三、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 上下文绑定规范

  1. 每个页面aboutToAppear调用工具 setContext 绑定当前页面上下文,权限校验依赖页面上下文;
  2. 禁止全局复用单一上下文,切换页面必须重新绑定,否则权限状态判断失效;
  3. Ability 全局上下文不可用于页面权限申请,仅页面 UI 上下文有效。

5.2 权限申请时机规范

  1. 调用敏感功能(相册、相机、定位)点击按钮时动态申请,不页面加载自动弹窗;
  2. 批量权限按需申请,拍照仅申请相机,录像才追加麦克风,不一次性申请全部权限;
  3. 用户永久拒绝后不再重复发起申请弹窗,仅文字提示引导系统设置。

5.3 交互体验规范

  1. 每次权限申请附带对应业务文字提示,告知用户权限用途;
  2. 授权失败统一 Toast 提示,不直接崩溃闪退;
  3. 永久拒绝场景明确告知用户需手动去系统设置开启;
  4. 权限校验与业务功能强耦合,未授权直接阻断后续逻辑执行。

5.4 性能与内存规范

  1. 权限校验为同步轻量操作,无性能损耗,功能调用前必执行;
  2. 页面销毁无需清理权限工具实例,仅下次进入重新绑定上下文即可;
  3. 未完成的异步权限申请,页面销毁后自动中断,不会残留弹窗。

六、API23 升级高频兼容问题与解决方案

问题 1:打开相册 / 相机直接闪退,无任何弹窗提示 解决:功能调用前增加动态权限校验,module.json5 补充对应权限静态声明。

问题 2:用户拒绝一次权限后,再次点击按钮不再弹出授权框 解决:区分临时拒绝与永久拒绝状态,临时拒绝允许重复申请弹窗;永久拒绝拦截申请,给出设置引导。

问题 3:切换页面后权限状态判断永远返回未授权 解决:每个页面 aboutToAppear 重新调用 setContext 绑定当前页面上下文。

问题 4:同时申请相机 + 相册,两个弹窗重叠弹出 解决:使用工具类 requestBatch 批量串行申请,系统自动顺序弹窗。

问题 5:升级 API23 后旧版 requestPermission 单参数接口编译报错 解决:全部替换为 accessToken 标准 requestPermissions 数组传参写法。

问题 6:后台切回页面自动弹出权限弹窗,干扰操作 解决:仅在用户主动点击按钮时发起权限申请,禁止 aboutToAppear 自动申请。

七、总结

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

相关推荐
进击的前栈1 小时前
鸿蒙实战:全栈回顾——Kit API 全景、架构总结与未来演进
华为·架构·harmonyos
在书中成长1 小时前
HarmonyOS 小游戏《对战五子棋》开发第33篇 - AI异步落子与setTimeout
人工智能·harmonyos
listening7771 小时前
HarmonyOS 6.1 分布式文件服务实战:跨设备商品图片/视频秒开方案
wpf·harmonyos
xianjixiance_1 小时前
鸿蒙实战:BindRelationPage 四步状态机与邀请卡倒计时
华为·harmonyos·鸿蒙系统
解局易否结局1 小时前
鸿蒙三方库生态与实战:从安装到发布全链路指南
深度学习·华为·harmonyos
在书中成长1 小时前
HarmonyOS 小游戏《对战五子棋》开发第31篇 - TwoPlayerPage双人对战(三):结果弹窗与游戏结束处理
harmonyos
绝世番茄2 小时前
鸿蒙HarmonyOS ArkTS原生学习:缩放手势实现 PinchToZoom 深度解析
学习·华为·harmonyos·鸿蒙
进击的前栈2 小时前
鸿蒙实战:19 页面全景汇总与 7 大共享设计模式回顾
华为·设计模式·harmonyos
ujainu小2 小时前
原生性能优化:6变量实现高效桥接
开发语言·华为·性能优化·harmonyos
星释2 小时前
鸿蒙智能体开发实战:27.Skill 测试、发布与管理
ui·华为·log4j·harmonyos·鸿蒙·智能体