一个选图功能,在 Android 上装个权限就完事,在鸿蒙上却能让应用审核卡住两周。
原因不在代码写错,而在鸿蒙把「读相册」这件事从「一个权限」拆成了「两条完全不同的路」,走错路不是报错,而是审核驳回,提示还只有一句话。
这篇把这两条路说清楚,以及为什么很多人第一次一定走错。
一、先分清两件不同的事
绝大多数人栽在第一步:把「我要读相册」当成一个动作。在鸿蒙里它是两个:
第一件:我要选一张图给用户上传。 用户点按钮,弹系统选图界面,选完把文件交给你。
第二件:我要扫一遍相册,把里面的图都列出来。 自己画网格、自己做预览,用户在你的界面里挑。
这两件事的技术方案完全不同,权限要求也完全不同。混着理解,就会写出「既申请了完整权限、又调了系统选图器」的四不像,然后被驳回。
判断方法很简单:界面是系统画的还是你自己画的。 系统画的那条路,不需要相册权限;自己画的那条,才需要。
二、第一条路:系统选图器,不需要权限
这是鸿蒙从 API 12 起主推的方式,叫 Picker。
用户点头像 → 拉起系统相册选择器 → 用户选一张 → 系统把这张图的临时访问凭证交给应用。整个过程中,应用从来没有拿到过相册的读取权限,只拿到了用户主动选中的那一张(或几张)。
调用很短:
import {
photoAccessHelper } from '@kit.MediaLibraryKit';
async function pickOne(): Promise<string> {
const options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
const picker = new photoAccessHelper.PhotoViewPicker();
const result = await picker.select(options);
return result.photoUris.length > 0 ? result.photoUris[0] : '';
}
拿到的是 file://media/Photo/... 这种 URI,可以直接传给图片组件显示,也可以读成 ArrayBuffer 上传。
这条路的关键优势在审核侧: 不声明相册权限,就没有「过度索取权限」这条驳回理由。鸿蒙的应用审核对权限声明查得比 Android 严——你声明了,就得说明为什么,还得在用户拒绝后能用。
所以只要你的场景是「让用户上传一张图」,就走这条路,别去申请权限。
三、第二条路:自己画相册,才要权限
什么时候必须走第二条?两种情况:
要展示相册全貌。 比如做壁纸应用,得把用户相册里的图铺成网格让用户滑。
要按自己规则筛选。 比如做拼图,要一次读多张、按尺寸或时间分组。
这时候才需要申请权限。鸿蒙这里又分两个级别:
受限访问(推荐)。 用户授权后,应用只能读到用户勾选的那部分照片。系统会给一个「选择更多照片」的入口,用户想给更多就自己加。这个模式审核最容易过。
完整访问。 能读整个相册。审核时要写清必要性,一般只有相册管理类、备份类应用能过。
申请受限访问的写法:
import {
abilityAccessCtrl, common } from '@kit.AbilityKit';
async function requestPhotoAccess(context: common.UIAbilityContext) {
const atManager = abilityAccessCtrl.createAtManager();
const result = await atManager.requestPermissionsFromUser(context, [
'ohos.permission.READ_IMAGEVIDEO'
]);
return result.authResults[0] === 0;
}
注意 requestPermissionsFromUser 是拉起系统弹窗、等用户点,不是静默授予。用户点拒绝,返回非 0,这条路就走不通,你的界面必须有「没权限时怎么办」的降级。
四、权限要在配置文件里声明
只在代码里申请不够。module.json5 里必须声明,否则申请直接失败:
{
"module": {
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO",
"reason": "$string:photo_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
这里三个字段各有各的坑:
reason 必须是字符串资源引用,不能直接写中文。 直接写会编译失败。得在 resources/base/element/string.json 里定义 photo_reason,文案要说清「用来做什么」,不能写「需要相册权限」这种废话——审核会看这句。
when 只有两个值:inuse 和 always。 相册这类选 inuse。写 always 会额外被问「为什么要后台读相册」。
abilities 要写实际用到权限的 Ability 名。 写错了不报错,但申请时找不到对应场景。
五、五个被驳回的真实原因
按出现频率排。
1. 声明了权限,但功能里没用到。 最常见。比如用了系统 Picker 却还声明着 READ_IMAGEVIDEO,审核会判定「多余权限申请」。检查方法:全文搜权限名,看有没有对应调用。
2. reason 的文案说不清用途。 写「为了更好的体验」「需要此权限才能使用」这类,一律驳回。得写具体: 「用于选择头像图片并上传」。
3. 用户拒绝后应用不可用。 审核会模拟拒绝。如果你的界面在拒绝后白屏、报错、或者功能直接消失,判「未做降级处理」。正确做法是显示一句「无法访问相册,你可以从文件中选择」并提供替代入口。
4. 用完整访问做了只需受限访问的事。 只是想上传一张图,却申请了完整相册读取,判「最小必要原则不符」。
5. 权限和 Profile 里的授权对不上。 这个坑最隐蔽——代码和配置都对,但 AGC 上的 Profile 没勾对应权限,装得上、点就崩。表现是 hdc install 成功但功能一闪退。
六、审核驳回后怎么改
驳回通知通常只有一句话,比如「权限申请不合理」。要定位得自己比对三处:
第一处,看代码里到底调了什么。 是 Picker 还是自己遍历相册?这决定你该不该有权限。
第二处,看 module.json5 声明了什么。 有没有多声明、reason 是不是资源引用、when 对不对。
第三处,看 AGC 里 Profile 的权限清单。 有没有勾上,和配置文件是否一致。
三处一致才算通过。只要有一处不一致,表现都是「能装能跑但审核不过」或者「装得上但功能崩」,这两种现象看着完全不同,根子是同一个。
七、为什么鸿蒙要这么设计
Android 的存储权限是一个粗粒度的开关,给了就是整个相册。代价是用户不知道你读了哪些图。鸿蒙把这条路拆成「Picker(不授权)」和「受限访问(授权部分)」两条,是把选择权还给用户。
代价是开发者要多想一步:我这个功能,到底是「用户给我一张」还是「我要看用户全部」。
想不清这一步,就会本能地去申请最大权限,然后被审核挡回来。
八、自学这块容易卡在哪
第一处:不知道有 Picker 这条路。 从 Android 过来的人,第一反应是「申请读存储权限然后遍历」。搜到的老文章也多是这个思路。结果是写完了才发现审核不过,回头改要动整个图片选择模块。
第二处:reason 直接写中文,编译报错。 报错信息指向资源解析,不指向「你应该用资源引用」,第一次遇到会查很久。
第三处:以为「申请了权限就等于有了权限」。 用户拒绝、用户只授权部分照片、系统策略变更,三种情况都会让权限不可用。没有降级逻辑的应用在这三种情况下的表现是崩溃或者卡住,而审核一定会测。
第四处:三处配置对不上,但不知道要查三处。 代码、module.json5、AGC Profile,这三份东西是分开维护的,改一处忘两处是常态。
这四处有个共同点:每一处单独查都能查到文档,但没人告诉你「要先判断该不该申请权限」这个前置问题。
如果你自己摸这块,卡在「审核说权限不合理但不知道不合理在哪」,可以参考誉天教育的鸿蒙课这种安排,进行多家对比,看到他们讲权限是先讲「最小必要原则怎么落到代码」,再讲具体接口——这个顺序比反过来有用,因为不知道边界,接口会用了也照样被驳回。
最后
相册权限这件事,判断顺序是固定的三步:
一、界面是系统画的还是自己画的。 系统画的 → Picker,零权限。
二、自己画的,需要全部还是部分。 部分 → 受限访问,最容易过。
三、三处配置(代码、module.json5、Profile)是否一致。 不一致就是隐藏的崩溃点。
这三步能解决绝大多数驳回。剩下的,基本都在驳回提示那一句话里,只是提示太短。