摘要

picker 媒体选择模块是 OpenHarmony 系统原生相册调取能力,支持单图 / 多图选取,常用于头像更换、笔记配图、商品上传等业务场景。API Version23 重构媒体文件权限校验、相册文件 uri 返回规则、多文件批量选取逻辑,修复低版本选取后图片无法渲染、多选数量限制失效、无权限闪退、部分设备相册打开空白等兼容缺陷。低版本工程升级 API23 后常出现:选中图片空白不显示、多选最多只能选 1 张、无媒体权限无弹窗提示、反复打开相册内存堆积等问题。本文基于 DevEco Studio 适配 OpenHarmony API23 及以上版本,封装全局图片选择工具类,实现单图选取、多图批量选取两大核心能力,搭配头像更换、笔记配图上传两个实战页面提供完整可运行代码,输出媒体权限配置、图片 uri 渲染、内存释放规范,汇总版本升级兼容故障解决方案,为鸿蒙图片选取功能开发提供标准化实操模板。

关键词

OpenHarmony;ArkUI;API Version23;picker;相册选择;图片选取;媒体权限;图片 uri 渲染

一、引言

1.1 相册选择开发背景

个人头像、笔记配图、表单图片上传等功能均需要调取系统相册读取本地图片,原生 picker 模块封装系统底层媒体库,无需自行遍历本地文件,权限、文件隔离逻辑由系统统一管控,开发成本更低。 OpenHarmony API Version23 针对 picker 底层媒体引擎完成升级,核心变更点:

  1. 统一媒体读取权限校验逻辑,未授予权限统一捕获异常并弹窗提示;
  2. 标准化 PhotoViewPicker 选取返回媒体 uri 格式,Image 组件可直接加载;
  3. 强化多选数量 count 参数约束,严格限制最大选取图片数量;
  4. 优化相册窗口销毁逻辑,多次打开相册无内存残留;
  5. 废弃旧版 fileSystem 访问媒体文件方案,强制使用 picker 系统相册;
  6. 区分图片 / 视频媒体类型,支持仅筛选图片资源,过滤视频文件。

大量 API9~11 旧项目升级后,选取图片页面无展示、多选功能失效,根源是旧版文件路径方案权限失效,未使用标准 picker 接口,因此掌握 API23 标准相册选取封装规范是图片类业务必备技能。

1.2 开发环境与测试场景

开发工具:DevEco Studio 5.0 及以上 适配系统:OpenHarmony API Version23、HarmonyOS NEXT 导入模块:@ohos.file.picker 权限配置:module.json5 配置 READ_MEDIA_IMAGES 媒体图片读取权限 测试场景:头像单张图片更换、笔记批量配图多选、选取失败异常捕获、无权限弹窗提示

二、API23+ picker 相册核心 API 与版本变更说明

2.1 相册选择器创建

ets

import picker from '@ohos.file.picker'
// 创建图片相册选择实例
const photoPicker = new picker.PhotoViewPicker()

2.2 选取配置参数

  1. MIMEType:媒体筛选类型,PhotoMIMEType.IMAGE 仅展示图片
  2. count:最大可选图片数量,1 为单选,2~9 为多选

2.3 选取结果返回

select () 异步调用返回 PhotoSelectResult 对象,photoUris 为选中图片 uri 数组,可直接传入 Image 组件渲染。

2.4 API23 废弃与强制约束

  1. 废弃直接读取媒体文件本地绝对路径,仅允许使用系统返回 uri 渲染图片;
  2. 未配置 READ_MEDIA_IMAGES 权限会直接抛出异常,必须 try-catch 捕获;
  3. count 参数最大限制 9,传入大于 9 的数值会自动截断为 9;
  4. 禁止循环频繁创建 PhotoViewPicker 实例,工具类单次调用自动销毁;
  5. 不支持自定义相册 UI,只能调用系统原生相册页面。

三、API23 标准基础示例代码

3.1 单张图片选取基础调用

ets

import picker from '@ohos.file.picker'
import promptAction from '@ohos.promptAction'

async function getSingleImage() {
  try {
    const pick = new picker.PhotoViewPicker()
    const res = await pick.select({
      MIMEType: picker.PhotoMIMEType.IMAGE,
      count: 1
    })
    if (res.photoUris.length > 0) {
      return res.photoUris[0]
    }
  } catch (err) {
    promptAction.showToast({message:"相册打开失败,请授予图片权限"})
  }
  return ""
}

3.2 多张图片批量选取基础调用

ets

async function getMultiImage(maxNum:number=6) {
  try {
    const pick = new picker.PhotoViewPicker()
    const res = await pick.select({
      MIMEType: picker.PhotoMIMEType.IMAGE,
      count: maxNum
    })
    return res.photoUris
  } catch (err)
    promptAction.showToast({message:"相册打开失败"})
    return []
  }
}

四、两大业务完整实战案例(全兼容 API23)

4.1 实战一:全局图片选择工具类封装(utils/image_picker.ets)

ets

import picker from '@ohos.file.picker'
import promptAction from '@ohos.promptAction'

class ImagePickerUtil {
  private static instance: ImagePickerUtil

  static getInstance(): ImagePickerUtil {
    if (!ImagePickerUtil.instance) {
      ImagePickerUtil.instance = new ImagePickerUtil()
    }
    return ImagePickerUtil.instance
  }

  // 单选单张图片,返回图片uri字符串
  async selectSingle(): Promise<string> {
    try {
      const photoPicker = new picker.PhotoViewPicker()
      const result = await photoPicker.select({
        MIMEType: picker.PhotoMIMEType.IMAGE,
        count: 1
      })
      const uriList = result.photoUris
      if (uriList && uriList.length > 0) {
        return uriList[0]
      }
      return ""
    } catch (error) {
      promptAction.showToast({ message: "获取图片失败,请开启相册权限" })
      console.error("单选图片异常", JSON.stringify(error))
      return ""
    }
  }

  // 多选图片,传入最大选择数量,默认最多9张
  async selectMulti(maxCount: number = 9): Promise<string[]> {
    try {
      const photoPicker = new picker.PhotoViewPicker()
      const result = await photoPicker.select({
        MIMEType: picker.PhotoMIMEType.IMAGE,
        count: maxCount
      })
      return result.photoUris || []
    } catch (error) {
      promptAction.showToast({ message: "获取图片失败,请开启相册权限" })
      console.error("多选图片异常", JSON.stringify(error))
      return []
    }
  }
}

export default ImagePickerUtil.getInstance()

4.2 实战二:头像更换页面(单选图片场景)

ets

import ImagePickerUtil from '../utils/image_picker'

@Entry
@Component
struct AvatarPage {
  @State avatarUri: ResourceStr = ""

  // 打开相册更换头像
  async changeAvatar() {
    const uri = await ImagePickerUtil.selectSingle()
    if (uri) {
      this.avatarUri = uri
    }
  }

  build() {
    Column({ space: 30 }) {
      Text("头像更换功能演示")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
      
      // 头像展示区域
      if (this.avatarUri) {
        Image(this.avatarUri)
          .width(160)
          .height(160)
          .clipShape(Circle())
          .objectFit(ImageFit.Cover)
      } else {
        Text("点击下方按钮选择头像")
          .width(160)
          .height(160)
          .backgroundColor("#eeeeee")
          .borderRadius(80)
          .textAlign(TextAlign.Center)
          .fontColor("#999999")
      }

      Button("打开相册选择头像")
        .width(280)
        .height(48)
        .backgroundColor("#007DFF")
        .fontColor(Color.White)
        .onClick(async () => await this.changeAvatar())
    }
    .width("100%")
    .height("100%")
    .padding(20)
    .justifyContent(FlexAlign.Center)
  }
}

4.3 实战三:笔记多图配图页面(多选图片场景)

ets

import ImagePickerUtil from '../utils/image_picker'

@Entry
@Component
struct NoteImagePage {
  @State imgUriList: string[] = []

  // 多选最多6张配图
  async addNoteImage() {
    const list = await ImagePickerUtil.selectMulti(6)
    if (list.length === 0) return
    // 合并已有图片与新选图片
    this.imgUriList = [...this.imgUriList, ...list]
  }

  build() {
    Column({ space: 20 }) {
      Text("笔记多图上传(最多6张)")
        .fontSize(22)
        .fontWeight(FontWeight.Bold)
      
      // 图片网格展示
      Wrap({ space: 10, wrapDirection: WrapDirection.Row }) {
        ForEach(this.imgUriList, (uri: string) => {
          Image(uri)
            .width(100)
            .height(100)
            .borderRadius(8)
            .objectFit(ImageFit.Cover)
        })
      }
      .width("100%")
      .padding(10)
      .backgroundColor("#f5f5f5")
      .borderRadius(10)

      Button("添加相册配图")
        .width(280)
        .height(48)
        .backgroundColor("#007DFF")
        .onClick(async () => await this.addNoteImage())
    }
    .width("100%")
    .height("100%")
    .padding(20)
  }
}

五、API23+ 相册选取适配与性能优化规范

5.1 媒体权限配置规范

  1. module.json5 必须添加 READ_MEDIA_IMAGES 权限,否则直接捕获异常;
  2. 应用首次打开相册系统自动弹出权限申请弹窗,用户拒绝后捕获异常并给出 Toast 提示;
  3. 线上应用不可绕过系统相册,禁止使用文件接口直接读取媒体目录。

5.2 图片渲染性能规范

  1. 系统返回 uri 直接传入 Image 组件,无需转换本地路径,API23 原生适配;
  2. 头像、缩略图固定宽高,搭配 objectFit:ImageFit.Cover 避免图片拉伸变形;
  3. 多选图片页面限制最大选取数量,一次性加载大量图片会造成页面卡顿;
  4. 页面 aboutToDisappear 清空图片数组,释放图片渲染内存。

5.3 业务分层规范

  1. 相册选取逻辑统一封装至 image_picker 工具类,页面仅调用方法,不重复编写 picker 代码;
  2. 单选头像业务调用 selectSingle,多图上传业务调用 selectMulti;
  3. 选取操作全部包裹 try-catch,覆盖无权限、相册崩溃、取消选择各类异常。

六、API23 升级高频兼容问题与解决方案

问题 1:选中图片后 Image 组件空白,无法显示图片 解决:使用系统返回 photoUris 原生 uri,不拼接本地文件绝对路径,API23 仅支持媒体 uri 渲染。

问题 2:多选参数 count 设置 9,只能选中 1 张图片 解决:创建 PhotoViewPicker 时 MIMEType 固定为 IMAGE,确认 count 参数在 select 配置内部传入。

问题 3:打开相册直接闪退,无任何提示 解决:module.json5 添加 READ_MEDIA_IMAGES 媒体图片权限,代码增加 try-catch 捕获异常并弹窗提示。

问题 4:多次打开相册页面,应用内存持续上涨 解决:每次 select 调用完成自动销毁 PhotoViewPicker 实例,无需全局缓存 picker 对象。

问题 5:升级 API23 后旧版文件路径读取媒体代码失效 解决:全部删除自定义文件遍历逻辑,统一替换为 PhotoViewPicker 标准相册选取方案。

问题 6:选取图片后页面退出,图片缓存残留占用内存 解决:页面 aboutToDisappear 清空 @State 存储的图片 uri 数组,释放图片资源。

七、总结

picker 相册选取是 OpenHarmony 读取本地图片标准官方方案,API Version23 重构媒体权限、文件 uri、多选限制底层逻辑,彻底解决旧版文件路径权限失效、图片渲染空白、多选失效等核心兼容问题。项目统一封装全局图片选择工具类,区分单选头像、多图配图两套方法,配套异常捕获与权限提示,适配头像更换、笔记配图、表单图片上传等主流业务场景。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐