HarmonyOS 图片选择别自己做图库:PhotoViewPicker 最小实践【鸿蒙心迹】

👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 HarmonyOS 的过程都记录在这里。
🛠️ 主要方向:ArkTS 语言基础、HarmonyOS 原生应用(Stage 模型、UIAbility/ServiceAbility)、分布式能力与软总线、元服务/卡片、应用签名与上架、性能与内存优化、项目实战,以及 Android → 鸿蒙的迁移踩坑与复盘。
🧭 内容节奏:从基础到实战——小示例拆解框架认知、专项优化手记、实战项目拆包、面试题思考与复盘,让每篇都有可落地的代码与方法论。
💡 我相信:写作是把知识内化的过程,分享是让生态更繁荣的方式。
如果你也想拥抱鸿蒙、热爱成长,欢迎关注我,一起交流进步!🚀
前言
头像设置、发帖上传、OCR、识图、人脸比对这类功能,经常都有同一个入口:让用户从本地相册里选一张图片。
这个需求看起来很简单,但如果从“读取整个图库”开始做,很快就会多出相册列表、图片网格、分页加载、权限申请、资源访问、选择状态等一整套逻辑。对于只是“让用户明确选择一张或几张图片”的普通应用,其实没必要先造一个图库页面。
Media Library Kit 已经向普通应用提供 Picker 方式选择媒体资源的能力。华为官方在文字识别、人脸比对等示例中,也直接使用 photoAccessHelper.PhotoViewPicker 拉起系统媒体选择界面,拿到图片 URI 后再通过 Core File Kit 和 Image Kit 转换成 PixelMap。
这篇文章就收敛到一个最小场景:
点击“选择图片” → 拉起系统图库 → 用户选择一张图片 → 获得 URI → 读取图片 → 解码为 PixelMap → 在 ArkUI 页面显示。
代码不包含 OCR、上传服务器等业务逻辑,只把“选图”这条基础链路跑通。
一、为什么只是选一张图,不建议自己做整套图库
自己实现图库页面,并不只是写一个 Grid。
一旦应用直接围绕媒体库做完整浏览能力,就要开始考虑资源查询、相册分类、权限范围、分页、图片缩略图、媒体变化等问题。而 Picker 的思路不同:由系统负责让用户查看并决定分享哪些媒体资源,应用只处理用户最终选中的内容。
Media Library Kit 官方资料明确区分了开放范围:使用 Picker 选择或保存媒体资源属于面向所有应用开放的能力,而部分直接管理媒体库的能力受到更严格的权限限制。
这意味着,如果业务实际只需要:
- 修改头像;
- 发帖添加图片;
- 给 OCR 选择一张待识别图片;
- 选择图片做人脸比对;
- 选择若干图片导入应用;
那么优先研究 Picker,通常比从“申请图库权限 + 查询整个媒体库”开始更贴合业务边界。
华为官方 2026 年的 Core Vision Kit 文字识别示例就是这个思路:使用 PhotoViewPicker 获取图片 URI,再使用 fileIo.open() 和 image.createImageSource() 解码。
二、先把版本、Kit 和权限关系弄清楚
本文按照当前 HarmonyOS 最新公开文档整理。
截至 2026 年 9 月,HarmonyOS 7 对应 API 26.0.0。华为官方升级适配文档明确说明,HarmonyOS 7.0 对应的开发套件 API 版本为 26.0.0。
本文涉及三个 Kit:
| 作用 | Kit | 主要接口 |
|---|---|---|
| 系统媒体选择 | Media Library Kit | PhotoViewPicker、PhotoSelectOptions |
| 打开用户选中的 URI | Core File Kit | fileIo.open() |
| 图片解码 | Image Kit | image.createImageSource()、createPixelMap() |
对应导入如下:
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
其中 Media Library Kit 的 photoAccessHelper 模块首批接口从 API version 10 开始提供,最新文档继续包含 PhotoViewPicker。
这里最容易产生的误解:是不是一定要申请 READ_IMAGEVIDEO?
如果只是通过 Picker 让用户主动选择媒体文件,不能把它等同于应用直接读取整个媒体库。
Media Library Kit 官方介绍把“使用 Picker 选择媒体资源”列为普通应用开放能力;而直接访问部分受限制媒体库能力时,才涉及 ohos.permission.READ_IMAGEVIDEO、ohos.permission.WRITE_IMAGEVIDEO 等权限。
所以本文这个最小示例不为了 Picker 额外在 module.json5 中声明 READ_IMAGEVIDEO。
这也是为什么系统 Picker 很适合头像、上传附件、识图等“用户主动选文件”的场景:应用拿到的是用户明确选择的资源,而不是默认获得浏览整个图库的能力。
三、最小实践要做什么
页面只保留三个元素:
- 一个“选择图片”按钮;
- 一块图片预览区域;
- 一行当前状态文字。
点击按钮后:
ArkUI Button
↓
PhotoViewPicker.select()
↓
PhotoSelectResult.photoUris
↓
fileIo.open(uri)
↓
image.createImageSource(fd)
↓
createPixelMap()
↓
ArkUI Image(PixelMap)
官方 OCR 示例和人脸比对示例都采用了基本相同的数据路径。OCR 示例中直接从 photoSelectResult.photoUris[0] 取得 URI,随后调用 fileIo.open()、image.createImageSource() 和 createPixelMap()。
真正值得掌握的不是按钮怎么写,而是 URI 后面的资源读取和释放边界。
四、创建 PhotoViewPicker 并配置选图参数
单图选择需要两个核心参数:
let options = new photoAccessHelper.PhotoSelectOptions();
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
MIMEType 用来限定当前选择图片类型,maxSelectNumber 控制最大选择数量。
华为官方文字识别示例使用的就是:
let photoSelectOptions =
new photoAccessHelper.PhotoSelectOptions();
photoSelectOptions.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
photoSelectOptions.maxSelectNumber = 1;
随后创建:
let photoPicker: photoAccessHelper.PhotoViewPicker =
new photoAccessHelper.PhotoViewPicker();
再调用:
let result = await photoPicker.select(photoSelectOptions);
该调用返回的选择结果中,通过 photoUris 获取用户选中的资源 URI。官方文字识别和人脸比对 API 示例均采用这一写法。
如果是头像、OCR 入口这类功能,把 maxSelectNumber 设为 1 就足够了。
五、拿到 URI 后,不要把它当普通文件路径
例如:
const result = await photoPicker.select(options);
if (result.photoUris.length === 0) {
return;
}
const uri: string = result.photoUris[0];
这里拿到的是资源 URI。
对于后续图片处理,更合适的做法是让 Core File Kit 去打开该 URI:
const file = await fileIo.open(
uri,
fileIo.OpenMode.READ_ONLY
);
官方当前 OCR 示例也是直接:
let fileSource =
await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
随后通过文件描述符创建 ImageSource。
这比尝试把 URI 强行解析成某个本地绝对路径更符合系统提供的资源访问模型。
六、从文件描述符转换成 PixelMap
拿到 file.fd 后,Image Kit 可以创建图片数据源:
const source: image.ImageSource =
image.createImageSource(file.fd);
然后:
const pixelMap: image.PixelMap =
await source.createPixelMap();
于是整个核心链路只有三步:
const file =
await fileIo.open(uri, fileIo.OpenMode.READ_ONLY);
const source =
image.createImageSource(file.fd);
const pixelMap =
await source.createPixelMap();
华为官方文字识别示例给出的实现与此一致:先打开 URI,再把文件描述符交给 createImageSource(),最终得到 PixelMap。
PixelMap 不只是为了显示图片。后续如果业务要接:
- OCR;
- 目标识别;
- 人脸比对;
- 图像分割;
- 其他接受 PixelMap 的视觉能力;
都可以继续沿用这条输入链路。
这也是 PhotoViewPicker 很实用的一点:Picker 只负责选资源,后续图像处理能力可以继续组合。
七、完整的 ArkUI 最小示例
下面代码按照华为官方当前 PhotoViewPicker → URI → fileIo → ImageSource → PixelMap 接口组织成一个尽量精简的页面。
它解决的只有一件事:用户选择一张图片,然后显示在当前页面。
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
import { BusinessError } from '@kit.BasicServicesKit';
@Entry
@Component
struct Index {
@State private selectedImage: image.PixelMap | undefined = undefined;
@State private statusText: string = '尚未选择图片';
private async selectPhoto(): Promise<void> {
let file: fileIo.File | undefined = undefined;
let source: image.ImageSource | undefined = undefined;
try {
const options =
new photoAccessHelper.PhotoSelectOptions();
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
const picker: photoAccessHelper.PhotoViewPicker =
new photoAccessHelper.PhotoViewPicker();
const result: photoAccessHelper.PhotoSelectResult =
await picker.select(options);
if (result.photoUris.length === 0) {
this.statusText = '未选择图片';
return;
}
const uri: string = result.photoUris[0];
file = await fileIo.open(
uri,
fileIo.OpenMode.READ_ONLY
);
source = image.createImageSource(file.fd);
const pixelMap: image.PixelMap =
await source.createPixelMap();
if (this.selectedImage) {
await this.selectedImage.release();
}
this.selectedImage = pixelMap;
this.statusText = '已选择图片';
} catch (error) {
const err = error as BusinessError;
console.error(
`Select photo failed. code=${err.code}, message=${err.message}`
);
this.statusText = '未完成图片选择';
} finally {
if (source) {
source.release();
}
if (file) {
await fileIo.close(file);
}
}
}
aboutToDisappear(): void {
if (this.selectedImage) {
void this.selectedImage.release();
this.selectedImage = undefined;
}
}
build() {
Column({ space: 16 }) {
Text('PhotoViewPicker 最小实践')
.fontSize(24)
.fontWeight(FontWeight.Bold)
if (this.selectedImage) {
Image(this.selectedImage)
.width('100%')
.height(320)
.objectFit(ImageFit.Contain)
} else {
Text('请选择一张图片')
.width('100%')
.height(320)
.textAlign(TextAlign.Center)
}
Text(this.statusText)
.fontSize(14)
Button('选择图片')
.width('100%')
.onClick(() => {
void this.selectPhoto();
})
}
.width('100%')
.padding(24)
}
}
这段代码没有加入业务上传、缓存复制或者视觉识别逻辑,方便把注意力集中在 Picker 本身。
八、真正需要关注的四个地方
1. PhotoViewPicker 返回的是 URI,不是图片本身
Picker 完成后,关键结果是:
result.photoUris
不要把它理解成已经得到 PixelMap。
如果下一步只是把 URI 交给支持 URI 的组件,可能不需要主动解码;但 OCR、图像算法等业务通常需要 PixelMap,此时才继续使用 fileIo 和 Image Kit。
2. 图片类型和选择数量应该在 Picker 阶段约束
单张图片:
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 1;
不要拉起 Picker 后让用户随便选择,再在业务层大量过滤。
系统选择阶段能表达的约束,应尽量在 PhotoSelectOptions 中完成。
3. 文件描述符和 ImageSource 不是可以一直不管的对象
官方 OCR 示例在处理结束后会释放 PixelMap、ImageSource,并关闭通过 fileIo 打开的文件。
因此示例中使用:
finally {
if (source) {
source.release();
}
if (file) {
await fileIo.close(file);
}
}
页面长期反复选图时,还要处理上一个 PixelMap:
if (this.selectedImage) {
await this.selectedImage.release();
}
否则代码功能可能看起来没问题,但资源生命周期没有收好。
4. Picker 和“读取整个图库”不是一个权限模型
这是本文最希望强调的一点。
如果业务只是:
“请用户选择一张图片交给应用处理。”
那么 Picker 本身就是一个更自然的入口。
如果业务需求变成:
“应用自己扫描用户全部照片、查询媒体数据库、长期管理相册资源。”
这已经是另一类能力,不能再把本文 Picker 示例直接外推过去。
部分 PhotoAsset 等直接媒体访问接口明确要求 ohos.permission.READ_IMAGEVIDEO。例如官方 PhotoAsset.getThumbnail() 文档就标明了该权限要求。
两类场景不要混在一起。
九、用户取消选择怎么处理
用户进入系统选择界面之后,业务代码必须允许“没有得到图片”这件事发生。
本文建议至少守住两个边界:
try {
const result = await picker.select(options);
if (result.photoUris.length === 0) {
return;
}
// 正常处理
} catch (error) {
// 没有进入图片处理流程
}
这里不建议业务代码为了“取消”写一个本文没有从当前官方资料确认到的固定错误码。
更稳妥的最小实践是:
只有真正拿到非空 photoUris 后才进入文件打开和图片解码流程;没有 URI,就结束本次选择。
同时,异常统一进入 catch,不要在失败情况下继续访问:
result.photoUris[0]
这类边界处理看起来简单,却比在 UI 上区分十几种提示更重要。
十、多选其实只需要改很少的代码
如果发帖业务允许一次选择 9 张图,Picker 部分最直接的变化是:
options.maxSelectNumber = 9;
然后不再取:
result.photoUris[0]
而是处理:
const uris: Array<string> = result.photoUris;
华为官方人脸比对示例就是多选场景:把 maxSelectNumber 设置为 2,然后分别读取两个 URI 并转换为两个 PixelMap。
例如:
const options =
new photoAccessHelper.PhotoSelectOptions();
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
options.maxSelectNumber = 2;
const picker =
new photoAccessHelper.PhotoViewPicker();
const result =
await picker.select(options);
const uris: Array<string> =
result.photoUris;
需要注意,真正到了大量图片场景,后面的解码策略就不应简单写成:
for (...) {
await createPixelMap();
}
然后把所有原始 PixelMap 长期保存在页面里。
如果业务一次导入大量图片,应进一步考虑缩略图、任务调度和资源生命周期。华为官方也提供了基于 PhotoViewPicker 与 taskpool 的批量图片加载实践,用来处理批量导入和缩略图生成场景。
所以:
单图最小实践可以直接解码;大量图片则应该继续研究图片尺寸、内存与任务调度。
十一、这个示例为什么没有 module.json5 权限配置
有些 Picker 示例一上来就写:
"requestPermissions": [
{
"name": "ohos.permission.READ_IMAGEVIDEO"
}
]
这很容易给读者形成一个印象:
“调用 PhotoViewPicker 必须先申请图库读取权限。”
本文没有这样写。
原因并不是漏掉配置,而是当前场景只是让用户通过系统 Picker 主动选择资源。Media Library Kit 官方把“Picker 选择媒体资源”作为向普通应用开放的能力;需要 READ_IMAGEVIDEO 的则是另一类直接访问媒体资源的接口。
如果你的项目除了 Picker 之外还调用了需要该权限的接口,应当根据那个接口自己的官方文档单独配置,而不是把权限需求归到 PhotoViewPicker 身上。
十二、实际项目中怎么排查
如果点击选择图片后结果不符合预期,可以按下面顺序检查。
先看 SDK / API 版本。
HarmonyOS 7 当前对应 API 26.0.0。如果项目刚升级开发套件,建议结合官方升级适配说明检查 compileSdkVersion、targetSdkVersion 与兼容版本设置。
再看导入模块。
本文使用的是:
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
import { image } from '@kit.ImageKit';
不要把不同历史版本示例中的旧导入方式混在一份代码里。
然后检查 PhotoSelectOptions。
单图场景至少确认:
MIMEType = IMAGE_TYPE
maxSelectNumber = 1
再检查结果数组。
不要默认:
photoUris[0]
永远存在。
先判断:
photoUris.length
接着看 URI 是否正常交给 fileIo.open()。
不要自行把媒体 URI 拼成应用沙箱路径。
最后检查资源释放。
File、ImageSource、长期持有的 PixelMap 都应该有明确生命周期。
十三、从选图到识图,其实只差下一步业务接口
把这个最小示例单独抽出来还有一个好处:它可以成为很多图像功能的公共输入层。
例如官方文字识别示例:
PhotoViewPicker
↓
URI
↓
PixelMap
↓
TextRecognition
官方人脸比对则是:
PhotoViewPicker
↓
两个 URI
↓
两个 PixelMap
↓
FaceComparator
华为官方现在甚至在 OCR、目标识别等开发示例里直接重复使用这套链路。
因此实际工程中可以把:
select image -> uri -> PixelMap
封装成独立的图片输入能力。
业务层只关心拿到的 PixelMap 要做什么,而不用每个识图页面都重新实现一套图库浏览功能。
开发经验总结
这个能力本身并不复杂,真正容易理解错的是“系统 Picker”和“应用读取整个媒体库”的边界。
做头像、发帖选图、OCR、识图这类功能时,可以先记住四点:
- 用户主动选择媒体资源时,优先研究
PhotoViewPicker,不必一开始就自己实现图库页面。 PhotoViewPicker.select()得到的核心结果是 URI,图像算法需要 PixelMap 时,再通过 Core File Kit 和 Image Kit 解码。- Picker 选择媒体和直接读取整个媒体库不是同一套权限场景,不要机械给 Picker 添加
READ_IMAGEVIDEO。 - 真正拿到非空
photoUris后再进入业务处理,同时及时释放 File、ImageSource 和 PixelMap。
对于 HarmonyOS 7 项目,还应该明确当前版本基线:HarmonyOS 7 对应 API 26.0.0;如果代码来自旧版本示例,最好重新回到当前官方 API 文档确认导入路径和接口状态,而不是直接复制历史代码。
如果你的页面里现在还有一套只为了“让用户选一张照片”而实现的自定义图库,不妨重新审视一下:业务真正需要的是图库管理能力,还是只需要一次由用户主动发起的媒体选择?
📝 写在最后
如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!
我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!
感谢你的阅读,我们下篇文章再见~👋
✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。
更多推荐


所有评论(0)