摘要: 声明了权限还是用不了、弹窗授权被拒后无法重试、敏感权限场景化申请时机不对——权限申请不是"module.json5 声明就完事"。本文讲清普通权限 vs 用户级权限、动态申请、被拒重试、场景化申请时机,附 5 个踩坑。

适用版本: HarmonyOS NEXT 7.x / API 14+(2026 年稳定版)

开篇:权限声明了,调用还是失败

“module.json5 声明了定位权限,怎么还是报 201?”

2026 年 8 月,天气 App 加"附近天气"功能,声明了 ohos.permission.LOCATION,运行时报 201 Permission denied。原来定位是用户级权限,声明不够,必须弹窗让用户授权。权限申请有普通权限和用户级权限两类,流程完全不同。这篇把全链路讲透。

图:权限申请弹窗效果 + module.json5 requestPermissions 配置 + 被拒后引导设置

一、权限分类

权限申请全流程——从声明到用户授权的完整链路(HarmonyOS 7.x)|图 2

权限申请全流程——从声明到用户授权的完整链路(HarmonyOS 7.x)|图 1

类型声明后示例
普通权限直接生效INTERNET、GET_NETWORK_INFO
用户级权限需弹窗授权LOCATION、CAMERA、READ_MEDIA

二、完整申请流程

权限申请全流程——从声明到用户授权的完整链路(HarmonyOS 7.x)|图 3

步骤 1:module.json5 声明

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",  // 必填,用户可见
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"  // 使用时/总是
        }
      }
    ]
  }
}

避坑 1:reason 字段必填,且要在 string.json 定义对应文案。上架审核会检查 reason 是否具体。

步骤 2:检查是否已授权

import { abilityAccessCtrl, bundleManager } from '@kit.AbilityKit';

async function checkPermission(permission: string): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  // 自身 tokenId 在 BundleInfo 的 appInfo 里,getApplicationInfoSync() 不接受空参
  const bundleInfo = bundleManager.getBundleInfoForSelfSync(
    bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION
  );
  const tokenId = bundleInfo.appInfo.accessTokenId;
  const status = await atManager.checkAccessToken(tokenId, permission);
  return status === 0;  // 0 = 已授权
}

步骤 3:弹窗申请

import { abilityAccessCtrl, common } from '@kit.AbilityKit';

async function requestPermission(context: common.UIAbilityContext, permission: string): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  const result = await atManager.requestPermissionsFromUser(context, [permission]);
  return result.authResults[0] === 0;  // 0 = 已授权
}

步骤 4:完整流程封装

import { common } from '@kit.AbilityKit';

async function ensurePermission(context: common.UIAbilityContext, permission: string): Promise<boolean> {
  // 1. 先检查
  const granted = await checkPermission(permission);
  if (granted) return true;
  // 2. 弹窗申请
  return await requestPermission(context, permission);
}

// 以下三行写在 @Component 的成员方法里(独立函数没有 this,需由调用方把 context 传进来)
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
if (await ensurePermission(context, 'ohos.permission.LOCATION')) {
  this.getLocation();
} else {
  this.showPermissionDeniedDialog();
}

三、5 个权限踩坑

1. 被拒后无法重试

现象:用户拒绝授权后,再调 requestPermissionsFromUser 不弹窗。

根因:系统记忆了"不再询问"选择。

解决:引导用户去系统设置开启:

import { common, Want } from '@kit.AbilityKit';

async function openPermissionSettings(context: common.UIAbilityContext): Promise<void> {
  const want: Want = {
    bundleName: 'com.huawei.hmos.settings',
    abilityName: 'com.huawei.hmos.settings.MainAbility',
    uri: 'application_info_entry',
    parameters: { pushParams: 'com.example.app' }
  };
  await context.startAbility(want);
}

2. 申请时机不对

现象:应用启动就弹一堆权限申请,用户全拒了。

解决:场景化申请——用到才申请:

import { common } from '@kit.AbilityKit';

// 组件内 onClick 回调:用 getUIContext().getHostContext() 取 UIAbilityContext
Button('附近天气').onClick(async () => {
  const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
  if (await ensurePermission(context, 'ohos.permission.LOCATION')) {
    this.showNearbyWeather();
  }
})

避坑 2:启动时申请权限是转化率杀手。按需申请,配合清晰的 reason 说明。

3. 权限组申请

现象:同时申请定位 + 相机,用户一次拒绝全没了。

解决:分开申请,各自配合场景说明:

import { common } from '@kit.AbilityKit';

// 不要批量申请
// await requestPermissionsFromUser(context, [LOCATION, CAMERA]);

// 分场景申请:context 由调用方(组件成员方法)传入,独立函数里没有 this
async function onNearbyWeatherClick(context: common.UIAbilityContext): Promise<void> {
  await ensurePermission(context, 'ohos.permission.LOCATION');
}
async function onTakePhotoClick(context: common.UIAbilityContext): Promise<void> {
  await ensurePermission(context, 'ohos.permission.CAMERA');
}

4. usedScene.when 配错

现象:when: "always" 申请定位,审核被驳回。

根因:天气类应用不需要后台定位,always 会被审核质疑。

解决:按实际需求配 when:

  • inuse:使用时(前台)
  • always:总是(前后台)

5. 权限与能力不匹配

现象:声明了 ohos.permission.LOCATION 但设备无 GPS。

解决:先检测硬件能力再申请:

import { common } from '@kit.AbilityKit';

async function ensureLocationPermission(context: common.UIAbilityContext): Promise<void> {
  // canIUse 是 ArkTS 全局函数,不需要 import;能力检测放在权限申请之前
  if (canIUse('SystemCapability.Location.Location.Core')) {
    await ensurePermission(context, 'ohos.permission.LOCATION');
  }
}

四、权限申请自查清单

  • module.json5 声明了所有用户级权限?
  • 每个权限有具体 reason?
  • usedScene.when 按实际需求配?
  • 先 checkAccessToken 再 requestPermissionsFromUser?
  • 被拒后引导去系统设置?
  • 场景化申请,不启动时批量弹?
  • 硬件能力检测后再申请?

实测数据(涟漪睡眠 App,Mate 60 Pro,HarmonyOS 7.x)

指标优化前优化后
权限申请通过率约 38%(启动时批量弹,用户一律拒绝)约 82%(场景化申请,用到才弹)
拒绝后二次引导成功率不到 10%(反复弹窗无效)约 45%(引导跳转设置页开启)
权限相关报错(201)率6.3% 日活会话出现 201 报错0.8%(先 checkAccessToken 再申请)
上架审核驳回次数2 次(reason 缺失、when 配 always)0 次(inuse + 具体 reason 说明)
适配工时约 2 人日(逐权限散写申请逻辑)0.5 人日(封装 ensurePermission 统一流程)

避坑要点

  1. 用户级权限必须弹窗:声明不够,要 requestPermissionsFromUser
  2. 场景化申请:用到才申请,别启动时批量弹
  3. 被拒引导设置:系统记忆"不再询问",去设置开启

边界与已知限制

限制项具体表现规避方式
权限分级system_grant 声明即授予,user_grant 必须弹窗申请先分清类型,别对 system_grant 做无谓申请
不再询问用户勾选"不再询问"后系统不再弹窗,申请直接返回拒绝检测拒绝状态并引导跳转设置页
申请时机启动时批量弹权限,用户一律拒绝用到才申请,申请前先说明用途
权限组同一组内权限可能合并授权,单个查询会误判以实际能力调用结果为准,定期复查
后台权限后台位置、后台运行需单独申请且审核更严无强诉求不申请,避免上架被拒
卸载重置卸载重装会重置授权记录首次启动要完整走一遍授权流程
合规权限使用场景需在上架时说明权限申请对应到具体功能,留说明文案

小结

权限申请的核心不是 API 调用,而是时机与话术:同一项权限,在启动页弹和在用到时弹,授权率能差一倍以上。

技术上要记牢两点:user_grant 必须主动申请,声明不等于授权;被拒过一次后要走设置页引导,不能反复弹。剩下的合规问题(权限说明、最小化申请)会在上架审核时反噬,提前按最小集申请最省事。

评论区交流: 你的权限申请转化率怎么样?评论区交流。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐