HarmonyOS 应用做上架自查时,权限最容易被轻视。很多人以为只要在module.json5里把权限声明上,再调用一次requestPermissionsFromUser,事情就结束了。
实际排查下来,真正容易出问题的不是“会不会调 API”,而是这几件事有没有对上:
- 权限是不是当前功能真的要用;
- 用户点功能之前,有没有先说明用途;
- 用户拒绝后,页面有没有兜底路径;
- 隐私声明、权限用途、代码里的申请时机是不是一致。
这篇只讲一个具体问题:权限申请应该放在什么时机,怎么写才更稳。下面的例子按 HarmonyOS 5.0.0+ 的 Stage 模型来拆,核心围绕requestPermissionsFromUser、权限声明和上架审核自查。
先看容易出问题的写法
有些项目会在应用启动时直接申请一批权限:
// 不推荐:应用刚打开就申请一堆权限asyncfunctionrequestOnAppStart(context:common.UIAbilityContext){constatManager=abilityAccessCtrl.createAtManager();awaitatManager.requestPermissionsFromUser(context,['ohos.permission.CAMERA','ohos.permission.READ_MEDIA','ohos.permission.LOCATION']);}这个写法看起来省事,但问题很明显:用户刚打开应用,还不知道你为什么要相机、相册、定位,系统弹窗先出来了。
如果功能页里只有“扫描菜谱图片”需要相机,那启动时就申请相机是不合适的;如果用户只是浏览页面,根本没用到图片识别或拍照入口,也不应该被权限弹窗打断。
上架审核时也容易卡在这里:你在隐私声明里写了“用于扫描菜谱图片”,但代码在启动阶段就申请了权限。审核视角会继续追问:为什么启动就要?没有使用这个功能时为什么也要?
正确拆法:先判断,再触发,再兜底
我更倾向把权限流程拆成三段。
第一段,页面先告诉用户这个功能要做什么。比如“扫描菜谱图片需要使用相机,用来识别图片里的食材”。这一步不要马上弹系统权限框。
第二段,用户点击“扫描”按钮以后,再检查权限状态。如果没有授权,再调用requestPermissionsFromUser。
第三段,用户拒绝以后,不要直接把页面卡死。可以给手动导入图片、文字输入、跳转设置页说明这些兜底方式。
代码可以按这个方向封装:
import{abilityAccessCtrl,common,Permissions}from'@kit.AbilityKit';typePermissionResult='granted'|'denied'|'unknown';constCAMERA_PERMISSION:Permissions='ohos.permission.CAMERA';asyncfunctionrequestCameraWhenFeatureTriggered(context:common.UIAbilityContext):Promise<PermissionResult>{constatManager=abilityAccessCtrl.createAtManager();constresult=awaitatManager.requestPermissionsFromUser(context,[CAMERA_PERMISSION]);constindex=result.permissions.indexOf(CAMERA_PERMISSION);if(index<0){return'unknown';}returnresult.authResults[index]===0?'granted':'denied';}这里重点不是这几行代码有多复杂,而是职责边界更清楚:
module.json5负责声明应用可能会用到的权限;- 功能入口负责解释为什么要用;
requestPermissionsFromUser只在用户触发功能时调用;- 页面负责处理授权、拒绝和异常结果。
案例一:扫描图片功能怎么处理
假设页面上有一个“扫描菜谱图片”的入口。用户点击之前,不申请权限,只展示功能说明。
asyncfunctiononTapScanRecipe(context:common.UIAbilityContext){constresult=awaitrequestCameraWhenFeatureTriggered(context);if(result==='granted'){openCameraScanner();return;}if(result==='denied'){showPermissionFallback({title:'相机权限没有打开',message:'你还可以手动导入图片,或者到系统设置里打开相机权限。',primaryAction:'手动导入图片',secondaryAction:'查看设置说明'});return;}showPermissionFallback({title:'暂时无法确认相机权限',message:'可以先用文字输入食材,后面再重新尝试扫描。',primaryAction:'改用文字输入'});}这个流程的好处是,用户知道为什么弹权限,也知道拒绝以后还能怎么继续。上架自查时,隐私声明里的“相机用于扫描菜谱图片”也能和功能入口对上。
本地用一个小脚本模拟了三种结果:
{"unknown":{"action":"request-when-user-taps-feature","fallback":"explain-why-before-system-dialog","reviewRisk":"medium"},"denied":{"action":"show-manual-guide","fallback":"use-imported-file-or-text-input","reviewRisk":"low"},"granted":{"action":"open-feature","fallback":null,"reviewRisk":"low"}}这个验证不是为了模拟系统弹窗,而是验证页面决策不会只剩“授权成功”一条路。权限被拒绝、授权状态异常时,页面仍然有可继续操作的入口。
案例二:相册导入不要跟相机混在一起
第二个常见问题,是把相机和相册权限绑在同一个按钮里申请。
比如用户只是想从相册选一张图,代码却同时申请相机权限:
// 不推荐:用户只想选图,却顺手申请 CAMERAawaitatManager.requestPermissionsFromUser(context,['ohos.permission.CAMERA','ohos.permission.READ_MEDIA']);这会让权限用途变得很难解释。更稳的方式是把入口拆开:
- “拍照扫描”入口只处理相机;
- “从相册导入”入口只处理媒体读取或系统 Picker;
- 如果系统 Picker 已经能满足场景,就优先用 Picker 减少权限打扰。
页面代码可以写成两个独立分支:
asyncfunctiononTapTakePhoto(context:common.UIAbilityContext){constresult=awaitrequestCameraWhenFeatureTriggered(context);if(result==='granted'){openCameraScanner();}else{showCameraFallback();}}asyncfunctiononTapImportImage(){// 能用系统 Picker 解决的,就不要把相机权限绑进来constimageUri=awaitpickImageFromSystemPicker();if(imageUri){startImageRecognize(imageUri);}}这样做以后,代码、页面和隐私说明会更容易对齐:
| 场景 | 触发入口 | 申请什么 | 拒绝后怎么处理 |
|---|---|---|---|
| 拍照扫描 | 用户点“拍照扫描” | 相机权限 | 手动导入或文字输入 |
| 相册导入 | 用户点“从相册导入” | 优先系统 Picker | 返回页面,不打断其它功能 |
| 普通浏览 | 用户只看列表 | 不申请权限 | 不弹窗 |
为什么我选择这种拆法
权限申请有几种写法:
| 写法 | 好处 | 问题 |
|---|---|---|
| 启动时统一申请 | 代码省事 | 用户不理解,审核说明难对齐 |
| 进入页面就申请 | 比启动时好一点 | 用户可能只是看看页面,仍然太早 |
| 点击具体功能再申请 | 用途最清楚 | 要多写状态和兜底 |
| 尽量使用系统 Picker 或安全控件 | 少打扰用户 | 需要按功能拆入口 |
我会优先选“点击具体功能再申请”。它不是最省代码的方案,但最容易解释,也最容易自查。
上架前我会用这张清单过一遍:
module.json5里声明的权限,页面里是否真的有对应功能;- 权限用途说明是否和隐私声明一致;
- 用户没点功能时,是否不会提前弹权限;
- 用户拒绝后,是否不会强制退出或卡死;
- 相机、相册、定位这类权限有没有被混在一个无关入口里申请;
- 日志里能否看出用户走到了授权、拒绝还是兜底路径。
封装成项目里的规则
最后可以把它沉淀成一个项目规则:页面不要直接散落调用requestPermissionsFromUser,而是通过一个权限服务统一收口。
exportclassPermissionService{constructor(privatecontext:common.UIAbilityContext){}asyncrequestCameraForScan():Promise<PermissionResult>{returnrequestCameraWhenFeatureTriggered(this.context);}canFallbackToManualInput(result:PermissionResult):boolean{returnresult!=='granted';}}页面只关心结果:
constresult=awaitpermissionService.requestCameraForScan();if(result==='granted'){openCameraScanner();}elseif(permissionService.canFallbackToManualInput(result)){openManualInputPanel();}这样以后再加 OCR、图片识别、扫码、定位推荐,也不用每个页面都重新写一套权限判断。更重要的是,上架自查时可以直接从入口、权限服务、隐私声明三处核对,不会一查才发现申请时机和说明对不上。
最后沉淀成一句检查规则
权限申请不要只问“能不能弹出来”,还要问“为什么现在弹、为什么要这个权限、拒绝后还能不能继续”。
如果这三个问题都能回答清楚,requestPermissionsFromUser这类权限申请代码才算真的写稳了。