【HarmonyOS NEXT】鸿蒙中如何获取用户相册图片?PhotoViewPicker 超详细实战(2026最新版)——图片选择、权限机制、多选、拍照、URI读取、最佳实践一网打尽
系列文章:HarmonyOS NEXT 实战教程
适用版本:HarmonyOS 5.0+ / API 12+
开发工具:DevEco Studio 5.x
开发语言:ArkTS
文章特点:从零开始 + 实战案例 + 原理分析 + 企业级最佳实践
前言
在移动应用开发过程中,选择图片几乎是每一个应用都会涉及的功能,例如:
- 微信修改头像
- QQ发送图片
- 小红书发布笔记
- 抖音上传视频
- 美团上传评价图片
- 淘宝晒单
- 企业OA上传附件
对于 Android 开发者来说,大家已经非常熟悉:
Intent.ACTION_PICK
或者
ActivityResultLauncher
再或者
ACTION_OPEN_DOCUMENT
那么来到 HarmonyOS NEXT 后,很多开发者都会有一个疑问:
鸿蒙如何获取用户相册图片?
很多人第一反应就是:
我要申请相册权限。
其实这是一个误区。
HarmonyOS NEXT 为了更好的保护用户隐私,引入了更加安全的图片访问机制。
官方推荐开发者使用:
photoAccessHelper.PhotoViewPicker
而不是直接访问整个图库。
今天,我们就彻底搞懂它。
一、为什么使用 PhotoViewPicker?
Android 与 HarmonyOS 的区别
Android 以前的开发流程通常是:
申请读取权限
↓
读取整个相册
↓
自己写图片列表
↓
用户选择
↓
返回图片
这种方式存在很多问题。
例如:
- 应用可以读取整个图库
- 用户不知道哪些图片被读取
- 权限过大
- 容易泄露隐私
因此,现在 Android 也逐渐开始推广系统 Photo Picker。
HarmonyOS 更进一步。
官方直接推荐:
绝大多数应用不要申请相册权限。
而应该使用:
PhotoViewPicker
它由系统图库负责展示图片。
应用只负责:
打开选择器
↓
用户自己选择
↓
返回图片URI
整个过程中:
应用看不到其它图片。
只能拿到:
用户选择的图片。
安全性更高。
二、什么是 PhotoViewPicker?
官方定义:
PhotoViewPicker 是 HarmonyOS 提供的系统图片选择器,用于让用户主动选择图片或视频资源。
它最大的特点:
✅ 不需要自己开发图库
✅ 不需要读取全部图片
✅ 不需要申请图库读取权限(大多数场景)
✅ 用户体验统一
✅ 官方推荐
可以理解成:
App
│
│ 调用
▼
PhotoViewPicker
│
▼
系统图库
│
▼
用户选择
│
▼
返回URI
│
▼
App显示图片
整个流程全部由系统负责。
三、PhotoViewPicker 的优势
相比自己开发图库,它具有很多优势。
1、不需要读取全部相册
传统方案:
10000张图片
↓
全部读取
↓
全部显示
PhotoViewPicker:
10000张图片
↓
系统展示
↓
用户选1张
↓
App仅获得1张URI
是不是简单很多?
2、不需要申请权限
很多开发者最喜欢这一点。
例如:
微信修改头像。
用户点击:
修改头像
然后:
PhotoViewPicker
↓
用户选择
↓
返回URI
整个过程:
无需申请:
READ_IMAGEVIDEO
权限。
3、安全性更高
因为:
App 根本看不到其它照片。
例如:
用户相册:
10000张图片
应用最终只能获得:
IMG_001.jpg
这一张。
其它图片:
完全无法访问。
4、系统体验统一
所有鸿蒙应用:
打开图库都是一样的。
用户学习成本低。
体验一致。
四、PhotoViewPicker 能做什么?
支持:
✅ 选择图片
✅ 选择视频
✅ 多图选择
✅ 多视频选择
✅ 图片+视频混选
✅ 调用相机拍照
✅ 返回URI
例如:
PhotoViewPicker
↓
图片
视频
拍照
↓
返回:
photoUris
五、开发前准备
首先导入媒体库。
import { photoAccessHelper } from '@kit.MediaLibraryKit'
如果需要日志:
import { hilog } from '@kit.PerformanceAnalysisKit'
如果需要显示图片:
import { image } from '@kit.ImageKit'
通常:
PhotoViewPicker 就来自:
MediaLibraryKit
六、PhotoViewPicker 的核心对象
整个图片选择只涉及几个对象。
photoAccessHelper
│
│
▼
PhotoViewPicker
│
▼
PhotoSelectOptions
│
▼
PhotoSelectResult
它们分别负责:
| 类 | 作用 |
|---|---|
| PhotoViewPicker | 打开系统图库 |
| PhotoSelectOptions | 设置选择参数 |
| PhotoSelectResult | 返回选择结果 |
| photoUris | 用户选择的图片URI |
记住这四个对象即可完成绝大多数图片选择功能。
七、创建 PhotoViewPicker
首先创建选择器对象。
import { photoAccessHelper } from '@kit.MediaLibraryKit'
let picker = new photoAccessHelper.PhotoViewPicker()
代码非常简单。
实际上:
PhotoViewPicker 内部只是一个系统能力入口。
真正的图库:
由系统负责展示。
因此:
不要频繁创建。
建议:
页面创建一次
↓
重复使用
例如:
@State
picker: photoAccessHelper.PhotoViewPicker =
new photoAccessHelper.PhotoViewPicker()
这样效率更高。
八、PhotoSelectOptions 是什么?
PhotoSelectOptions:
用于配置:
选择什么
选择多少
是否拍照
是否允许视频
例如:
let options = new photoAccessHelper.PhotoSelectOptions()
这是整个 Photo Picker 最重要的配置对象。
后续章节将详细讲解:
- MIMEType
- maxSelectNumber
- isPhotoTakingSupported
- filter
- 默认选中
- 多媒体过滤
- 图片与视频混合选择
- 企业项目中的高级配置
九、第一个 Photo Picker 示例
最简单的代码如下:
import { photoAccessHelper } from '@kit.MediaLibraryKit'
async function selectPhoto() {
try {
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
const result = await picker.select(options)
console.info(JSON.stringify(result.photoUris))
} catch (err) {
console.error(`Photo Picker Error: ${JSON.stringify(err)}`)
}
}
运行流程:
点击按钮
↓
打开系统图库
↓
用户选择图片
↓
关闭图库
↓
返回URI
↓
打印photoUris
整个流程不到二十行代码即可完成。
十、本章小结
本章我们完成了 PhotoViewPicker 的基础认识,主要学习了以下内容:
- 为什么 HarmonyOS 推荐使用 PhotoViewPicker
- PhotoViewPicker 与传统读取相册方式的区别
- PhotoViewPicker 的核心优势
- 图片选择整体工作流程
- 如何导入 MediaLibraryKit
- 如何创建 PhotoViewPicker 对象
- PhotoSelectOptions 的基本作用
- 第一个图片选择示例
到这里,你已经能够完成一个最基础的图片选择功能。
十一、PhotoSelectOptions 是什么?
如果把 PhotoViewPicker 比作一辆汽车,那么:
PhotoViewPicker
│
▼
PhotoSelectOptions
就是这辆汽车的方向盘。
所有选择行为几乎都由它控制。
例如:
- 选择图片还是视频?
- 最多可以选几张?
- 是否允许拍照?
- 是否允许录像?
- 是否允许混选?
- 返回哪些资源?
都是通过它完成。
创建方式十分简单:
import { photoAccessHelper } from '@kit.MediaLibraryKit'
let options = new photoAccessHelper.PhotoSelectOptions()
它只是一个配置对象。
真正打开图库仍然是:
await picker.select(options)
因此:
PhotoSelectOptions
↓
配置参数
↓
传递给Picker
↓
系统图库按照配置展示
十二、Photo Picker 的完整调用流程
企业项目中,一般都会遵循下面流程。
创建 PhotoViewPicker
↓
创建 PhotoSelectOptions
↓
配置各种参数
↓
调用 select()
↓
系统打开图库
↓
用户选择
↓
返回 PhotoSelectResult
对应代码:
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
const result = await picker.select(options)
是不是非常简单?
真正复杂的是:
options 如何配置。
下面开始逐个介绍。
十三、MIMEType 详解
这是 Photo Picker 最重要的参数。
决定:
系统图库显示什么资源。
例如:
只显示图片
或者
只显示视频
或者
图片+视频一起显示
什么是 MIME?
很多开发者第一次看到:
MIMEType
感觉很陌生。
其实:
MIME 就是:
媒体类型(Media Type)
例如:
图片:
image/jpeg
image/png
image/webp
视频:
video/mp4
video/avi
但是:
HarmonyOS 已经帮我们封装好了。
无需自己填写:
image/jpeg
直接使用官方枚举即可。
十四、只允许选择图片
例如:
用户修改头像。
当然不能选择视频。
此时:
const options = new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
完整代码:
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
const result = await picker.select(options)
打开系统图库以后:
只能看到:
✔ JPG
✔ PNG
✔ GIF
✔ WEBP
......
×
MP4
×
MOV
×
AVI
视频会自动隐藏。
十五、IMAGE_TYPE 工作原理
很多人认为:
Picker 会扫描全部文件。
其实不是。
真正流程:
图库数据库
↓
系统过滤
↓
IMAGE_TYPE
↓
只显示图片
↓
用户选择
因此:
效率非常高。
App 完全不用参与过滤。
十六、只选择视频
如果开发的是:
例如:
短视频App。
可以:
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.VIDEO_TYPE
系统打开以后:
看到的只有:
MP4
MOV
AVI
MKV
RMVB
所有图片:
全部自动隐藏。
例如:
上传视频:
点击上传
↓
打开Picker
↓
只有视频
↓
用户选择
↓
返回URI
体验非常好。
十七、图片 + 视频混选
很多社交软件都会支持:
例如:
朋友圈。
小红书。
微博。
都可以:
图片
视频
一起选择。
此时:
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_VIDEO_TYPE
打开以后:
例如:
IMG001.jpg
IMG002.png
Video001.mp4
Video002.mov
全部显示。
用户可以自由选择。
十八、三种 MIMEType 对比
| 类型 | 图片 | 视频 | 推荐场景 |
|---|---|---|---|
| IMAGE_TYPE | √ | × | 修改头像、上传图片、证件照 |
| VIDEO_TYPE | × | √ | 上传视频、短视频 |
| IMAGE_VIDEO_TYPE | √ | √ | 社交App、聊天App |
企业开发中:
IMAGE_TYPE 使用频率最高。
十九、maxSelectNumber 详解
第二重要参数:
maxSelectNumber
意思:
最多允许选择多少张。
例如:
options.maxSelectNumber = 1
表示:
只能选一张。
适用于:
修改头像
上传身份证
上传营业执照
上传二维码
多图上传
例如:
朋友圈。
options.maxSelectNumber = 9
用户可以:
□ 图片1
□ 图片2
□ 图片3
□ 图片4
......
最多:
9张。
例如:
电商评价:
options.maxSelectNumber = 5
效果:
晒单图片
最多上传:
5张
二十、为什么要限制数量?
有人会问:
为什么不能:
9999
原因有很多。
例如:
图片:
1张
5MB
如果:
1000张
就是:
5000MB
内存:
瞬间爆炸。
服务器:
上传压力巨大。
用户:
等待几十分钟。
因此:
企业都会限制。
例如:
| App | 数量 |
|---|---|
| 微信朋友圈 | 9 |
| QQ空间 | 9 |
| 小红书 | 18~20 |
| 淘宝评价 | 5 |
| 企业OA | 3 |
二十一、企业项目推荐配置
头像:
options.maxSelectNumber = 1
朋友圈:
options.maxSelectNumber = 9
发帖:
options.maxSelectNumber = 18
上传合同:
options.maxSelectNumber = 20
企业一般不会无限制。
二十二、完整示例:选择 9 张图片
import { photoAccessHelper } from '@kit.MediaLibraryKit'
async function selectImages() {
try {
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
// 只允许图片
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
// 最多9张
options.maxSelectNumber = 9
const result = await picker.select(options)
console.info(`图片数量:${result.photoUris.length}`)
result.photoUris.forEach((uri) => {
console.info(uri)
})
} catch (err) {
console.error(JSON.stringify(err))
}
}
执行流程:
点击上传
↓
打开系统图库
↓
最多选择9张
↓
点击完成
↓
返回9个URI
↓
上传服务器
二十三、PhotoSelectResult 返回的数据
当用户点击完成以后:
返回的是:
const result =
await picker.select(options)
里面最重要的是:
result.photoUris
类型:
Array<string>
例如:
[
"file://media/Photo/10000001",
"file://media/Photo/10000002",
"file://media/Photo/10000003"
]
注意:
这里返回的不是:
Bitmap
PixelMap
Image
而是:
URI(统一资源标识符)。
后续需要根据 URI 去读取图片或交给支持 URI 的组件使用。
本章小结
这一章重点介绍了 PhotoSelectOptions 中最常用的两个配置项:
MIMEType:控制显示图片、视频或混合资源。maxSelectNumber:限制用户可选择的资源数量。
掌握这两个参数后,已经可以覆盖大多数图片选择场景。
二十四、开启拍照功能(isPhotoTakingSupported)
很多 App 在选择图片时,第一项都会显示一个 "拍照" 按钮,例如:
┌─────────────────────────────┐
│ 📷 拍照 │
├─────────────────────────────┤
│ 🖼 IMG_001.jpg │
│ 🖼 IMG_002.jpg │
│ 🖼 IMG_003.jpg │
│ 🖼 IMG_004.jpg │
└─────────────────────────────┘
HarmonyOS 的 PhotoViewPicker 已经内置了这一能力。
只需要开启一个配置即可。
options.isPhotoTakingSupported = true
是不是非常简单?
二十五、isPhotoTakingSupported 原理
很多开发者以为:
Photo Picker
↓
自己调用Camera
↓
拍照
其实并不是。
真正流程如下:
App
↓
PhotoViewPicker
↓
系统图库
↓
点击拍照
↓
系统相机
↓
拍照完成
↓
系统保存图片
↓
返回URI
↓
App获得URI
整个流程全部由 HarmonyOS 系统完成。
应用:
无需调用 Camera Kit。
也无需申请:
CAMERA
权限(具体是否需要权限以当前 HarmonyOS 版本和业务场景为准,如果直接使用系统 Picker 的拍照入口,通常由系统完成拍照流程)。
二十六、完整示例:打开拍照入口
import { photoAccessHelper } from '@kit.MediaLibraryKit'
async function selectPhoto() {
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber = 1
// 开启拍照
options.isPhotoTakingSupported = true
const result = await picker.select(options)
console.info(JSON.stringify(result.photoUris))
}
效果如下:
点击上传头像
↓
系统图库
↓
第一页
📷 拍照
🖼 图片
🖼 图片
🖼 图片
二十七、什么时候适合开启拍照?
企业开发中,并不是所有场景都应该开启。
例如:
修改头像
推荐:
修改头像
↓
拍照
或者
选择相册
用户体验最好。
身份证上传
例如:
上传身份证
↓
拍照
↓
OCR识别
推荐开启。
发朋友圈
一般开启。
用户可以:
刚拍照片
↓
立即发送
浏览历史图片
例如:
浏览企业资料
一般无需开启拍照。
所以:
是否开启拍照,应根据业务场景决定。
二十八、为什么返回的是 URI?
很多 Android 开发者第一次接触 Photo Picker 都会问:
为什么:
不是 Bitmap?
为什么:
不是 PixelMap?
为什么:
不是 Image?
实际上:
HarmonyOS 返回的是:
result.photoUris
例如:
file://media/Photo/100000123
这是:
图片资源 URI。
二十九、URI 到底是什么?
URI:
全称:
Uniform Resource Identifier
中文:
统一资源标识符。
它可以理解成:
图片身份证
而不是:
图片本身。
例如:
现实生活:
身份证号
↓
找到某个人
HarmonyOS:
URI
↓
找到图片
所以:
URI 本身:
不是图片。
只是:
图片的位置。
三十、为什么不用 PixelMap?
原因很多。
第一:节省内存
假设:
每张图片:
4000 × 3000
解码以后:
大约:
35MB
如果:
用户选择:
20张
内存:
35 × 20
=
700MB
很多手机:
直接:
OOM。
所以:
HarmonyOS:
返回:
URI
需要时:
再加载。
第二:懒加载
真正流程:
返回URI
↓
列表展示
↓
滑动到当前位置
↓
读取图片
↓
解码
↓
显示
效率非常高。
第三:保护隐私
系统:
不会主动暴露:
图片内容。
App:
只有真正需要时:
才读取。
三十一、PhotoSelectResult 详解
返回对象:
const result = await picker.select(options)
里面:
最重要:
就是:
result.photoUris
例如:
[
file://media/Photo/001
file://media/Photo/002
file://media/Photo/003
]
类型:
Array<string>
因此:
遍历即可。
result.photoUris.forEach((uri) => {
console.info(uri)
})
三十二、用户取消选择怎么办?
这是很多新人容易忽略的问题。
例如:
打开图库:
用户点击:
取消
怎么办?
不要认为:
一定:
报异常。
很多情况下:
返回:
photoUris.length == 0
所以:
推荐:
const result = await picker.select(options)
if (result.photoUris.length === 0) {
console.info("用户取消选择")
return
}
这样:
体验最好。
三十三、try-catch 必不可少
企业开发:
永远不要:
直接:
await picker.select(options)
一定:
try {
} catch {
}
例如:
try {
const result = await picker.select(options)
} catch (err) {
console.error(JSON.stringify(err))
}
原因:
可能出现:
- 系统异常
- Picker 打开失败
- Ability 已销毁
- 用户环境异常
- API 调用错误
统一捕获。
三十四、封装企业工具类
企业开发:
一般不会:
每个页面:
都写:
new PhotoViewPicker()
通常都会封装:
例如:
export class PhotoPickerUtil {
static async selectImage(max: number = 1): Promise<Array<string>> {
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber = max
options.isPhotoTakingSupported = true
const result = await picker.select(options)
return result.photoUris
}
}
以后:
任何页面:
只需要:
const uris = await PhotoPickerUtil.selectImage()
或者:
const uris = await PhotoPickerUtil.selectImage(9)
即可。
代码更加整洁。
三十五、不同业务场景推荐配置
| 业务场景 | MIMEType | maxSelectNumber | 拍照 |
|---|---|---|---|
| 修改头像 | IMAGE_TYPE | 1 | √ |
| 身份证上传 | IMAGE_TYPE | 1 | √ |
| 企业报销附件 | IMAGE_TYPE | 10 | √ |
| 微信朋友圈 | IMAGE_VIDEO_TYPE | 9 | √ |
| 视频上传 | VIDEO_TYPE | 1 | × |
| 企业相册 | IMAGE_TYPE | 20 | × |
这也是大多数企业项目采用的配置方式。
三十六、本章总结
这一章我们掌握了企业开发中最常见的高级配置,包括:
✅ isPhotoTakingSupported 开启拍照入口
✅ Photo Picker 与系统相机的协作流程
✅ 为什么返回的是 URI 而不是 PixelMap
✅ URI 的设计思想和优势
✅ 用户取消选择的处理方式
✅ try-catch 异常捕获
✅ 企业级 PhotoPickerUtil 工具类封装
到这里,你已经具备了在实际项目中使用 PhotoViewPicker 完成大部分图片选择需求的能力。
三十七、Photo Picker 返回的数据到底是什么?
上一章我们已经知道:
const result = await picker.select(options)
真正返回的是:
result.photoUris
例如:
[
"file://media/Photo/100001",
"file://media/Photo/100002"
]
很多新人看到以后第一反应:
这不是图片啊?
没错。
它只是:
图片的位置
或者说:
图片资源引用
真正图片:
仍然保存在:
系统图库
里面。
三十八、URI 与图片的关系
可以用快递举例。
假设:
用户拍了一张照片。
真正图片:
📷 IMG_001.jpg
保存在:
系统图库
而 Photo Picker 返回:
file://media/Photo/100001
其实就是:
快递单号
真正流程:
URI
↓
找到图片
↓
读取图片
↓
解码
↓
显示
所以:
URI:
不是图片。
只是:
图片的位置。
三十九、Image 能不能直接显示 URI?
答案:
可以,但要分情况。
例如:
ArkUI 的 Image 组件支持加载:
Resource
PixelMap
URI
网络图片
Base64
因此:
很多情况下:
直接:
Image(uri)
即可。
例如:
@State
photoUri: string = ""
build() {
Column() {
Image(this.photoUri)
.width(120)
.height(120)
.borderRadius(60)
}
}
是不是非常简单?
四十、完整头像案例
例如:
点击头像:
头像
↓
Photo Picker
↓
选择图片
↓
立即显示
代码:
import { photoAccessHelper } from '@kit.MediaLibraryKit'
@Entry
@Component
struct Demo {
@State
avatar: string = ""
async chooseAvatar() {
const picker =
new photoAccessHelper.PhotoViewPicker()
const option =
new photoAccessHelper.PhotoSelectOptions()
option.maxSelectNumber = 1
option.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
const result = await picker.select(option)
if (result.photoUris.length > 0) {
this.avatar = result.photoUris[0]
}
}
build() {
Column() {
Image(this.avatar)
.width(100)
.height(100)
.borderRadius(50)
Button("选择头像")
.margin({ top: 20 })
.onClick(() => {
this.chooseAvatar()
})
}
.width("100%")
}
}
运行效果:
默认头像
↓
点击
↓
打开系统图库
↓
选择图片
↓
头像立即更新
这也是企业项目最常见的写法。
四十一、为什么有时候不能直接显示?
很多同学反馈:
Image(uri)
没有显示。
一般有下面几个原因。
第一种:URI 已失效
例如:
昨天选择图片。
今天:
重新打开。
可能:
URI
↓
权限已经失效
因此:
建议:
如果需要长期保存图片,请在用户授权的前提下复制到应用沙箱目录,或者重新让用户选择,而不是长期依赖一次选择得到的临时 URI。
第二种:URI 格式错误
例如:
正确:
file://media/Photo/10001
错误:
Photo10001
或者:
10001
Image 无法识别。
第三种:图片已经删除
例如:
昨天:
IMG001
今天:
用户:
删除
App:
再次加载:
自然失败。
四十二、什么时候需要 PixelMap?
很多开发者:
一直纠结:
什么时候
Image(uri)
什么时候
PixelMap?
其实:
很简单。
如果:
只是:
显示图片
一般:
URI 就够了。
如果需要:
图片编辑
图片裁剪
图片旋转
图片压缩
添加水印
OCR识别
AI分析
通常需要进一步解码为 PixelMap 或其它图像对象,再进行处理。
可以总结:
只是显示
↓
URI
------------
需要处理图片
↓
PixelMap
四十三、企业上传服务器到底上传什么?
这是面试经常问的问题。
很多新人认为:
上传:
URI
其实:
服务器:
根本不知道:
file://media/Photo/10001
是什么。
真正流程:
URI
↓
读取文件
↓
File
↓
HTTP
↓
Multipart
↓
服务器
所以:
上传的是:
图片内容
不是:
URI
四十四、企业上传流程
真实项目:
一般都是:
点击上传
↓
Photo Picker
↓
返回URI
↓
读取图片
↓
压缩图片
↓
上传OSS
↓
服务器返回URL
↓
页面显示URL
例如:
https://cdn.xxx.com/avatar.jpg
以后:
头像:
一直使用:
网络URL
而不是:
本地URI
四十五、为什么要上传服务器?
因为:
URI:
只能:
当前设备:
使用。
例如:
用户:
A 手机:
file://media/Photo/10001
另一台:
手机:
没有。
服务器:
也没有。
所以:
必须:
上传。
流程:
URI
↓
读取文件
↓
上传
↓
URL
↓
所有设备共享
四十六、多图展示案例
假设:
用户:
选择:
9张图片
返回:
photoUris
直接:
@State
photos: Array<string> = []
选择:
this.photos = result.photoUris
页面:
Grid() {
ForEach(this.photos, (item: string) => {
GridItem() {
Image(item)
.width(100)
.height(100)
}
})
}
效果:
□ □ □
□ □ □
□ □ □
朋友圈:
就是这样实现。
四十七、企业级图片缓存
很多企业:
不会:
每次:
重新:
读取URI
而是:
建立:
缓存。
例如:
URI
↓
内存缓存
↓
磁盘缓存
↓
Image
优点:
第一次:
读取
↓
以后:
秒开
大型应用:
几乎都会这样。
四十八、企业工具类继续优化
上一章:
我们:
封装:
PhotoPickerUtil
这一章:
继续:
升级。
export class PhotoPickerUtil {
static async chooseAvatar(): Promise<string> {
const picker = new photoAccessHelper.PhotoViewPicker()
const options =
new photoAccessHelper.PhotoSelectOptions()
options.maxSelectNumber = 1
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.isPhotoTakingSupported = true
const result = await picker.select(options)
if (result.photoUris.length == 0) {
return ""
}
return result.photoUris[0]
}
}
以后:
页面:
只需要:
this.avatar =
await PhotoPickerUtil.chooseAvatar()
即可。
整个项目:
所有头像:
统一。
四十九、实际项目架构建议
对于中大型 HarmonyOS 项目,建议不要把图片选择、上传、显示逻辑全部写在页面中,而是进行职责拆分:
UI 页面
│
▼
PhotoPickerUtil
│
▼
UploadRepository
│
▼
NetworkService
│
▼
服务器
各层职责:
| 模块 | 职责 |
|---|---|
| Page | 响应用户点击、更新 UI |
| PhotoPickerUtil | 调用 PhotoViewPicker 获取 URI |
| ImageRepository | 图片读取、压缩、缓存 |
| UploadRepository | 上传文件、处理返回结果 |
| NetworkService | HTTP 请求封装 |
这样的结构更符合 MVVM 或企业级分层架构,也方便后续替换上传服务或增加图片处理能力。
本章总结
本章重点解决了实际开发中最容易遇到的问题:
- 理解
photoUris的本质:它是资源 URI,而不是图片本身。 - 学会直接使用 URI 在
Image组件中显示图片。 - 明确什么时候需要进一步处理为
PixelMap。 - 理解图片上传的完整流程:URI → 读取文件 → 上传 → 获取网络 URL。
- 学会多图展示及企业级工具类封装思路。
五十、PhotoViewPicker.select() 执行流程分析
很多开发者只知道:
const result = await picker.select(options)
但是不知道内部发生了什么。
实际上:
执行过程大概如下:
用户点击按钮
↓
ArkUI页面调用select()
↓
PhotoViewPicker向系统请求图库能力
↓
系统拉起PhotoPicker Ability
↓
图库读取媒体数据库
↓
按照PhotoSelectOptions过滤资源
↓
用户选择图片
↓
系统生成URI授权
↓
返回PhotoSelectResult
↓
应用继续执行
五十.一、select() 为什么是异步?
代码:
await picker.select(options)
为什么需要:
await
?
因为:
它不是普通函数。
它涉及:
- 启动系统页面
- 等待用户操作
- 等待系统返回结果
时间是不确定的。
例如:
用户:
打开图库以后:
可能:
30秒选择。
也可能:
5分钟选择。
所以必须:
异步。
五十一、PhotoViewPicker 生命周期
一个完整生命周期:
创建Picker
↓
配置Options
↓
调用select
↓
系统接管
↓
用户操作
↓
返回Result
↓
释放Picker
代码:
const picker =
new photoAccessHelper.PhotoViewPicker()
const result =
await picker.select(options)
结束以后:
result已经保存。
Picker对象:
就没有实际作用。
五十二、URI 权限生命周期(重点)
这是企业开发非常重要的知识。
很多新人踩坑:
为什么昨天保存的URI今天打不开?
原因:
URI权限不是永久的。
五十二.一、临时授权模型
流程:
用户选择图片
↓
系统授权App访问
↓
App获得URI
↓
使用URI
这个授权:
通常:
和本次选择行为绑定。
例如:
立即显示:
Image(uri)
正常。
但是:
保存:
一个月以后
再次访问:
可能失败。
五十三、长期保存图片怎么办?
企业项目:
一般不会长期保存:
file://media/Photo/xxx
而是:
复制或者上传。
流程:
用户选择
↓
获得URI
↓
读取图片
↓
保存到App沙箱
或者
上传服务器
↓
保存新的地址
例如:
服务器:
https://cdn.xxx.com/user/avatar001.png
以后:
使用:
https://cdn.xxx.com/user/avatar001.png
五十四、应用沙箱保存方案
例如:
用户选择头像。
流程:
PhotoPicker
↓
URI
↓
复制
↓
cache目录
↓
Image显示
优势:
不依赖图库。
适合:
- 编辑图片
- 裁剪
- 上传前处理
- 离线缓存
五十五、读取图片元数据
实际开发中:
经常需要:
例如:
上传图片之前:
判断:
图片大小。
图片尺寸。
拍摄时间。
方向。
例如:
用户上传:
身份证。
你可能需要:
宽度 > 1000
高度 > 600
文件 < 5MB
这时候:
需要读取图片信息。
五十六、图片信息包括什么?
常见:
| 信息 | 用途 |
|---|---|
| width | 图片宽度 |
| height | 图片高度 |
| size | 文件大小 |
| format | JPG/PNG |
| orientation | 图片方向 |
| date | 创建时间 |
| location | GPS信息 |
五十七、为什么需要图片宽高?
例如:
头像:
要求:
正方形。
判断:
width == height
商品图片:
要求:
比例:
4:3
文章封面:
要求:
16:9
五十八、图片大小检测
上传前:
建议:
判断:
5MB以内
例如:
if(size > 5 * 1024 * 1024){
showToast("图片太大")
}
避免:
上传失败。
五十九、大图片为什么需要压缩?
手机照片:
现在非常大。
例如:
手机拍照:
可能:
4000×3000
8MB
但是:
头像:
只需要:
300×300
如果不压缩:
问题:
- 上传慢
- 流量消耗
- 服务器压力
- 页面卡顿
六十、图片处理最佳流程
企业推荐:
用户选择图片
↓
获取URI
↓
读取图片
↓
判断尺寸
↓
压缩
↓
转换格式
↓
上传
↓
保存URL
六十一、完整图片上传架构
推荐:
Page
|
|
PhotoPickerService
|
|
ImageProcessService
|
|
UploadService
|
|
Server / OSS
Page层
负责:
按钮点击。
例如:
Button("上传")
.onClick(()=>{
upload()
})
PhotoPickerService
负责:
选择图片。
例如:
selectImage()
ImageProcessService
负责:
- 压缩
- 裁剪
- 转码
UploadService
负责:
网络上传。
六十二、企业级 PhotoPicker 服务封装
推荐:
创建:
common
└── service
└── PhotoPickerService.ets
代码:
import {
photoAccessHelper
} from '@kit.MediaLibraryKit'
export class PhotoPickerService {
static async selectImages(
maxCount:number = 1
):Promise<Array<string>>{
const picker =
new photoAccessHelper.PhotoViewPicker()
const options =
new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber =
maxCount
const result =
await picker.select(options)
return result.photoUris
}
}
页面调用:
const images =
await PhotoPickerService.selectImages(9)
页面完全不知道:
Picker细节。
六十三、为什么企业喜欢封装?
原因:
未来需求变化。
例如:
今天:
选择图片
明天:
增加:
压缩
裁剪
水印
上传
如果全部写页面:
几十个页面全部修改。
封装以后:
修改:
一个Service即可。
六十四、常见错误分析
错误1:
Cannot read URI
原因:
URI权限失效。
解决:
重新选择或者保存副本。
错误2:
Image blank
原因:
图片加载失败。
检查:
- URI
- 生命周期
- 权限
错误3:
select failed
原因:
可能:
- Ability状态异常
- 页面销毁
- 系统Picker异常
六十五、面试高频问题
Q1:HarmonyOS如何选择用户图片?
答案:
使用:
photoAccessHelper.PhotoViewPicker
通过系统Picker让用户主动选择。
Q2:为什么不用直接读取相册?
答案:
因为:
- 权限过大
- 隐私风险
- 官方推荐Picker模式
Q3:返回的数据是什么?
答案:
photoUris:Array<string>
是URI,不是图片对象。
Q4:URI可以永久保存吗?
答案:
不能简单认为永久有效。
长期使用:
建议:
复制到沙箱或者上传服务器。
Q5:多图选择如何实现?
设置:
options.maxSelectNumber
即可。
六十六、本章总结
本章重点:
✅ 理解 select() 工作流程
✅ 理解 Photo Picker 异步原因
✅ 掌握 URI 权限生命周期
✅ 明白为什么企业需要上传服务器
✅ 学会图片元数据处理思路
✅ 掌握企业级图片模块架构设计
到这里,PhotoViewPicker 已经从:
“会调用”
升级到了:
“理解原理 + 能设计企业方案”。
六十七、最终效果
完成以后:
页面:
+-----------------------+
👤
当前头像
[ 修改头像 ]
+-----------------------+
点击:
修改头像
↓
系统PhotoViewPicker
↓
选择图片
↓
返回URI
↓
显示预览
↓
上传
↓
刷新头像
六十八、项目结构设计
企业项目不要把所有代码写在页面。
推荐:
entry
├── pages
│ └── UserProfilePage.ets
├── viewmodel
│ └── UserProfileVM.ets
├── service
│ ├── PhotoPickerService.ets
│ └── UploadService.ets
├── model
│ └── UserInfo.ets
└── utils
└── ImageUtil.ets
职责:
| 模块 | 职责 |
|---|---|
| Page | UI展示 |
| ViewModel | 业务状态 |
| PhotoPickerService | 选择图片 |
| ImageUtil | 压缩处理 |
| UploadService | 上传服务器 |
六十九、定义用户模型
创建:
model/UserInfo.ets
代码:
export interface UserInfo {
userId:string
nickname:string
avatar:string
}
例如:
{
"userId":"10001",
"nickname":"Tom",
"avatar":
"https://cdn.xxx.com/avatar.png"
}
七十、创建 PhotoPickerService
负责:
只处理图片选择。
import {
photoAccessHelper
}
from '@kit.MediaLibraryKit'
export class PhotoPickerService {
static async chooseAvatar()
:Promise<string>{
const picker =
new photoAccessHelper.PhotoViewPicker()
const options =
new photoAccessHelper.PhotoSelectOptions()
// 只选择图片
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
// 单张
options.maxSelectNumber = 1
// 支持拍照
options.isPhotoTakingSupported = true
const result =
await picker.select(options)
if(result.photoUris.length>0){
return result.photoUris[0]
}
return ""
}
}
七十一、为什么头像只能选择1张?
因为:
头像业务:
天然单选。
逻辑:
头像
↓
一张图片
↓
覆盖旧头像
所以:
options.maxSelectNumber=1
如果设置:
9
用户可能:
选择:
9张头像。
业务逻辑反而混乱。
七十二、创建上传服务
模拟:
上传服务器。
真实项目:
可能:
OSS
对象存储
HTTP接口。
创建:
service/UploadService.ets
代码:
export class UploadService {
static async uploadAvatar(
uri:string
):Promise<string>{
console.info(
"upload:",
uri
)
// 模拟服务器返回
return Promise.resolve(
"https://cdn.xxx.com/avatar/new.png"
)
}
}
真实流程:
应该是:
URI
↓
读取文件
↓
Multipart
↓
HTTP POST
↓
服务器保存
↓
返回URL
七十三、创建 ViewModel
ViewModel负责:
业务状态管理。
import {
PhotoPickerService
}
from '../service/PhotoPickerService'
import {
UploadService
}
from '../service/UploadService'
export class UserProfileVM {
avatar:string=""
async changeAvatar(){
//1.选择图片
let uri =
await PhotoPickerService.chooseAvatar()
if(uri===""){
return
}
//2.上传
let url =
await UploadService.uploadAvatar(uri)
//3.更新头像
this.avatar=url
}
}
七十四、为什么需要ViewModel?
如果直接写页面:
Button(){
选择图片
上传
修改状态
}
问题:
页面越来越大。
例如以后增加:
- 修改昵称
- 修改手机号
- 实名认证
- 上传证件
页面:
几千行。
MVVM:
页面:
只负责显示。
七十五、页面实现
创建:
pages/UserProfilePage.ets
代码:
import {
UserProfileVM
}
from '../viewmodel/UserProfileVM'
@Entry
@Component
struct UserProfilePage {
private vm =
new UserProfileVM()
@State
avatar:string=""
build(){
Column(){
Image(
this.avatar
)
.width(120)
.height(120)
.borderRadius(60)
Button("修改头像")
.margin({top:30})
.onClick(async()=>{
await this.vm.changeAvatar()
this.avatar =
this.vm.avatar
})
}
}
}
七十六、运行流程分析
点击按钮:
Button
↓
changeAvatar()
↓
PhotoPickerService
↓
系统图库
↓
用户选择
↓
返回URI
↓
UploadService
↓
服务器URL
↓
更新avatar
↓
Image刷新
七十七、状态刷新问题
这里很多新人会遇到:
图片上传成功。
但是页面不更新。
原因:
状态没有绑定。
错误:
let avatar=""
修改:
不会刷新。
正确:
@State
avatar:string=""
因为:
ArkUI状态管理机制:
监听:
avatar变化
↓
重新build
↓
刷新Image
七十八、图片压缩设计
真实项目:
不能直接上传原图。
例如:
手机照片:
4000×3000
8MB
头像:
实际:
300×300
几十KB
所以:
需要:
选择图片
↓
压缩
↓
上传
七十九、头像压缩策略
推荐:
头像:
最大宽度:
512px
质量:
80%
格式:
JPEG
流程:
原图
4000×3000
↓
缩放
512×384
↓
压缩
↓
上传
八十、图片上传最佳实践
不要:
选择
↓
直接上传
推荐:
选择
↓
检查格式
↓
检查大小
↓
压缩
↓
生成缩略图
↓
上传
↓
保存URL
八十一、错误处理
生产环境:
必须处理:
用户取消
if(uri==""){
return
}
上传失败
例如:
网络断开:
try{
upload()
}
catch(e){
Toast("上传失败")
}
图片损坏
读取失败:
提示:
图片无法使用
八十二、完整业务链路总结
最终企业方案:
用户点击头像
↓
PhotoViewPicker
↓
photoUri
↓
ImageProcessService
↓
图片压缩
↓
UploadService
↓
OSS/CDN服务器
↓
avatar URL
↓
页面刷新
八十三、本章总结
本章完成了一个完整的:
HarmonyOS NEXT头像上传模块设计。
重点:
✅ Service层封装PhotoViewPicker
✅ ViewModel管理业务状态
✅ Page只负责UI
✅ URI到服务器URL完整流程
✅ ArkUI状态刷新机制
✅ 图片压缩设计思想
✅ 企业项目目录结构
到这里,你已经可以在实际鸿蒙项目中实现:
- 用户头像
- 企业头像
- 商品图片
- 发布动态图片
- 评论图片上传
【HarmonyOS NEXT】鸿蒙中如何获取用户相册图片?PhotoViewPicker 超详细实战(2026最新版)——多图选择、九宫格展示、删除、排序、批量上传实战(七)
前面我们完成了:
✅ 单图选择
✅ 头像上传
✅ URI处理
✅ 图片上传架构但是实际 App 中,更常见的是:
- 微信朋友圈发布图片
- 小红书发布笔记
- 电商评价晒单
- 企业报销上传附件
这些场景都需要:
一次选择多张图片 + 九宫格展示 + 删除 + 上传。
本章我们完整实现一个企业级多图选择组件。
八十四、多图选择业务分析
典型效果:
+--------------------------------+
已选择图片:
┌────┬────┬────┐
│ 图1 │ 图2 │ 图3 │
├────┼────┼────┤
│ 图4 │ 图5 │ 图6 │
├────┼────┼────┤
│ + │ │ │
└────┴────┴────┘
+--------------------------------+
功能:
- 点击 "+" 添加图片
- 打开系统图库
- 多选图片
- 显示缩略图
- 点击删除
- 上传服务器
八十五、多图选择核心配置
和头像最大的区别:
头像:
maxSelectNumber = 1
多图:
maxSelectNumber = 9
例如:
朋友圈:
options.maxSelectNumber = 9
完整:
const options =
new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber = 9
八十六、为什么限制9张?
很多 App 都限制:
9张。
原因:
第一:UI布局
手机屏幕:
3列九宫格。
刚好:
1 2 3
4 5 6
7 8 9
第二:上传压力
假设:
每张:
5MB
9张:
45MB
已经很大。
第三:用户体验
图片太多:
浏览困难。
八十七、创建多图选择Service
创建:
service/MultiPhotoPickerService.ets
代码:
import {
photoAccessHelper
}
from '@kit.MediaLibraryKit'
export class MultiPhotoPickerService {
static async selectPhotos(
count:number = 9
):Promise<Array<string>>{
const picker =
new photoAccessHelper.PhotoViewPicker()
const options =
new photoAccessHelper.PhotoSelectOptions()
options.MIMEType =
photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE
options.maxSelectNumber =
count
const result =
await picker.select(options)
return result.photoUris
}
}
调用:
let photos =
await MultiPhotoPickerService.selectPhotos(9)
返回:
[
"file://media/Photo/001",
"file://media/Photo/002",
"file://media/Photo/003"
]
八十八、页面状态设计
页面需要保存:
已选择图片。
定义:
@State
photos:Array<string> = []
例如:
[
"uri1",
"uri2",
"uri3"
]
八十九、选择图片按钮
代码:
Button("+")
.onClick(async()=>{
let result =
await MultiPhotoPickerService.selectPhotos(9)
this.photos =
this.photos.concat(result)
})
逻辑:
第一次:
选择3张:
photos:
[
1,
2,
3
]
第二次:
再选择2张:
合并:
[
1,
2,
3,
4,
5
]
九十、九宫格展示
HarmonyOS ArkUI 推荐:
Grid
实现。
代码:
Grid(){
ForEach(
this.photos,
(item:string)=>{
GridItem(){
Image(item)
.width(100)
.height(100)
}
}
)
}
效果:
┌────┬────┬────┐
│图片│图片│图片│
├────┼────┼────┤
│图片│图片│图片│
└────┴────┴────┘
九十一、增加删除按钮
实际 App:
图片右上角:
有删除:
┌──────┐
│图片 X│
└──────┘
结构:
GridItem
|
Stack
/ \
Image 删除按钮
代码:
GridItem(){
Stack(){
Image(item)
Button("×")
.onClick(()=>{
this.removePhoto(index)
})
}
}
九十二、删除图片
核心:
数组删除。
例如:
原:
[
A,
B,
C
]
删除B:
[
A,
C
]
代码:
removePhoto(index:number){
this.photos.splice(index,1)
}
因为:
@State
监听:
数组变化。
UI:
自动刷新。
九十三、限制最大数量
例如:
最多9张。
判断:
if(this.photos.length >= 9){
return
}
按钮:
隐藏:
if(this.photos.length < 9){
显示+
}
效果:
0张:
+
8张:
8张 +
9张:
9张
九十四、完整多图页面示例
@Entry
@Component
struct PublishPage {
@State
photos:Array<string> = []
async addPhoto(){
if(this.photos.length >=9){
return
}
let result =
await MultiPhotoPickerService.selectPhotos(
9-this.photos.length
)
this.photos =
this.photos.concat(result)
}
removePhoto(index:number){
this.photos.splice(index,1)
}
build(){
Column(){
Grid(){
ForEach(
this.photos,
(item:string,index:number)=>{
GridItem(){
Stack(){
Image(item)
Button("删除")
.onClick(()=>{
this.removePhoto(index)
})
}
}
}
)
}
Button("添加图片")
.onClick(()=>{
this.addPhoto()
})
}
}
}
九十五、为什么每次选择数量要动态计算?
很多开发者写:
maxSelectNumber=9
但是有问题。
例如:
已经选择:
6张
再次打开:
还能选:
9张。
最终:
15张。
错误。
正确:
9 - 当前数量
例如:
当前:
6
那么:
9-6=3
只能选择:
3张。
九十六、图片排序功能
很多App支持:
长按拖动:
改变顺序。
例如:
原:
1 2 3
调整:
3 1 2
数据层:
其实就是:
数组移动。
例如:
[
A,
B,
C
]
移动:
C到第一:
[
C,
A,
B
]
企业项目:
通常使用:
拖拽手势:
Gesture
+
Array操作
实现。
九十七、多图上传设计
选择:
9张。
不要:
同时上传。
例如:
错误:
9个请求同时发送
可能:
网络压力大。
推荐:
队列上传。
流程:
图片1
↓
上传成功
↓
图片2
↓
上传成功
↓
图片3
九十八、上传队列设计
例如:
async uploadImages(
photos:Array<string>
){
for(let item of photos){
await upload(item)
}
}
优点:
- 稳定
- 易控制
- 易显示进度
九十九、上传进度展示
例如:
朋友圈:
显示:
上传中...
3/9
状态:
currentIndex:number
上传:
1/9
2/9
3/9
一百、多图上传完整流程
最终:
用户点击添加图片
↓
PhotoViewPicker
↓
返回多个URI
↓
保存数组
↓
Grid展示
↓
用户删除/排序
↓
点击发布
↓
压缩图片
↓
循环上传
↓
获取服务器URL
↓
提交文章数据
一百零一、本章总结
本章完成了企业级多图选择能力:
✅ 多图片选择
✅ 九宫格展示
✅ 动态数量限制
✅ 删除图片
✅ 添加图片
✅ 图片排序设计
✅ 批量上传架构
✅ 上传队列思想
到这里,你已经可以实现类似:
- 微信朋友圈图片发布
- 小红书图片发布
- 电商评价图片
- 企业OA附件上传
等完整功能。
一百零二、为什么图片页面容易卡顿?
图片和普通文本最大的区别就是:
图片需要解码。
例如:
一张手机拍摄的照片:
4000 × 3000
JPEG 文件可能只有:
4 MB
但是解码后:
4000 × 3000 × 4(Byte)
≈ 48 MB
也就是说:
磁盘:
4MB
↓
解码
↓
内存:
48MB
这就是为什么:
很多图片页面:
文件不大
但是
内存很高
一百零三、Photo Picker 返回 URI 的真正意义
前面说过:
Photo Picker:
返回:
Array<string>
例如:
file://media/Photo/001
很多人认为:
只是方便。
其实真正目的:
就是:
避免一次性加载图片。
流程:
URI
↓
真正需要显示
↓
读取
↓
解码
↓
显示
而不是:
选择
↓
全部解码
↓
OOM
一百零四、错误示范:一次加载全部图片
很多新人喜欢这样:
选择100张
↓
全部读取
↓
全部生成PixelMap
↓
全部显示
假设:
每张:
30MB
100张:
3000MB
几乎所有手机:
都会:
OOM
一百零五、正确方案:按需加载
正确流程:
Grid
↓
当前屏幕
↓
9张
↓
读取
↓
滑动
↓
释放
↓
读取下一屏
只有:
用户看到:
才加载。
一百零六、Grid 为什么比 Column 更适合图片?
很多新人:
这样写:
Column() {
ForEach(...)
}
问题:
Column 会一次性创建所有子组件。
例如:
100张图片
↓
100个Image
↓
100次布局
↓
100次测量
性能很差。
推荐:
Grid
或者:
List
它们支持:
- 可视区域加载
- 滑出区域回收
- 减少内存占用
一百零七、LazyForEach 的优势
如果图片数量很多:
推荐:
LazyForEach(...)
它不会一次创建全部组件。
例如:
1000张图片。
普通:
1000个Image
Lazy:
当前屏幕
↓
约10个Image
用户滑动:
再继续创建。
性能提升:
非常明显。
一百零八、图片缩略图思想
很多人:
直接显示:
4000×3000
原图。
其实:
列表:
图片大小:
只有:
100×100
完全没必要。
推荐:
服务器:
返回:
thumbnail
例如:
avatar.jpg
↓
avatar_thumb.jpg
页面:
列表:
加载:
缩略图。
点击:
再加载:
原图。
一百零九、为什么微信加载那么快?
微信:
不会:
打开朋友圈:
立即下载:
100张原图
流程:
服务器
↓
缩略图
↓
列表
↓
点击
↓
原图
因此:
非常快。
HarmonyOS:
同样建议:
采用:
这种设计。
一百一十、本地缓存设计
企业项目:
通常:
建立:
两级缓存。
服务器
↓
磁盘缓存
↓
内存缓存
↓
Image
例如:
第一次:
下载
↓
缓存
第二次:
直接读取缓存
避免:
重复下载。
一百一十一、内存缓存
例如:
HashMap
key:
URI
value:
Image对象
下次:
直接:
URI
↓
缓存
↓
显示
不用:
重新读取。
但是:
缓存:
不能无限。
例如:
限制:
50MB
超过:
自动:
淘汰。
一百一十二、LRU 缓存算法
企业图片:
几乎都会:
使用:
LRU
Least Recently Used
意思:
最近最少使用。
例如:
缓存:
A
B
C
再次访问:
A
顺序:
变成:
B
C
A
如果:
缓存满了:
删除:
B
因为:
最久没有访问。
一百一十三、为什么不能一直缓存?
例如:
朋友圈:
10000张图片。
全部缓存:
10000 × 5MB
=
50GB
根本不现实。
所以:
缓存:
一定:
有限制。
一百一十四、图片预加载
例如:
当前:
用户:
正在看:
图片1
图片2
图片3
后台:
偷偷加载:
图片4
图片5
图片6
用户:
继续滑动:
立即显示。
体验:
非常流畅。
一百一十五、上传图片为什么也要压缩?
很多公司:
规定:
上传:
不能超过:
2MB
因为:
服务器:
压力。
例如:
原图:
10MB
压缩:
600KB
用户:
基本:
看不出来。
但是:
上传:
快很多。
一百一十六、上传线程设计
错误:
UI线程
↓
上传图片
↓
页面卡死
正确:
UI线程
↓
显示Loading
↓
后台线程上传
↓
通知UI刷新
图片上传、压缩、文件读取等耗时操作,都应该放在异步流程中执行,避免阻塞界面渲染。
一百一十七、分页加载图片
如果:
图库:
10000张。
不要:
一次读取
10000
推荐:
分页。
例如:
第一页:
50
↓
第二页:
50
↓
第三页:
50
企业项目:
基本:
都是:
分页。
一百一十八、朋友圈发布流程分析
真正:
朋友圈:
不是:
选择
↓
上传
↓
完成
而是:
选择图片
↓
立即显示本地URI
↓
后台压缩
↓
后台上传
↓
服务器URL
↓
替换本地状态
↓
点击发布
↓
提交URL列表
用户:
感觉:
上传:
很快。
实际上:
后台:
已经开始。
一百一十九、推荐的图片模块架构
对于大型 HarmonyOS 项目,可以将图片能力拆分为多个模块:
PhotoPickerService
│
▼
ImageDecodeService
│
▼
ImageCompressService
│
▼
ImageCacheService
│
▼
UploadService
各模块职责:
| 模块 | 作用 |
|---|---|
| PhotoPickerService | 调用系统图片选择器 |
| ImageDecodeService | 根据 URI 解码图片 |
| ImageCompressService | 压缩、缩放、格式转换 |
| ImageCacheService | 管理内存与磁盘缓存 |
| UploadService | 上传图片并返回服务器 URL |
这样每个模块职责单一,便于测试、维护和扩展。
一百二十、性能优化检查清单
在项目上线前,建议逐项检查:
| 检查项 | 是否建议 |
|---|---|
使用 PhotoViewPicker 替代自行扫描图库 |
✅ |
使用 Grid / List 展示大量图片 |
✅ |
大量数据使用 LazyForEach |
✅ |
| 不一次性解码全部图片 | ✅ |
| 上传前压缩图片 | ✅ |
| 使用缩略图展示列表 | ✅ |
| 建立内存 + 磁盘缓存 | ✅ |
| 控制缓存大小并采用 LRU | ✅ |
| 上传、压缩使用异步流程 | ✅ |
| 上传完成后保存服务器 URL,而不是长期保存本地 URI | ✅ |
本章总结
这一章重点讲解了 HarmonyOS 图片开发中的性能优化思路:
- 理解为什么图片解码会占用大量内存。
- 理解
PhotoViewPicker返回 URI 的设计目的。 - 学会按需加载、延迟解码,而不是一次性创建大量
PixelMap。 - 使用
Grid、List、LazyForEach提升大规模图片列表性能。 - 建立缩略图、缓存、压缩、异步上传等完整优化链路。
- 掌握企业级图片模块的分层设计。
到这里,你已经不仅能使用 PhotoViewPicker,还具备了设计高性能图片模块的能力。
一百二十一、为什么 HarmonyOS 要设计 PhotoViewPicker?
如果大家开发过 Android 6.0 以前的应用,会发现以前读取图片通常是这样:
申请存储权限
↓
扫描整个 SDCard
↓
读取所有图片
↓
自己实现图库 UI
↓
用户选择
这种方案存在很多问题:
- 权限过大
- 应用可以读取全部图片
- 用户隐私风险高
- 每个应用图库体验都不同
HarmonyOS 重新设计了图片访问模型。
新的流程:
App
↓
PhotoViewPicker
↓
系统图库
↓
用户主动授权
↓
返回 URI
↓
App 使用
App 永远只能访问用户选择的资源。
一百二十二、PhotoViewPicker 底层架构
从框架角度来看,可以把它理解成下面的结构:
ArkTS Application
│
▼
PhotoViewPicker API
│
▼
MediaLibraryKit
│
▼
System Photo Picker Ability
│
▼
Media Database
│
▼
Photo Asset
│
▼
URI 返回
可以看到:
App 从始至终没有直接访问图库数据库。
真正访问数据库的是:
System Photo Picker
这也是它安全的根本原因。
一百二十三、Photo Picker 为什么返回 URI?
很多人第一次学习都会问:
为什么不是:
Bitmap
为什么不是:
PixelMap
为什么偏偏返回:
URI
其实:
URI 有四个优势。
第一:减少内存
如果返回:
100张 PixelMap
可能:
占用:
3GB
而:
URI:
只是:
字符串
例如:
file://media/Photo/100001
占用几乎可以忽略。
第二:延迟加载
真正流程:
URI
↓
Image
↓
需要显示
↓
读取
↓
解码
只有真正显示:
才加载。
第三:权限隔离
URI:
包含:
系统授权。
App:
不能随便:
修改。
第四:统一资源管理
以后:
不仅:
图片。
视频:
音频:
文档:
都可以:
统一:
URI。
这也是 HarmonyOS 整个资源管理体系的重要思想。
一百二十四、PhotoViewPicker 与 PhotoAccessHelper 的区别
这是 HarmonyOS 面试中最经典的问题之一。
很多同学容易混淆。
下面用表格进行对比。
| 对比项 | PhotoViewPicker | PhotoAccessHelper |
|---|---|---|
| 使用方式 | 系统选择器 | 编程访问媒体库 |
| 是否需要用户主动选择 | ✅ | ❌ |
| 是否适合普通应用 | ✅ | ⚠️ 视业务而定 |
| 是否能浏览全部图库 | ❌ | ✅(有权限时) |
| 权限要求 | 通常无需读取整个图库权限 | 需要相应媒体库访问权限 |
| 官方推荐场景 | 上传头像、发朋友圈、选择图片 | 图库、相册管理、媒体管理工具 |
一句话总结:
普通 App 选
PhotoViewPicker,媒体管理类 App 才考虑PhotoAccessHelper。
一百二十五、PhotoViewPicker 为什么更安全?
传统方案:
App
↓
读取全部图片
↓
自己筛选
风险:
10000张图片
↓
App 全部可见
Photo Picker:
系统图库
↓
用户选择
↓
仅返回:
2张
App:
永远:
只能:
看到:
2张
因此:
符合:
最小权限原则(Least Privilege)。
一百二十六、Photo Picker 生命周期
整个生命周期:
创建 Picker
↓
创建 Options
↓
select()
↓
系统Ability
↓
用户选择
↓
生成URI
↓
返回Result
↓
结束
结束以后:
Picker:
就可以释放。
因此:
企业项目:
一般:
不会:
长期持有。
一百二十七、为什么 select() 必须 await?
很多新人:
喜欢:
picker.select(options)
console.info("结束")
结果:
日志:
先输出。
原因:
Picker:
本质:
是:
跨 Ability 调用
用户:
可能:
几分钟:
才选择。
因此:
必须:
await
等待。
一百二十八、PhotoSelectResult 数据结构
返回:
const result =
await picker.select(options)
很多人:
只知道:
result.photoUris
其实:
整个:
Result:
可以理解成:
PhotoSelectResult
│
├── photoUris
├── (未来版本可能扩展更多字段)
└── 状态信息
因此:
以后:
API:
扩展:
不会影响:
旧代码。
一百二十九、为什么 PhotoSelectOptions 单独设计?
为什么:
不是:
picker.select(
9,
true,
IMAGE
)
因为:
配置:
越来越多。
例如:
未来:
增加:
是否允许GIF
是否允许RAW
是否允许LivePhoto
默认选中资源
过滤规则
如果:
全部:
参数:
会变成:
select(
9,
true,
true,
false,
IMAGE,
......
)
维护:
灾难。
所以:
官方:
采用:
Builder(配置对象)思想。
一百三十、企业为什么封装 PhotoPickerService?
原因:
解耦。
例如:
今天:
使用:
PhotoViewPicker
以后:
产品:
要求:
企业私有图库
如果:
页面:
全部:
直接:
调用:
Picker。
几十个页面:
全部修改。
封装以后:
只修改:
PhotoPickerService
即可。
一百三十一、高频面试题(一)
Q:PhotoViewPicker 和 PhotoAccessHelper 有什么区别?
参考回答:
PhotoViewPicker是 HarmonyOS 提供的系统媒体选择器,适用于头像上传、发布动态等场景,由用户主动选择资源,应用通常无需申请读取整个图库权限。
PhotoAccessHelper则用于访问和管理媒体库资源,需要相应权限,更适用于图库、相册管理器等需要访问大量媒体资源的应用。
一百三十二、高频面试题(二)
Q:为什么返回 URI,而不是 PixelMap?
回答:
原因:
四点。
第一:
降低内存。
第二:
懒加载。
第三:
权限管理。
第四:
统一资源模型。
一百三十三、高频面试题(三)
Q:为什么官方推荐 PhotoViewPicker?
回答:
因为:
- 更安全
- 用户主动授权
- 不需要读取整个图库
- 统一系统体验
- 隐私保护更好
一百三十四、高频面试题(四)
Q:为什么 URI 不能长期保存?
回答:
因为:
URI 的访问权限可能与用户授权相关,并不适合作为长期持久化引用。
企业方案:
URI
↓
复制沙箱
或者
上传服务器
↓
保存新的地址
一百三十五、高频面试题(五)
Q:为什么企业项目要压缩图片?
回答:
原因:
- 节省流量
- 提高上传速度
- 降低服务器压力
- 提升用户体验
一百三十六、高频面试题(六)
Q:一次选择100张图片应该怎么处理?
回答:
不要:
全部:
解码。
应该:
URI
↓
LazyForEach
↓
按需加载
↓
缩略图
↓
缓存
这样:
内存:
最低。
一百三十七、PhotoViewPicker 最佳实践总结
企业开发中建议遵循以下原则:
| 建议 | 推荐程度 |
|---|---|
普通图片选择使用 PhotoViewPicker |
⭐⭐⭐⭐⭐ |
| 不长期保存临时 URI | ⭐⭐⭐⭐⭐ |
| 上传前压缩图片 | ⭐⭐⭐⭐⭐ |
大量图片使用 Grid + LazyForEach |
⭐⭐⭐⭐⭐ |
统一封装 PhotoPickerService |
⭐⭐⭐⭐⭐ |
| 上传后保存服务器 URL | ⭐⭐⭐⭐⭐ |
| 图片列表优先展示缩略图 | ⭐⭐⭐⭐☆ |
| 使用缓存减少重复解码 | ⭐⭐⭐⭐⭐ |
本章总结
这一章从框架设计角度分析了 PhotoViewPicker:
- 理解了 HarmonyOS 为什么采用系统 Photo Picker 模型。
- 掌握了
PhotoViewPicker与PhotoAccessHelper的定位区别。 - 理解了 URI、异步调用、配置对象等 API 的设计思想。
- 学习了企业开发中的封装原则和最佳实践。
- 总结了多个 HarmonyOS 面试高频问题及回答思路。
到这里,PhotoViewPicker 的核心知识体系已经完整建立,无论是实际开发还是技术面试,都能够系统地进行讲解。
一百三十八、为什么企业不会直接使用 PhotoViewPicker?
很多初学者都会这样写:
Button("选择图片")
.onClick(async () => {
const picker = new photoAccessHelper.PhotoViewPicker()
const options = new photoAccessHelper.PhotoSelectOptions()
const result = await picker.select(options)
})
如果项目只有一个页面,没有问题。
但是企业项目可能有:
- 用户头像
- 修改背景图
- 发布动态
- 上传身份证
- 上传营业执照
- 上传商品图片
- 上传聊天图片
- 上传评价图片
- 上传报销附件
最终整个项目可能有:
30+
页面
每个页面:
都有:
new PhotoViewPicker()
后果:
- 重复代码越来越多
- 修改 API 十分困难
- Bug 修复成本高
- 无法统一上传策略
所以:
企业一定会:
封装 ImagePicker。
一百三十九、ImagePicker 架构设计
推荐目录:
common
│
├── picker
│ ├── ImagePicker.ets
│ ├── VideoPicker.ets
│ ├── MediaPicker.ets
│
├── service
│ ├── UploadService.ets
│ ├── CompressService.ets
│ ├── CacheService.ets
│
├── model
│ ├── MediaItem.ets
│
└── utils
└── FileUtil.ets
职责清晰:
UI
↓
ImagePicker
↓
CompressService
↓
UploadService
↓
Server
一百四十、统一媒体对象设计
不要直接返回:
Array<string>
推荐封装统一数据模型。
export class MediaItem {
uri: string = ""
url: string = ""
width: number = 0
height: number = 0
size: number = 0
type: string = ""
fileName: string = ""
}
优势:
以后:
增加:
GIF
LivePhoto
RAW
HEIF
无需修改业务代码。
一百四十一、ImagePicker 接口设计
建议设计统一入口:
ImagePicker.selectImage()
ImagePicker.selectImages()
ImagePicker.selectVideo()
ImagePicker.selectMedia()
而不是:
PhotoViewPicker
VideoPicker
PhotoPicker
PicturePicker
业务层:
永远:
调用:
ImagePicker
即可。
一百四十二、单图接口
例如:
const avatar =
await ImagePicker.selectImage()
返回:
MediaItem
页面:
完全不知道:
Photo Picker。
一百四十三、多图接口
例如:
const images =
await ImagePicker.selectImages(9)
返回:
Array<MediaItem>
页面:
直接:
Grid 展示。
一百四十四、视频接口
例如:
const video =
await ImagePicker.selectVideo()
底层:
其实:
只是:
修改:
options.MIMEType
即可。
业务:
不用关心。
一百四十五、统一媒体接口
很多 App:
支持:
图片
+
视频
一起选择。
推荐:
const media =
await ImagePicker.selectMedia()
统一:
返回:
MediaItem
通过:
type
区分:
image
video
一百四十六、Promise 封装
推荐:
所有接口:
统一:
Promise。
例如:
async selectImage():Promise<MediaItem>
不要:
一部分:
Promise。
一部分:
Callback。
统一:
开发体验最好。
一百四十七、错误统一处理
不要:
每个页面:
try{
}catch{
}
推荐:
ImagePicker:
内部:
统一:
处理。
例如:
用户取消
↓
返回:
null
上传失败:
统一Toast
统一日志
统一错误码
一百四十八、上传统一封装
页面:
不要:
直接:
HTTP。
推荐:
ImagePicker
↓
UploadService
↓
Network
↓
Server
页面:
只需要:
let url =
await UploadService.upload(item)
一百四十九、缓存统一管理
不要:
页面:
缓存。
推荐:
CacheService
负责:
- Memory Cache
- Disk Cache
- 清理缓存
- LRU 管理
- 图片预加载
所有页面共享。
一百五十、企业级发布流程
例如:
发布动态:
点击发布
↓
ImagePicker
↓
返回URI
↓
压缩
↓
上传
↓
得到URL
↓
提交动态
↓
服务器保存
↓
刷新列表
整个页面:
根本不知道:
URI 如何读取。
一百五十一、GitHub 开源项目推荐结构
如果准备开源一个 HarmonyOS 图片组件,可以采用如下目录:
ImagePicker
│
├── README.md
├── CHANGELOG.md
├── LICENSE
├── docs
│
├── entry
│
├── library
│
├── picker
│
├── upload
│
├── cache
│
├── sample
│
└── test
其中:
sample:示例工程docs:开发文档test:单元测试library:核心能力
这样的结构更容易维护,也方便社区使用。
一百五十二、开发避坑清单(精选)
下面是实际项目中最容易踩坑的问题:
| 问题 | 建议 |
|---|---|
长期保存 photoUri |
❌ 不建议,及时复制或上传 |
| 原图直接上传 | ❌ 上传前压缩 |
| 一次解码大量图片 | ❌ 使用按需加载 |
大量图片使用 Column |
❌ 使用 Grid / List |
| 图片列表不用缩略图 | ❌ 优先加载缩略图 |
| 上传放在 UI 主线程 | ❌ 使用异步流程 |
页面直接调用 PhotoViewPicker |
❌ 封装统一 Service |
| 页面直接处理 HTTP 上传 | ❌ 使用 UploadService |
| 不限制图片数量 | ❌ 设置 maxSelectNumber |
| 不处理用户取消 | ❌ 判断返回结果为空 |
一百五十三、PhotoViewPicker 知识体系
经过整个系列,你已经掌握了完整知识链:
PhotoViewPicker
│
├── 基础使用
├── 参数配置
├── 图片选择
├── 视频选择
├── 多图
├── URI
├── Image显示
├── PixelMap
├── 上传
├── 压缩
├── 缓存
├── Grid
├── LazyForEach
├── 性能优化
├── 企业架构
├── ImagePicker封装
└── 面试专题
到这里,已经覆盖了日常开发中绝大多数图片选择场景。
一百五十四、系列总结
本系列从最基础的 API 入手,逐步深入到企业项目架构,希望帮助开发者不仅能够“会用”,更能够理解背后的设计思想。
整个系列主要覆盖了以下内容:
- 基础能力
- PhotoViewPicker 使用
- PhotoSelectOptions 配置
- 图片、视频选择
- 工程实践
- 单图、多图
- 九宫格展示
- 图片上传
- URI 生命周期
- 性能优化
- 图片解码
- 缩略图
- 缓存
- LazyForEach
- 大图优化
- 企业级架构
- Service 封装
- MVVM 分层
- ImagePicker 组件
- UploadService
- CacheService
- 面试与设计思想
- PhotoViewPicker 原理
- PhotoAccessHelper 对比
- URI 设计原因
- 高频面试题解析
更多推荐


所有评论(0)