在这里插入图片描述

一、开篇:这个 API 到底在干什么?

很多刚接触 HarmonyOS NEXT 图片开发的同学,看到 ImageSourcePixelMapImagePacker 三个对象时会觉得有点绕——明明只是加载一张图,怎么搞出三个类?
官方文档写得比较抽象,把三个对象的能力列了一遍,但没讲清楚它们在实际调用链路上是怎么分工的。
本文就直接用一个最小可运行的示例,把这三个对象串起来,让你理解它们各自负责什么、怎么协作。

二、Image Kit 解决什么问题

Image Kit(图片处理服务)是 HarmonyOS 提供的专门用于图片解码、编码和处理的基础框架。它的职责非常清晰:

能力 说明 适用场景
解码(Decode) 将图片文件(JPEG / PNG / WebP / HEIF 等)转换为内存中的 PixelMap 对象 加载图片到界面展示
编码(Encode) PixelMap 压缩成特定格式的二进制数据(比如保存到文件) 编辑图片后导出
像素处理 通过 PixelMap 直接读写像素数据 滤镜、缩略图、水印

和多媒体服务(Media Kit)的区别:Media Kit 处理的是视频、音频流,而 Image Kit 专门处理静态图片的编解码和像素操作。如果你只是需要加载一张图并显示,用 Image 组件配合 image.Source 就够了;但如果需要 解码后编辑像素编码保存,那就必须走 Image Kit 的三件套流程。

三、环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机或平板

四、核心对象关系与极简示例

4.1 三个对象的分工

  • ImageSource:负责读取图片源(文件、资源、Buffer),提供解码参数,输出 PixelMap
  • PixelMap:内存中的位图,可读可写像素数据,是图片处理的核心载体。
  • ImagePacker:负责将 PixelMap 编码成目标格式(JPEG/PNG)的二进制数据,用于保存或传输。

调用链路非常直观:
ImageSourcePixelMap → 处理 → ImagePacker → 编码数据

下面代码演示:从 rawfile 中读取一张图片,解码成 PixelMap,然后直接用 ImagePacker 编码成 JPEG 数据并保存到沙箱文件。

4.2 完整示例代码

// pages/Index.ets
import { image } from '@kit.ImageKit';
import { fileIo } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';

@Entry
@Component
struct Index {
  @State message: string = '';

  build() {
    Column() {
      Button('运行图片编解码')
        .onClick(() => {
          this.runImageDecodeEncode();
        })
      Text(this.message)
        .margin({ top: 20 })
    }
    .width('100%')
    .height('100%')
    .padding(20)
  }

  async runImageDecodeEncode() {
    const context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
    try {
      // 1. 从 rawfile 获取图片二进制数据
      const rawFile: Uint8Array = await context.resourceManager.getRawFileContent('test.jpg');
      const buffer: ArrayBuffer = rawFile.buffer.slice(0); // 复制一份,避免引用问题

      // 2. 创建 ImageSource(指定解码格式为 JPEG)
      const imageSource: image.ImageSource = image.createImageSource(buffer);
      // 可选:设置解码参数(比如缩放到 500x500)
      const decodingOptions: image.DecodingOptions = {
        desiredSize: { width: 500, height: 500 }
      };
      // 3. 解码得到 PixelMap
      const pixelMap: image.PixelMap = await imageSource.createPixelMap(decodingOptions);
      console.info('解码成功, PixelMap尺寸:', pixelMap.getImageInfoSync().size);

      // 4. (可选)对 PixelMap 进行像素处理,例如旋转或颜色变换,此处略

      // 5. 创建 ImagePacker
      const packer: image.ImagePacker = image.createImagePacker();
      const packOptions: image.PackingOption = {
        format: 'image/jpeg',
        quality: 90 // 0~100,数值越大质量越高
      };
      // 6. 编码得到 ArrayBuffer
      const encodedData: ArrayBuffer = await packer.pack(pixelMap, packOptions);
      console.info('编码成功, 数据大小:', encodedData.byteLength);

      // 7. 保存到沙箱文件
      const filePath: string = `${context.filesDir}/encoded_result.jpg`;
      const file: fileIo.File = fileIo.openSync(filePath, fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY);
      fileIo.writeSync(file.fd, encodedData);
      fileIo.closeSync(file);
      this.message = `文件已保存: ${filePath}`;
    } catch (error) {
      console.error('编解码失败:', JSON.stringify(error));
      this.message = '操作失败,请查看日志';
    }
  }
}

代码说明

  • getRawFileContent 返回的是 Uint8Array,注意不能直接作为 ArrayBuffer 传给 createImageSource,需要取其 buffer 属性。
  • createPixelMap 是异步方法,需要 await
  • PackingOptionformat 使用 MIME 类型字符串,例如 'image/jpeg''image/png'
  • 最后保存到 filesDir 下,方便用文件管理查看结果。

五、常见踩坑

坑 1:PixelMap 不手动 release 会导致内存泄漏

现象:频繁解码大图后,应用内存只增不降,最终 OOM 崩溃。
原因PixelMap 底层持有 native 的位图内存,ArkTS 的垃圾回收器虽然会回收 JavaScript 对象,但 native 内存不会自动释放。
解法:在不再使用 PixelMap 后,显式调用 pixelMap.release()。上面示例中 pack 结束后就可以 release。建议用 finally 块确保释放。

try {
  // ... 解码
  // 使用完毕后
  pixelMap.release();
} catch (e) {
  // ...
} finally {
  pixelMap?.release();
}

需要注意:release() 不是幂等的,多次调用会报错,请在确定不再使用后调用一次。

坑 2:ImagePacker.pack 在编码大图时可能超时阻塞 UI

现象:编码一张 4000x3000 的大图时,界面卡顿几秒钟。
原因pack 虽然是异步方法,但它默认在当前线程(主线程)执行耗时的编码计算。
解法:使用 @ohos.taskpool 将其移到子线程执行,或者拆分成 pack 之前先缩放到合理尺寸。推荐在解码时通过 desiredSize 提前缩小,而不是等编码再处理。

// 在解码选项中限制输出尺寸,减少后续编码压力
const decodingOptions: image.DecodingOptions = {
  desiredSize: { width: 1024, height: 1024 }
};
// 如果原图很大,desiredSize 会按比例缩放,不会拉伸

坑 3:JPEG 编码质量 quality 参数与预期不符

现象:设置 quality: 100 仍发现图片变大或质量下降。
原因quality 影响的是压缩算法的量化系数,并不是线性关系。对于 JPEG,100 依然有损压缩。若要无损输出,应使用 PNG 格式。另外,部分硬件编码器可能忽略 quality 参数。
建议:对画质要求高的场景使用 format: 'image/png',或者先测试实际输出大小与视觉差异。

六、最佳实践

  1. 解码时尽量指定输出尺寸:避免解码超大图浪费内存。desiredSize 会根据原图宽高比自动缩放,不会拉伸变形。
  2. 通用 release() 模式:在 try/catch/finally 或使用 using 语法(API 12+ 支持)释放资源,避免忘记释放。
    using pixelMap = await imageSource.createPixelMap(options);
    // 使用 pixelMap,离开作用域自动释放
    
  3. 不要把 PixelMap 存入 @State 长期持有PixelMap 不是状态变量,ArkUI 不会监听其内部变化。如果需要 UI 上显示编辑后的图片,建议将解码后的 PixelMap 直接赋给 Image 组件的 source,编辑时生成新的 PixelMap 替换。

七、Demo 入口

上面示例已经是一个完整可运行的 Index.ets 文件。只要在 rawfile 下放一张 test.jpg,即可测试。若需要从其他路径加载图片,可使用 fileIo 读取对应文件构造 ArrayBuffer

示例代码地址:项目地址

八、FAQ

Q:为什么我 decode 出来的 PixelMap 是 null?
A:检查 ImageSource 创建是否成功。常见原因是文件路径错误或图片格式不支持。可以通过 imageSource.getImageInfo() 验证是否成功解析图片头。

Q:Image Kit 支持网络图片直接解码吗?
A:不支持。需要先下载网络图片到本地(使用 @ohos.net.http),拿到 ArrayBuffer 后再传给 createImageSource

Q:同时解码多张图片时,如何控制并发?
A:不建议在短时间内大量调用 createPixelMap,每个 PixelMap 会占用独立 native 内存。推荐使用线程池或队列逐个处理,并控制同时活跃的 PixelMap 数量不超过 3~5 个。可以利用 taskpool 并行解码,但需要注意 PixelMap 的跨线程传递是有限制的(必须序列化),实际上建议还是主线程逐个进行。

Logo

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

更多推荐