👋 你好,欢迎来到我的博客!我是【菜鸟学鸿蒙】
   我是一名在路上的移动端开发者,正从传统“小码农”转向鸿蒙原生开发的进阶之旅。为了把学习过的知识沉淀下来,也为了和更多同路人互相启发,我决定把探索 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 KitPhotoViewPicker、PhotoSelectOptions
打开用户选中的 URICore File KitfileIo.open()
图片解码Image Kitimage.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 很适合头像、上传附件、识图等“用户主动选文件”的场景:应用拿到的是用户明确选择的资源,而不是默认获得浏览整个图库的能力。

三、最小实践要做什么

页面只保留三个元素:

  1. 一个“选择图片”按钮;
  2. 一块图片预览区域;
  3. 一行当前状态文字。

点击按钮后:

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、识图这类功能时,可以先记住四点:

  1. 用户主动选择媒体资源时,优先研究 PhotoViewPicker,不必一开始就自己实现图库页面。
  2. PhotoViewPicker.select() 得到的核心结果是 URI,图像算法需要 PixelMap 时,再通过 Core File Kit 和 Image Kit 解码。
  3. Picker 选择媒体和直接读取整个媒体库不是同一套权限场景,不要机械给 Picker 添加 READ_IMAGEVIDEO。
  4. 真正拿到非空 photoUris 后再进入业务处理,同时及时释放 File、ImageSource 和 PixelMap。

对于 HarmonyOS 7 项目,还应该明确当前版本基线:HarmonyOS 7 对应 API 26.0.0;如果代码来自旧版本示例,最好重新回到当前官方 API 文档确认导入路径和接口状态,而不是直接复制历史代码。

如果你的页面里现在还有一套只为了“让用户选一张照片”而实现的自定义图库,不妨重新审视一下:业务真正需要的是图库管理能力,还是只需要一次由用户主动发起的媒体选择?

📝 写在最后

如果你觉得这篇文章对你有帮助,或者有任何想法、建议,欢迎在评论区留言交流!你的每一个点赞 👍、收藏 ⭐、关注 ❤️,都是我持续更新的最大动力!

我是一个在代码世界里不断摸索的小码农,愿我们都能在成长的路上越走越远,越学越强!

感谢你的阅读,我们下篇文章再见~👋

✍️ 作者:菜鸟不学编程
🧵 本文原创,转载请注明出处。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐