对应Demo: PermissionDemo | 难度: 高级 | 关键词: abilityAccessCtrl, requestPermissionsFromUser, checkAccessToken, Permissions

我第一个提交华为应用市场的HarmonyOS应用,审核直接被拒了。拒审理由写了一堆,核心就一条——“应用在使用相机功能时未动态申请权限”。我当时心想,我module.json5里不是声明了ohos.permission.CAMERA权限吗?怎么还说没申请?后来才知道,HarmonyOS的权限分两种——normal类型的安装时自动授予,但user_grant类型的必须运行时动态申请,弹窗让用户确认。我光在json5里声明了,代码里压根没调用requestPermissionsFromUser,难怪被拒。

更搞笑的是,我后来加上动态申请的代码,又因为权限弹窗的reason字符串写了中文原文而不是$string:资源引用,再次被拒。审核标准那个严啊,你写"需要使用相机"不够具体,要写"需要使用相机拍照以完成头像设置"才行;你写中文原文不行,必须用$string:资源引用。我的应用前前后后提交了三次才过审,每次被拒的理由都不一样,但都是权限相关的问题。

还有个隐蔽的坑——我一开始的代码是先checkAccessToken检查权限状态,发现未授权后再requestPermissionsFromUser申请。看起来逻辑没问题对吧?但如果用户之前选了"拒绝且不再询问",requestPermissionsFromUser就不会再弹窗了,直接返回拒绝。我的代码以为会弹窗,一直等着用户操作,结果等了个寂寞,卡在那了。

我后来仔细回想了一下,整个权限功能的开发大概花了我三天——第一天被拒审然后排查原因,第二天加上动态申请代码又因为reason格式被拒,第三天把所有细节搞对终于过审。三天时间就为了搞个权限申请,这在Android上半天就能搞定的事。但HarmonyOS的审核确实更严格,每个细节都要对,一个不对就打回。特别是reason的写法,我见过有人写了五六个版本才通过,审核人员觉得reason跟功能不匹配就给你打回来。

还有个很多人不知道的事情——权限申请的时机也很重要。你一进入应用就弹一堆权限申请弹窗,审核可能不通过,因为用户体验差。审核人员希望看到的是"按需申请"——用户点击拍照按钮时才申请相机权限,点击发送语音时才申请麦克风权限。这种交互设计更合理,也是HarmonyOS推荐的做法。

今天这篇,我把权限相关的坑全部写出来,帮你们一次过审,少走弯路。


在这里插入图片描述

两种权限类型:normal vs user_grant

这是最基础的概念,搞不清这个后面全白搭。我第一天做权限功能的时候,根本不知道权限还分两种,以为声明了就自动拥有。结果相机死活打不开,日志里全是权限错误。

normal权限

安装时自动授予,不需要用户确认。应用安装完成后就自动拥有这些权限,用户感知不到。比如:

  • ohos.permission.INTERNET — 网络访问
  • ohos.permission.GET_NETWORK_INFO — 获取网络信息
  • ohos.permission.SET_NETWORK_INFO — 设置网络信息
  • ohos.permission.KEEP_BACKGROUND_RUNNING — 长时任务后台运行

这种权限你在module.json5里声明了就行,用户安装App后自动拥有,不需要任何额外操作。

user_grant权限

必须运行时动态申请,弹窗让用户确认。用户可以同意或拒绝,拒绝后功能不可用。比如:

  • ohos.permission.CAMERA — 相机
  • ohos.permission.MICROPHONE — 麦克风
  • ohos.permission.LOCATION — 精确定位
  • ohos.permission.APPROXIMATELY_LOCATION — 大概位置
  • ohos.permission.READ_IMAGEVIDEO — 读取图片/视频
  • ohos.permission.WRITE_IMAGEVIDEO — 写入图片/视频
  • ohos.permission.READ_CALENDAR — 读取日历
  • ohos.permission.ANSWER_CALL — 接听电话

这种权限光在json5里声明是不够的,必须在代码里调用requestPermissionsFromUser,用户点了"允许"才算授权成功。如果你只是声明了但没有动态申请,调用相关API时会直接报权限错误——这就是我被拒审的原因。

我第一次做的时候,json5声明了CAMERA权限,代码里直接调相机API,结果运行时直接报权限错误。当时还以为json5配置有问题,检查了八百遍,最后才知道——user_grant权限必须动态申请!

怎么区分normal和user_grant

最简单的方法是看权限的授权方式。在HarmonyOS官方文档的权限列表里,每个权限都标注了授权方式。system_grant就是normal(系统自动授予),user_grant就是需要用户确认的。

经验法则:涉及用户隐私的权限都是user_grant——相机、麦克风、位置、通讯录、日历、图片等。不涉及隐私的都是normal——网络、震动、屏幕亮度等。不过也有例外,比如ohos.permission.DISTRIBUTED_DATASYNC(分布式数据同步)虽然是数据相关权限,但因为涉及跨设备数据传输,也是user_grant。

在开发过程中,你可以通过adb shell pm list permissions或者在DevEco Studio的权限管理界面查看所有权限的详细信息,包括授权方式。不过最靠谱的方法还是查官方文档——权限列表里每个权限都标注了授权方式。

还有一个很多人忽略的细节——user_grant权限的授权状态是会变的。用户可能在系统设置里随时撤销已经授予的权限。所以你每次使用敏感功能之前,都应该检查权限状态,不要缓存在内存里。我之前在aboutToAppear里检查了一次相机权限,然后整个页面生命周期内都认为权限是已授权的,结果用户中途去设置里关了相机权限,回到应用后调用相机API直接崩溃。正确的做法是每次使用功能前都检查权限,或者在onPageShow里重新检查。权限不是"申请一次管一辈子"的东西,用户随时可以收回。

module.json5配置

不管是normal还是user_grant,都需要在module.json5里声明:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.INTERNET"
        // normal权限,不需要reason和usedScene
      },
      {
        "name": "ohos.permission.CAMERA",
        "reason": "$string:camera_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
        // user_grant权限,必须提供reason和usedScene
      },
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:microphone_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.APPROXIMATELY_LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      },
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

reason字符串:必须用$string资源引用——这是我被拒审的第二个原因

这是我被拒审的第二个原因,也是很多人容易忽略的。reason字段必须引用$string:资源,不能直接写中文原文。

// ❌ 直接写中文原文,审核不通过
{
  "name": "ohos.permission.CAMERA",
  "reason": "需要使用相机拍照",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}
// 审核人员会看这个reason,直接写原文不符合规范

// ✅ 使用$string资源引用
{
  "name": "ohos.permission.CAMERA",
  "reason": "$string:camera_reason",
  "usedScene": {
    "abilities": ["EntryAbility"],
    "when": "inuse"
  }
}

然后在resources/base/element/string.json里定义这个字符串:

{
  "string": [
    {
      "name": "camera_reason",
      "value": "需要使用相机拍照以完成头像设置"
    },
    {
      "name": "microphone_reason",
      "value": "需要使用麦克风录音以发送语音消息"
    },
    {
      "name": "location_reason",
      "value": "需要获取您的位置信息以提供附近的商家推荐"
    }
  ]
}

reason的内容也有讲究——不能太笼统。你写"需要使用相机"可能不够,得写清楚具体场景,比如"需要使用相机拍照以完成头像设置"。审核人员会看这个reason是否合理,是否跟应用功能匹配。如果你的App是个记账软件,reason写"需要使用相机拍照",审核人员会疑惑——记账为什么要用相机?你得写"需要使用相机扫描发票进行记账"才行。

usedScene字段

usedScene告诉系统这个权限在什么场景下使用:

  • abilities — 使用该权限的Ability列表,通常是["EntryAbility"]。如果你的应用有多个Ability都需要这个权限,就都列出来。
  • when — 使用时机,inuse(使用时)或always(始终)

大部分场景用inuse就行——只有在使用相关功能时才需要权限。always一般是后台持续需要权限的场景(比如后台定位、后台录音),审核会更严格,需要提供更详细的reason说明为什么需要后台持续使用。如果你的应用不是特别需要后台使用权限,千万不要写always,否则审核很可能被打回。

还有一个关于usedScene的细节——如果你声明了when: "inuse",但你的应用在后台也调用了需要权限的API,系统会拒绝。所以when的值必须跟你实际的使用场景匹配。


abilityAccessCtrl:权限管理核心API

创建AtManager实例

所有权限操作都通过AtManager实例来做:

import { abilityAccessCtrl, Permissions, bundleManager } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

// 创建权限管理器
let atManager = abilityAccessCtrl.createAtManager()

这里有个ArkTS的类型推断问题。如果写成let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(),类型标注虽然精确,但有时候会遇到命名空间引用的编译问题。我用下来直接让类型推断更省事:

// 直接让类型推断,简单省事
let atManager = abilityAccessCtrl.createAtManager()

// 或者显式声明类型(如果推断有问题或需要明确类型)
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager()

requestPermissionsFromUser:运行时申请权限

这是最核心的API——运行时向用户请求权限。调用后系统会弹出一个权限申请弹窗,用户可以选择允许或拒绝。

import { abilityAccessCtrl, Permissions, common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

@Entry
@Component
struct PermissionDemo {
  @State permissionStatus: string = '未申请'

  private async requestCameraPermission(): Promise<void> {
    let atManager = abilityAccessCtrl.createAtManager()
    let context = this.getUIContext().getHostContext() as common.UIAbilityContext
    let permissions: Array<Permissions> = ['ohos.permission.CAMERA']

    try {
      let result = await atManager.requestPermissionsFromUser(context, permissions)
      // result.authResults是一个数组,对应每个权限的授权结果
      // 0 = 已授权(PERMISSION_GRANTED)
      // -1 = 未授权(PERMISSION_DENIED)
      let grantStatus = result.authResults[0]
      if (grantStatus === 0) {
        this.permissionStatus = '已授权'
        // 继续执行需要相机权限的操作
        this.openCamera()
      } else {
        this.permissionStatus = '用户拒绝'
        // 用户拒绝了,引导用户去设置页面手动开启
        this.showPermissionDeniedTip()
      }
    } catch (err) {
      let e = err as BusinessError
      console.error(`请求权限失败: ${e.code} - ${e.message}`)
      this.permissionStatus = '请求失败'
    }
  }

  build() {
    Column({ space: 20 }) {
      Text(`相机权限状态: ${this.permissionStatus}`)
        .fontSize(16)

      Button('请求相机权限')
        .onClick(() => {
          this.requestCameraPermission()
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  private openCamera(): void {
    console.info('打开相机...')
  }

  private showPermissionDeniedTip(): void {
    console.info('用户拒绝了权限,引导去设置')
  }
}

authResults的值含义

requestPermissionsFromUser返回的PermissionRequestResult对象有个authResults数组,每个元素对应该权限的授权状态:

值含义对应常量
0已授权PERMISSION_GRANTED
-1未授权PERMISSION_DENIED

这里有个重要细节——用户第一次被请求权限时,如果点了"拒绝",authResults返回-1。但系统还会给用户一次机会——下次请求时会再弹一次弹窗。如果用户点了"拒绝且不再询问",那以后请求就不再弹窗了,直接返回-1。

这意味着你不能简单地认为"返回-1就是用户拒绝了,下次还会弹窗"。你无法区分"用户拒绝了但下次还可以弹"和"用户拒绝了且不再询问"这两种情况。所以最好的做法是——如果用户拒绝了,就引导去系统设置页面手动开启权限。

let result = await atManager.requestPermissionsFromUser(context, permissions)

// 检查每个权限的状态
for (let i = 0; i < permissions.length; i++) {
  let status = result.authResults[i]
  if (status === 0) {
    console.info(`${permissions[i]} 已授权`)
    // 可以使用相关功能
  } else {
    console.info(`${permissions[i]} 未授权`)
    // 引导用户去设置页面手动开启
  }
}

checkAccessToken:检查当前权限状态

有时候你想先检查权限是否已授权,再决定是否弹出申请弹窗。比如用户进入相机页面前先检查相机权限,已授权就直接打开相机,未授权才弹出申请。这就要用到checkAccessToken。

获取tokenId

checkAccessToken需要两个参数:tokenId和权限名。tokenId需要从应用信息里获取——每个应用都有唯一的tokenId,用来标识应用身份。

import { bundleManager } from '@kit.AbilityKit'

// 获取本应用的bundleInfo(同步方法)
let bundleInfo = bundleManager.getBundleInfoForSelfSync(
  bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
)
// 从bundleInfo中获取tokenId
let tokenId = bundleInfo.appInfo.accessTokenId

getBundleInfoForSelfSync是同步方法,不需要await。注意要传GET_BUNDLE_INFO_WITH_APPLICATION这个flag,不然appInfo可能是空的。

检查权限状态

private async checkCameraPermission(): Promise<boolean> {
  let atManager = abilityAccessCtrl.createAtManager()
  let bundleInfo = bundleManager.getBundleInfoForSelfSync(
    bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  )
  let tokenId = bundleInfo.appInfo.accessTokenId
  let permission: Permissions = 'ohos.permission.CAMERA'

  try {
    let grantStatus = await atManager.checkAccessToken(tokenId, permission)
    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
  } catch (err) {
    let e = err as BusinessError
    console.error(`检查权限失败: ${e.code} - ${e.message}`)
    return false
  }
}

checkAccessToken返回GrantStatus枚举:

  • GrantStatus.PERMISSION_GRANTED — 已授权
  • GrantStatus.PERMISSION_DENIED — 未授权

同步版本:checkAccessTokenSync

如果不想用异步方式,还有个同步版本checkAccessTokenSync。在UI构建等不方便用await的场景下很好用:

private checkCameraPermissionSync(): boolean {
  let atManager = abilityAccessCtrl.createAtManager()
  let bundleInfo = bundleManager.getBundleInfoForSelfSync(
    bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  )
  let tokenId = bundleInfo.appInfo.accessTokenId

  let grantStatus = atManager.checkAccessTokenSync(tokenId, 'ohos.permission.CAMERA')
  return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
}

在aboutToAppear里用同步版本检查权限状态,然后根据结果决定UI显示:

aboutToAppear(): void {
  this.hasCameraPermission = this.checkCameraPermissionSync()
  if (this.hasCameraPermission) {
    // 已授权,可以直接使用相机
  } else {
    // 未授权,显示"请求权限"按钮
  }
}

常见陷阱:先检查后申请的问题

我见过一种写法——先checkAccessToken检查权限,如果未授权再requestPermissionsFromUser。看起来逻辑没问题,但实际有个大坑:

第一次运行时,checkAccessToken返回DENIED(因为从未授权过),然后requestPermissionsFromUser弹出申请弹窗。这没问题。

但如果用户之前拒绝了权限(且选择了"不再询问"),checkAccessToken返回DENIED,requestPermissionsFromUser不再弹窗(直接返回DENIED),你的代码可能陷入逻辑错误——你以为是"弹了窗用户拒绝了",实际是"根本没弹窗直接拒绝了"。两种情况的处理方式应该不同——前者可以提示"您拒绝了权限",后者需要引导"去设置页面手动开启"。

// ❌ 有问题的逻辑:先检查后申请,无法区分"拒绝"和"拒绝且不再询问"
private async requestPermissionIfNeeded(): Promise<boolean> {
  let hasPermission = await this.checkCameraPermission()
  if (!hasPermission) {
    // 如果用户之前点了"拒绝且不再询问",这里不会弹窗
    // 直接返回-1,你的代码以为弹了窗被拒绝
    let result = await this.requestPermissionsFromUser()
    return result.authResults[0] === 0
  }
  return true
}

// ✅ 更好的做法:直接申请,或者不过度依赖"是否弹窗"的判断
private async requestPermissionSafely(): Promise<boolean> {
  let atManager = abilityAccessCtrl.createAtManager()
  let context = this.getUIContext().getHostContext() as common.UIAbilityContext
  let permissions: Array<Permissions> = ['ohos.permission.CAMERA']

  try {
    let result = await atManager.requestPermissionsFromUser(context, permissions)
    if (result.authResults[0] === 0) {
      return true  // 授权成功
    }
    // 不管是用户拒绝了还是不再弹窗,都引导去设置页面
    this.showPermissionDeniedDialog()
    return false
  } catch (err) {
    return false
  }
}

最佳实践:try → fail → request → retry

这是业界推荐的权限申请模式——先尝试执行操作,如果因为权限不足而失败,再申请权限,然后重试。不要一进入页面就弹一堆权限申请弹窗,用户很烦这种。

private async takePhoto(): Promise<void> {
  // 先尝试直接操作
  try {
    await this.doTakePhoto()
    return  // 成功了就不需要申请权限
  } catch (err) {
    let e = err as BusinessError
    // 检查是否是权限不足导致的失败
    if (e.code === 201) {  // 权限不足的错误码
      // 申请权限
      let granted = await this.requestCameraPermission()
      if (granted) {
        // 权限获取成功,重试操作
        try {
          await this.doTakePhoto()
        } catch (retryErr) {
          // 重试也失败了,可能不是权限问题
          this.showError('拍照失败')
        }
      } else {
        // 用户拒绝权限,引导去设置
        this.showPermissionDeniedTip()
      }
    } else {
      // 其他错误,不是权限问题
      this.showError('拍照失败')
    }
  }
}

这种模式的好处:

  1. 已授权的情况下不会弹窗,用户无感知
  2. 只在真正需要权限的时候才申请
  3. 逻辑清晰——失败→申请→重试

同时申请多个权限

如果你的应用需要多个权限,可以一次性申请,不用一个一个弹。比如视频通话场景需要同时申请相机和麦克风:

private async requestMultiplePermissions(): Promise<void> {
  let atManager = abilityAccessCtrl.createAtManager()
  let context = this.getUIContext().getHostContext() as common.UIAbilityContext
  let permissions: Array<Permissions> = [
    'ohos.permission.CAMERA',
    'ohos.permission.MICROPHONE',
  ]

  try {
    let result = await atManager.requestPermissionsFromUser(context, permissions)
    // 逐个检查每个权限的状态
    for (let i = 0; i < permissions.length; i++) {
      let status = result.authResults[i]
      if (status === 0) {
        console.info(`${permissions[i]} 已授权`)
      } else {
        console.info(`${permissions[i]} 未授权`)
      }
    }
  } catch (err) {
    let e = err as BusinessError
    console.error(`申请权限失败: ${e.code}`)
  }
}

多个权限一起申请时,系统会依次弹出每个权限的申请弹窗。用户可以逐个选择允许或拒绝。authResults数组里按顺序对应每个权限的结果。

不过我建议不要一次申请太多权限——三个以上用户就容易烦了,全部拒绝的概率大大增加。最好是按功能分批申请:进入相机页面申请CAMERA,进入录音页面申请MICROPHONE,进入地图页面申请LOCATION。每次最多申请1-2个相关联的权限。


定位权限的特殊情况

定位权限在HarmonyOS里分两个级别,这是很多开发者不知道的:

  • ohos.permission.APPROXIMATELY_LOCATION — 大概位置(城市级别精度,约5公里)
  • ohos.permission.LOCATION — 精确位置(GPS级别精度,约10米)

这个设计跟Android类似——Android也是分粗略位置和精确定位。但HarmonyOS的弹窗行为有点不同。

如果你同时申请这两个权限,系统会弹出一个合并的定位权限弹窗,用户可以选择"大概位置"或"精确位置"。如果用户选了"大概位置",APPROXIMATELY_LOCATION授权,LOCATION不授权。

// 只申请大概位置——地图显示城市级别就够了
let permissions: Array<Permissions> = ['ohos.permission.APPROXIMATELY_LOCATION']

// 申请精确位置——导航、打车需要精确位置
let permissions: Array<Permissions> = [
  'ohos.permission.APPROXIMATELY_LOCATION',
  'ohos.permission.LOCATION',
]

注意:申请LOCATION前必须先申请APPROXIMATELY_LOCATION。如果你只申请LOCATION不申请APPROXIMATELY_LOCATION,系统可能会拒绝。这是因为精确定位包含了大概位置的信息,所以大概位置是精确位置的前置条件。

// ❌ 只申请LOCATION,可能失败
let permissions: Array<Permissions> = ['ohos.permission.LOCATION']

// ✅ 同时申请两个
let permissions: Array<Permissions> = [
  'ohos.permission.APPROXIMATELY_LOCATION',
  'ohos.permission.LOCATION',
]

引导用户去设置页面

如果用户点了"拒绝且不再询问",requestPermissionsFromUser不会再弹窗了。这时候需要引导用户去系统设置页面手动开启权限。这是用户体验的关键——不要让用户卡在一个无法操作的状态。

import { common } from '@kit.AbilityKit'

private showPermissionDeniedDialog(): void {
  AlertDialog.show({
    title: '需要相机权限',
    message: '您已拒绝相机权限,请在系统设置中手动开启,否则无法使用拍照功能',
    primaryButton: {
      value: '去设置',
      action: () => {
        this.openPermissionSettings()
      }
    },
    secondaryButton: {
      value: '取消',
      action: () => {}
    }
  })
}

private openPermissionSettings(): void {
  let context = this.getUIContext().getHostContext() as common.UIAbilityContext
  // 尝试打开应用详情页
  let want = {
    action: 'action.system.settings.application.info',
    parameters: {
      settingsParamBundleName: context.abilityInfo.bundleName,
    },
  }
  context.startAbility(want)
}

不过打开系统设置页面的具体方式可能因系统版本不同而变化,建议测试时验证一下。有些版本用action.system.settings.application.info可以打开应用详情页,用户可以直接在那里开启权限。


实战:完整的权限管理工具类

我把权限相关的逻辑封装成了一个工具类,各个页面都可以复用。这样不用每个页面都写一堆权限申请的代码:

// PermissionUtil.ets
import { abilityAccessCtrl, bundleManager, Permissions, common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'

export class PermissionUtil {
  // 检查单个权限(同步版本,适合在aboutToAppear中使用)
  static checkPermission(permission: Permissions): boolean {
    let atManager = abilityAccessCtrl.createAtManager()
    let bundleInfo = bundleManager.getBundleInfoForSelfSync(
      bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
    )
    let tokenId = bundleInfo.appInfo.accessTokenId
    let grantStatus = atManager.checkAccessTokenSync(tokenId, permission)
    return grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
  }

  // 请求单个权限
  static async requestPermission(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    let atManager = abilityAccessCtrl.createAtManager()
    let permissions: Array<Permissions> = [permission]
    try {
      let result = await atManager.requestPermissionsFromUser(context, permissions)
      return result.authResults[0] === 0
    } catch (err) {
      let e = err as BusinessError
      console.error(`请求权限失败: ${e.code} - ${e.message}`)
      return false
    }
  }

  // 请求多个权限
  static async requestMultiplePermissions(
    context: common.UIAbilityContext,
    permissions: Array<Permissions>
  ): Promise<Map<Permissions, boolean>> {
    let atManager = abilityAccessCtrl.createAtManager()
    let resultMap = new Map<Permissions, boolean>()
    try {
      let result = await atManager.requestPermissionsFromUser(context, permissions)
      for (let i = 0; i < permissions.length; i++) {
        resultMap.set(permissions[i], result.authResults[i] === 0)
      }
    } catch (err) {
      let e = err as BusinessError
      console.error(`请求权限失败: ${e.code} - ${e.message}`)
    }
    return resultMap
  }

  // 检查并请求(便捷方法:先检查,未授权再申请)
  static async ensurePermission(
    context: common.UIAbilityContext,
    permission: Permissions
  ): Promise<boolean> {
    if (PermissionUtil.checkPermission(permission)) {
      return true  // 已经授权,不需要申请
    }
    return await PermissionUtil.requestPermission(context, permission)
  }
}

在页面中使用这个工具类:

import { PermissionUtil } from '../utils/PermissionUtil'
import { common } from '@kit.AbilityKit'

@Entry
@Component
struct CameraPage {
  @State hasCameraPermission: boolean = false

  aboutToAppear(): void {
    // 页面加载时检查权限状态
    this.hasCameraPermission = PermissionUtil.checkPermission('ohos.permission.CAMERA')
  }

  build() {
    Column({ space: 20 }) {
      if (this.hasCameraPermission) {
        // 已授权,显示相机功能
        Text('相机权限已授权,可以使用相机功能')
          .fontSize(16)
          .fontColor('#333')

        Button('拍照')
          .onClick(() => {
            this.takePhoto()
          })
      } else {
        // 未授权,显示申请按钮
        Text('相机权限未授权,点击下方按钮申请')
          .fontSize(16)
          .fontColor('#999')

        Button('请求相机权限')
          .onClick(async () => {
            let context = this.getUIContext().getHostContext() as common.UIAbilityContext
            this.hasCameraPermission = await PermissionUtil.requestPermission(
              context,
              'ohos.permission.CAMERA'
            )
            if (!this.hasCameraPermission) {
              this.showGoToSettingsDialog()
            }
          })
      }
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }

  private takePhoto(): void {
    console.info('开始拍照...')
  }

  private showGoToSettingsDialog(): void {
    AlertDialog.show({
      title: '权限不足',
      message: '请在系统设置中开启相机权限后重试',
      primaryButton: {
        value: '去设置',
        action: () => {
          // 打开设置页面
        }
      },
      secondaryButton: {
        value: '取消',
        action: () => {}
      }
    })
  }
}

ensurePermission这个便捷方法结合了"先检查再申请"的逻辑——如果已授权就直接返回true,如果未授权就弹窗申请。但它避免了前面说的"拒绝且不再询问"的陷阱——因为不管申请结果如何,都返回明确的boolean值,你可以根据结果决定后续操作。


在这里插入图片描述

常见踩坑汇总

坑1:只声明不申请

  • user_grant权限必须在代码中调用requestPermissionsFromUser
  • 光在json5里声明不够,运行时调用相关API会报权限错误
  • 这是审核被拒最常见的原因

坑2:reason直接写中文

  • 必须用$string:资源引用
  • 直接写中文原文审核不通过
  • 我因为这个被拒过

坑3:reason写得太笼统

  • "需要使用相机"太笼统
  • "需要使用相机拍照以完成头像设置"才够具体
  • reason要跟应用功能匹配,审核人员会看

坑4:先checkAccessToken再requestPermissionsFromUser

  • 用户拒绝且不再询问后,request不再弹窗
  • 你无法区分"弹了窗被拒绝"和"没弹窗直接拒绝"
  • 建议:不管什么情况,拒绝就引导去设置

坑5:申请LOCATION但不申请APPROXIMATELY_LOCATION

  • LOCATION依赖APPROXIMATELY_LOCATION
  • 必须同时申请或先申请APPROXIMATELY_LOCATION
  • 只申请LOCATION可能失败

坑6:一次申请太多权限

  • 用户容易烦,全部拒绝的概率增加
  • 按功能分批申请,最多3个一组
  • 优先申请当前功能必须的权限

坑7:不处理用户拒绝的情况

  • 用户拒绝后要给提示,引导去设置
  • 不能什么都不做就卡在那
  • 否则用户面对一个什么都不能操作的页面很困惑

坑8:忘记import Permissions类型

  • Permissions类型需要从@kit.AbilityKitimport
  • 不import的话Array<Permissions>类型标注会报错
  • BusinessError需要从@kit.BasicServicesKitimport

坑9:checkAccessToken需要tokenId

  • tokenId不是context本身,需要从bundleInfo获取
  • bundleManager.getBundleInfoForSelfSync要传GET_BUNDLE_INFO_WITH_APPLICATIONflag
  • 不传flag的话appInfo可能为空

坑10:requestPermissionsFromUser需要UIAbilityContext

  • 不是所有context都行,需要UIAbilityContext
  • 在Component中通过this.getUIContext().getHostContext() as common.UIAbilityContext获取

权限管理这块虽然逻辑不复杂,但每个细节都跟审核挂钩。我第一版应用被拒了三次才搞对,每次被拒的理由都不一样但都是权限相关。建议大家提交审核前,把每个user_grant权限都跑一遍完整流程:声明→申请→授权→使用→拒绝→引导设置,确保每条路径都正常。不要像我一样,提交了才发现遗漏。

我还想强调一点——权限申请不仅仅是技术问题,更是产品设计问题。好的权限申请策略应该是"按需申请、合理说明、优雅降级"。"按需申请"就是只在用户用到功能时才申请权限,不要一启动就弹一堆。"合理说明"就是reason要写得清楚具体,让用户理解为什么需要这个权限。"优雅降级"就是用户拒绝权限后,应用不应该崩溃或卡死,而应该给出替代方案或者友好提示。

这三个原则做到了,审核基本没问题,用户体验也好。


实用要点回顾:

  1. normal权限安装时自动授予,user_grant权限必须运行时申请
  2. requestPermissionsFromUser是运行时申请权限的核心API
  3. authResults数组:0=授权,-1=拒绝
  4. json5的reason必须用$string:资源引用,内容要具体
  5. checkAccessToken/checkAccessTokenSync用于检查当前权限状态
  6. 推荐try→fail→request→retry模式,不要一上来就申请权限
  7. 定位权限要先申请APPROXIMATELY_LOCATION再申请LOCATION
  8. 用户拒绝后引导去系统设置页面手动开启
  9. 分批申请权限,每次不超过3个
  10. 封装权限工具类便于复用
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐