在 HarmonyOS 应用开发中,权限管理是保障用户隐私和系统安全的核心环节 —— 应用访问摄像头、麦克风、相册等敏感资源时,必须遵循鸿蒙的权限申请规范。直接硬编码权限申请逻辑会导致代码冗余、维护困难,而封装通用的权限工具类能大幅提升开发效率。本文将从鸿蒙权限核心概念入手,详解如何封装 “请求授权 + 二次引导授权” 的通用工具类,并以麦克风权限为例,演示完整的权限申请流程。

一、鸿蒙权限核心概念(必掌握)

鸿蒙将权限分为系统授权(system_grant)用户授权(user_grant) 两类,敏感权限(与用户隐私强相关)必须走用户授权流程,且需满足严格的配置要求。

1. 权限类型区分

权限类型 授权方式 适用场景
system_grant 应用安装时系统自动预授予 非敏感权限(如 INTERNET、ACCESS_NETWORK_STATE)
user_grant 运行时向用户申请,需用户手动确认 敏感权限(麦克风、相机、相册、地理位置等)

2. 敏感权限配置要求

申请敏感权限(如麦克风、相机)时,必须在配置文件中满足两个核心要求:

  • 声明reason:权限使用理由(需通过字符串资源引用,不可硬编码);
  • 配置usedScene:权限使用的场景(如关联的 Ability)。

敏感权限清单:地理位置、相机、麦克风、日历、健身运动、身体传感器、音乐、文件、图片视频等。

二、第一步:配置权限声明(module.json5)

在应用的module.json5配置文件中声明所需权限,区分 system_grant 和 user_grant 类型,以下是 INTERNET(系统授权)和 MICROPHONE(用户授权)的配置示例:

1. 权限声明节点

json

{
  "module": {
    // 其他配置(如package、name)省略
    "requestPermissions": [
      // 示例1:system_grant类型(无需用户授权,安装即授予)
      {
        "name": "ohos.permission.INTERNET"
      },
      // 示例2:user_grant类型(敏感权限,需配置reason和usedScene)
      {
        "name": "ohos.permission.MICROPHONE",
        "reason": "$string:permission_microphone", // 权限使用理由(引用字符串资源)
        "usedScene": {
          "abilities": ["EntryAbility"], // 关联的Ability
          "when": "always" // 权限使用时机(always:始终需要)
        }
      }
    ]
  }
}

2. 配置权限理由字符串(string.json)

src/main/resources/base/element/string.json中添加权限理由的字符串资源,避免硬编码,同时支持多语言适配:

json

{
  "string": [
    // 其他字符串资源省略
    {
      "name": "permission_microphone",
      "value": "录音功能需要使用麦克风权限,仅用于音频录制,不会收集其他信息"
    }
  ]
}

三、核心:封装通用权限工具类

重复的权限申请逻辑(创建 AtManager、获取上下文、判断授权结果)会增加代码冗余,因此封装一个通用的Permission工具类,提供 “请求用户授权” 和 “拉起权限设置页” 两个核心能力,适配所有 user_grant 类型的权限。

1. 工具类完整代码

typescript

运行

// utils/PermissionUtil.ets
import { abilityAccessCtrl, Permissions } from '@kit.AbilityKit';
import { AppStorageV2 } from '@kit.ArkUI';
import { SavedContext } from '../../models/SavedContext'; // 需根据项目路径调整

/**
 * 通用权限工具类
 * 提供:请求用户授权、拉起权限设置页能力
 */
class PermissionUtil {
  /**
   * 请求用户授权(首次授权)
   * @param permissions 权限列表(如['ohos.permission.MICROPHONE'])
   * @returns boolean 所有权限是否都授权成功
   */
  async requestPermissions(permissions: Permissions[]): Promise<boolean> {
    try {
      // 1. 创建权限管理实例
      const atManager = abilityAccessCtrl.createAtManager();
      // 2. 获取应用上下文(适配AppStorageV2存储的上下文)
      const ctx = AppStorageV2.connect(SavedContext)!.context;
      
      if (!ctx) {
        console.error('权限申请失败:上下文为空');
        return false;
      }

      // 3. 向用户发起权限申请
      const result = await atManager.requestPermissionsFromUser(ctx, permissions);
      
      // 4. 判断所有权限是否都授权成功(every:全部为GRANTED才返回true)
      return result.authResults.every(
        (result) => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );
    } catch (error) {
      console.error('请求权限异常:', error);
      return false;
    }
  }

  /**
   * 拉起系统权限设置页(二次授权引导)
   * @param permissions 权限列表
   * @returns boolean 跳转设置页后,权限是否已授权
   */
  async openPermissionSetting(permissions: Permissions[]): Promise<boolean> {
    try {
      const atManager = abilityAccessCtrl.createAtManager();
      const ctx = AppStorageV2.connect(SavedContext)!.context;
      
      if (!ctx) {
        console.error('打开权限设置失败:上下文为空');
        return false;
      }

      // 调用系统API,拉起权限设置页
      const authResults = await atManager.requestPermissionOnSetting(ctx, permissions);
      
      // 判断权限是否已授权
      return authResults.every(
        (result) => result === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );
    } catch (error) {
      console.error('打开权限设置异常:', error);
      return false;
    }
  }
}

// 导出单例实例(避免重复创建)
export const permissionUtil = new PermissionUtil();

2. 工具类核心解析

  • 单例模式:导出permissionUtil单例实例,避免每次使用都创建新对象,节省内存;
  • 上下文获取:通过AppStorageV2.connect(SavedContext)获取全局上下文,适配多页面 / 组件的权限申请场景;
  • 结果判断:使用every方法判断所有权限是否授权成功,适配多权限批量申请场景;
  • 异常捕获:包裹try/catch捕获上下文为空、权限 API 调用失败等异常,避免应用崩溃;
  • 通用性:参数为Permissions[]类型,支持传入任意 user_grant 类型的权限列表(如麦克风 + 相机组合)。

四、实战:使用工具类申请麦克风权限

以音频录制页面(AudioPage)为例,演示如何使用封装的permissionUtil实现 “首次申请授权→未授权则引导至设置页” 的完整流程。

1. 页面代码实现

typescript

运行

// pages/AudioPage.ets
import { permissionUtil } from '../../utils/PermissionUtil'; // 导入权限工具类
import { promptAction, getContext } from '@kit.ArkUI';
import { abilityAccessCtrl } from '@kit.AbilityKit';

@Component
struct AudioPage {
  // 页面即将显示时申请权限(推荐时机:功能使用前,而非页面加载时)
  async aboutToAppear(): Promise<void> {
    await this.requestMicrophonePermission();
  }

  /**
   * 申请麦克风权限(首次+二次引导)
   */
  async requestMicrophonePermission() {
    // 1. 定义需要申请的权限列表
    const permissionList = ['ohos.permission.MICROPHONE'] as const;

    // 2. 首次请求用户授权
    const isGranted = await permissionUtil.requestPermissions(permissionList);
    
    if (isGranted) {
      // 授权成功:执行录音相关逻辑
      promptAction.showToast({ message: '麦克风权限授权成功' });
      this.initAudioRecord();
    } else {
      // 授权失败:提示用户并引导至设置页
      promptAction.showToast({ message: '麦克风权限未授权,无法使用录音功能,请前往设置页开启' });
      
      // 3. 二次引导:拉起系统权限设置页
      const isSettingGranted = await permissionUtil.openPermissionSetting(permissionList);
      
      if (isSettingGranted) {
        promptAction.showToast({ message: '已成功开启麦克风权限' });
        this.initAudioRecord();
      } else {
        promptAction.showToast({ message: '仍未开启麦克风权限,录音功能无法使用' });
        // 可选:返回上一页/禁用录音按钮
        // router.back();
      }
    }
  }

  /**
   * 初始化录音功能(权限授权成功后执行)
   */
  initAudioRecord() {
    console.log('初始化录音功能');
    // 此处编写录音相关逻辑(如创建AudioRecorder实例)
  }

  build() {
    Column({ space: 20 }) {
      Text('音频录制页面')
        .fontSize(20)
        .fontWeight(600);
      
      // 录音按钮(权限未授权时禁用)
      Button('开始录音')
        .disabled(false) // 可根据权限状态动态禁用
        .onClick(() => {
          // 录音逻辑
        });
    }
    .padding(40)
    .height('100%')
    .width('100%')
    .justifyContent(FlexAlign.Center);
  }
}

// 页面入口Builder
@Builder
export function AudioBuilder() {
  AudioPage();
}

2. 实战流程解析

  1. 权限申请时机:在aboutToAppear中申请,但更推荐在 “点击录音按钮” 时申请(避免页面加载时频繁弹授权框,提升用户体验);
  2. 首次授权:调用permissionUtil.requestPermissions向用户发起授权请求,系统弹出授权弹窗;
  3. 结果判断
    • 授权成功:初始化录音功能;
    • 授权失败:提示用户并调用permissionUtil.openPermissionSetting拉起系统权限设置页;
  4. 二次授权验证:用户在设置页操作后,判断权限是否已开启,给出对应提示。

五、关键避坑指南

1. 上下文获取失败

  • 错误原因:AppStorageV2.connect(SavedContext)!.context为空,或在非 UI 线程获取上下文;
  • 解决方案:
    • 确保SavedContext已正确存储应用上下文(如在 EntryAbility 中初始化);
    • 临时场景可使用getContext(this)(组件内)替代全局上下文:

      typescript

      运行

      // 组件内临时获取上下文的写法(工具类可扩展支持传入上下文)
      const ctx = getContext(this);
      const result = await atManager.requestPermissionsFromUser(ctx, permissionList);
      

2. 权限名称错误

  • 错误示例:将ohos.permission.MICROPHONE写成ohos.permission.MIC
  • 解决方案:参考鸿蒙官方权限清单,确保权限名称完全一致,敏感权限名称不可简写。

3. 二次授权引导时机不当

  • 错误做法:用户首次拒绝后立即拉起设置页,体验差;
  • 解决方案:先通过promptAction告知用户 “权限的用途”,再询问是否前往设置页,示例:

    typescript

    运行

    // 优化后的二次授权引导
    promptAction.showDialog({
      title: '权限提示',
      message: '录音功能需要麦克风权限,是否前往设置页开启?',
      buttons: [
        { text: '取消' },
        { text: '前往设置' }
      ]
    }).then((result) => {
      if (result.index === 1) {
        permissionUtil.openPermissionSetting(permissionList);
      }
    });
    

4. 忽略权限申请结果的异步性

  • 错误做法:同步判断授权结果(未 await);
  • 解决方案:所有权限申请方法都是异步的,必须使用await等待结果返回。

5. 配置文件缺失 reason/usedScene

  • 错误现象:敏感权限申请弹窗不显示,或授权后仍无法使用;
  • 解决方案:严格按照要求配置reason(引用字符串资源)和usedScene(关联 Ability)。

六、扩展优化建议

1. 支持多权限批量申请

工具类已支持传入权限列表,可直接申请多个权限,示例:

typescript

运行

// 同时申请麦克风+相机权限
const permissionList = ['ohos.permission.MICROPHONE', 'ohos.permission.CAMERA'] as const;
const isAllGranted = await permissionUtil.requestPermissions(permissionList);

2. 封装权限状态检查方法

新增 “检查权限是否已授权” 的方法,避免重复申请已授权的权限:

typescript

运行

// PermissionUtil.ts中新增方法
async checkPermissions(permissions: Permissions[]): Promise<boolean> {
  const atManager = abilityAccessCtrl.createAtManager();
  const ctx = AppStorageV2.connect(SavedContext)!.context;
  if (!ctx) return false;
  
  // 获取当前权限授权状态
  const statusMap = await atManager.checkPermissions(ctx, permissions);
  return Object.values(statusMap).every(
    (status) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
  );
}

// 使用示例:先检查,未授权再申请
if (!await permissionUtil.checkPermissions(permissionList)) {
  await this.requestMicrophonePermission();
}

3. 统一权限提示语

将权限提示语封装到字符串资源中,统一管理,提升可维护性。

七、总结

本文基于鸿蒙abilityAccessCtrl模块封装了通用的权限工具类,实现了 “首次授权申请 + 二次授权引导” 的完整能力,核心要点可总结为:

  1. 权限配置:敏感权限必须配置reasonusedScene,且reason需引用字符串资源;
  2. 工具封装:将重复的权限申请逻辑封装为工具类,提升代码复用性和可维护性;
  3. 体验优化:首次拒绝授权后,先告知权限用途,再引导至设置页,避免强制跳转;
  4. 异步处理:所有权限操作都是异步的,必须使用await等待结果,避免同步判断错误。

这套权限申请方案适配鸿蒙所有 user_grant 类型的敏感权限,不仅适用于麦克风,还可直接迁移到相机、相册、地理位置等权限申请场景,是鸿蒙应用权限管理的最优实践之一。

Logo

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

更多推荐