摘要

鸿蒙将权限分为安装时静态权限运行时动态敏感权限,相机、相册、文件、通知、位置等都必须动态申请,原生权限 API 重复代码多、缺少永久拒绝跳转设置页逻辑、无统一弹窗提示、未做权限状态判断封装。本文基于@ohos.accessToken封装全局权限工具类,整合相机、相册、存储、通知、定位全套常用权限申请逻辑,包含权限已授予 / 临时拒绝 / 永久拒绝三种状态分支处理,搭配弹窗引导用户开启权限,适配页面拍照、上传图片、本地文件读写、消息通知等业务场景。API23 重构 accessToken 权限校验、权限请求回调、应用设置页跳转接口,修复多权限并发申请错乱、永久拒绝无引导、权限状态判断失效、页面销毁权限弹窗残留等问题。

关键词

OpenHarmony;ArkTS;API23;accessToken;动态权限;Permission;权限申请;跳转应用设置页

一、引言

1.1 动态权限开发痛点

  1. 每次调用相机 / 相册都要重复写权限判断、申请逻辑,大量冗余代码;
  2. 用户选择永久拒绝权限后,再次申请不会弹出授权框,缺少跳转系统设置引导;
  3. 多权限同时申请(存储 + 相机)并发回调错乱;
  4. 无统一弹窗提示告知用户该功能需要对应权限;
  5. 页面销毁后权限申请回调执行,页面变量报错;
  6. 无法统一封装权限授予 / 拒绝回调,业务页面重复写分支逻辑。

1.2 动态权限分类(高频业务)

表格

权限名称 用途
ohos.permission.CAMERA 相机拍照
ohos.permission.READ_MEDIA_IMAGES 读取相册图片
ohos.permission.WRITE_MEDIA 保存图片到相册
ohos.permission.NOTIFICATION_AGENT 系统通知推送
ohos.permission.READ_FOLDER 沙盒外部文件读写
ohos.permission.LOCATION 定位获取坐标

API23 accessToken 核心升级:

  1. 标准化checkAccessTokenSync同步校验权限状态,简化异步判断;
  2. 权限请求结果细分三种状态:已授权、临时拒绝、永久拒绝;
  3. 新增统一跳转应用权限设置接口,无需拼接 want 参数;
  4. 多权限批量申请优化,回调不会丢失单权限结果;
  5. 页面销毁自动终止未完成的权限申请,杜绝内存泄漏。

二、权限工具完整封装 utils/permission_util.ets

ets

import accessToken from '@ohos.accessToken'
import common from '@ohos.app.ability.common'
import abilityAccessCtrl from '@ohos.abilityAccessCtrl'
import promptAction from '@ohos.promptAction'
import Want from '@ohos.app.ability.Want'
import UIAbility from '@ohos.app.ability.UIAbility'
import DialogUtil from './dialog_util'

// 全局常用动态权限常量
export const PERMISSION = {
  CAMERA: "ohos.permission.CAMERA",
  READ_IMAGE: "ohos.permission.READ_MEDIA_IMAGES",
  WRITE_MEDIA: "ohos.permission.WRITE_MEDIA",
  NOTIFICATION: "ohos.permission.NOTIFICATION_AGENT",
  LOCATION: "ohos.permission.LOCATION"
}

// 权限申请回调类型定义
export type PermissionCallback = (granted: boolean) => void

class PermissionUtil {
  private static instance: PermissionUtil
  private ctx: common.UIAbilityContext | null = null
  private atManager: abilityAccessCtrl.AbilityAccessCtrl | null = null

  static getInstance() {
    if (!PermissionUtil.instance) {
      PermissionUtil.instance = new PermissionUtil()
    }
    return PermissionUtil.instance
  }

  // 注入页面/Ability上下文
  setContext(context: common.UIAbilityContext) {
    this.ctx = context
    this.atManager = abilityAccessCtrl.createAtManager()
  }

  // 同步校验单个权限是否已授予
  checkPermission(permission: string): boolean {
    if (!this.ctx) return false
    const tokenId = this.ctx.applicationInfo.accessTokenId
    const status = accessToken.checkAccessTokenSync(tokenId, permission)
    return status === accessToken.GrantStatus.PERMISSION_GRANTED
  }

  // 跳转系统应用权限设置页面
  jumpAppSetting() {
    if (!this.ctx) return
    const want: Want = {
      bundleName: "com.ohos.settings",
      abilityName: "com.ohos.settings.MainAbility",
      uri: `settings://app/${this.ctx.bundleName}/permissions`
    }
    this.ctx.startAbility(want)
  }

  // 申请单个动态权限
  async requestPermission(permission: string, desc: string, callback: PermissionCallback) {
    if (!this.atManager || !this.ctx) {
      promptAction.showToast({ message: "上下文未初始化" })
      return callback(false)
    }
    // 已授权直接执行回调
    if (this.checkPermission(permission)) {
      return callback(true)
    }
    // 发起权限申请
    const result = await this.atManager.requestPermissionsFromUser(this.ctx, [permission])
    const grantStatus = result.authResults[0]
    // 1. 用户同意授权
    if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
      return callback(true)
    }
    // 2. 永久拒绝,弹窗引导跳转设置
    if (result.deniedPermanent) {
      DialogUtil.confirm({
        title: "权限提示",
        content: `${desc}需要对应权限才能正常使用,请前往设置开启`
      }, () => {
        this.jumpAppSetting()
        callback(false)
      })
    } else {
      // 临时拒绝,提示功能无法使用
      promptAction.showToast({ message: "未授予权限,功能无法使用" })
      callback(false)
    }
  }

  // 批量申请多个权限(相机+相册)
  async requestMultiPermission(permissions: string[], desc: string, callback: PermissionCallback) {
    if (!this.atManager || !this.ctx) return callback(false)
    // 过滤未授权权限
    const needReqList = permissions.filter(p => !this.checkPermission(p))
    if (needReqList.length === 0) return callback(true)

    const result = await this.atManager.requestPermissionsFromUser(this.ctx, needReqList)
    // 判断全部权限是否授予
    const allGranted = result.authResults.every(
      s => s === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
    )
    if (allGranted) return callback(true)
    // 存在永久拒绝权限,引导跳转设置
    if (result.deniedPermanent) {
      DialogUtil.confirm({
        title: "权限缺失",
        content: `${desc}需要相关权限,请前往系统设置手动开启`
      }, () => this.jumpAppSetting())
    } else {
      promptAction.showToast({ message: "缺少必要权限,功能不可用" })
    }
    callback(false)
  }

  // 快捷申请相机权限
  reqCamera(cb: PermissionCallback) {
    this.requestPermission(PERMISSION.CAMERA, "拍照功能", cb)
  }

  // 快捷申请相册读写权限
  reqImage(cb: PermissionCallback) {
    this.requestMultiPermission([PERMISSION.READ_IMAGE, PERMISSION.WRITE_MEDIA], "相册选择/保存图片", cb)
  }

  // 快捷申请通知权限
  reqNotify(cb: PermissionCallback) {
    this.requestPermission(PERMISSION.NOTIFICATION, "消息通知推送", cb)
  }

  // 快捷申请定位权限
  reqLocation(cb: PermissionCallback) {
    this.requestPermission(PERMISSION.LOCATION, "获取位置信息", cb)
  }
}

export default PermissionUtil.getInstance()

三、业务页面实战调用

3.1 拍照上传页面(相机权限)

ets

import PermissionUtil, { PERMISSION } from '../utils/permission_util'
import CameraUtil from '../utils/camera_util'

@Entry
@Component
struct CameraPage {
  async aboutToAppear() {
    PermissionUtil.setContext(getContext(this))
  }

  openCamera() {
    PermissionUtil.reqCamera((granted: boolean) => {
      if (granted) {
        // 权限通过,打开相机
        CameraUtil.startCapture()
      }
    })
  }

  build() {
    Column() {
      Button("打开相机拍照")
        .width(300)
        .height(48)
        .onClick(() => this.openCamera())
    }
    .width("100%")
    .height("100%")
    .justifyContent(Center)
  }
}

3.2 头像选择页面(相册批量权限)

ets

import PermissionUtil from '../utils/permission_util'
import ImagePickerUtil from '../utils/image_picker_util'

// 打开相册选择图片
function pickImage() {
  PermissionUtil.reqImage((granted) => {
    if (granted) {
      ImagePickerUtil.openAlbum()
    }
  })
}

3.3 通知设置页申请通知权限

ets

Button("开启消息通知")
  .onClick(() => {
    PermissionUtil.reqNotify((granted) => {
      if (granted) {
        NotifyUtil.sendBasicNotify(1001, "通知已开启", "您将收到应用资讯推送")
      }
    })
  })

3.4 定位功能页面

ets

function getLocation() {
  PermissionUtil.reqLocation(async (granted) => {
    if (granted) {
      const loc = await LocationUtil.getNowLocation()
      console.info("当前坐标", loc)
    }
  })
}

四、module.json5 权限静态声明配置

动态权限必须在 module.json5 提前声明,否则申请直接失败

json

"requestPermissions": [
  { "name": "ohos.permission.CAMERA", "reason": "用于拍照上传图片" },
  { "name": "ohos.permission.READ_MEDIA_IMAGES", "reason": "读取相册图片" },
  { "name": "ohos.permission.WRITE_MEDIA", "reason": "保存图片至相册" },
  { "name": "ohos.permission.NOTIFICATION_AGENT", "reason": "推送系统消息通知" },
  { "name": "ohos.permission.LOCATION", "reason": "获取地理位置信息" }
]

五、API23 权限开发编码规范

5.1 上下文规范

  1. 页面aboutToAppear注入页面上下文PermissionUtil.setContext(getContext(this))
  2. EntryAbility onCreate 全局初始化一次,全局页面可复用。

5.2 权限申请时机规范

  1. 用户点击功能按钮时再申请,禁止页面加载自动弹窗索要权限;
  2. 高危功能(相机、相册)操作前校验权限,不提前静默申请。

5.3 永久拒绝分支规范

  1. 用户永久拒绝权限后,弹出确认弹窗引导跳转系统设置;
  2. 弹窗说明权限用途,提升用户开启概率。

5.4 批量权限规范

  1. 相册读取 + 保存配套批量申请,避免分两次弹窗打扰用户;
  2. 无关权限禁止批量合并申请。

5.5 页面生命周期规范

  1. 页面销毁无需手动取消权限申请,API23 底层自动终止未完成请求;
  2. 权限回调内操作页面变量增加判空,防止页面已销毁。

六、高频报错问题解决方案

问题 1:调用 requestPermissionsFromUser 无授权弹窗 解决:module.json5 声明对应 permission 权限名称,名称完全匹配。

问题 2:用户勾选永久拒绝后再次申请无弹窗 解决:工具内判断 deniedPermanent,弹窗引导跳转应用设置页手动开启。

问题 3:批量申请相册权限只授予其中一个 解决:使用 requestMultiPermission 批量接口,同时校验读写两张媒体权限。

问题 4:提示上下文为空,权限校验失败 解决:页面 aboutToAppear 调用 setContext 注入当前页面 context。

问题 5:升级 API23 旧版权限代码编译报错 解决:替换废弃 requestPermissions 异步接口,使用 atManager 标准 API,同步校验改用 checkAccessTokenSync。

问题 6:跳转设置页面找不到应用权限 解决:want 内 bundleName 使用当前应用真实包名,工具自动读取上下文 bundleName 无需手写。

七、总结

PermissionUtil 统一封装鸿蒙动态权限全套逻辑,整合相机、相册、通知、定位高频权限快捷方法,自动区分已授权 / 临时拒绝 / 永久拒绝三种状态,永久拒绝场景弹窗引导跳转系统设置,解决原生权限代码冗余、无引导、多权限并发错乱等工程化痛点。 完全兼容前文相机、相册、通知、上传等业务工具,是所有涉及媒体、通知、定位功能项目必备底层工具,适配 API23 与 HarmonyOS NEXT 课程设计、毕设完整工程架构。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐