系列文章: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
  • 使用 GridListLazyForEach 提升大规模图片列表性能。
  • 建立缩略图、缓存、压缩、异步上传等完整优化链路。
  • 掌握企业级图片模块的分层设计。

到这里,你已经不仅能使用 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 模型。
  • 掌握了 PhotoViewPickerPhotoAccessHelper 的定位区别。
  • 理解了 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 入手,逐步深入到企业项目架构,希望帮助开发者不仅能够“会用”,更能够理解背后的设计思想。

整个系列主要覆盖了以下内容:

  1. 基础能力
    • PhotoViewPicker 使用
    • PhotoSelectOptions 配置
    • 图片、视频选择
  2. 工程实践
    • 单图、多图
    • 九宫格展示
    • 图片上传
    • URI 生命周期
  3. 性能优化
    • 图片解码
    • 缩略图
    • 缓存
    • LazyForEach
    • 大图优化
  4. 企业级架构
    • Service 封装
    • MVVM 分层
    • ImagePicker 组件
    • UploadService
    • CacheService
  5. 面试与设计思想
    • PhotoViewPicker 原理
    • PhotoAccessHelper 对比
    • URI 设计原因
    • 高频面试题解析
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐