一张低分辨率图片经过图像增强后,页面要从原图切到增强图。交互看似只是给 Image 换一个数据源,真正难处理的却是旧 PixelMap 的归属。增强结果尚在解码、用户已经切换图片,或者页面关闭时还留着一次异步操作,三种情况叠在一起,轻则出现旧图覆盖新图,重则把仍在使用的原生图像对象提前释放。

本文采用 SuperSwap 作为演示项目,只实现“已产生的增强结果如何安全预览”,不实现超分模型推理,也不把缩放当成超分辨率。示例任务 SRS-208 的原图尺寸记作 640×360,增强结果素材记作 1600×900,结果修订号为 12。这些值都是为了说明资源流转而预设的样例数据,并非实测推理结果。

一、先划清推理和预览两层的责任

图像超分处理可能来自单独的模型、服务或 SDK,而 Image Kit 的 ImageSource、PixelMap 负责的是图像解码、像素表示与后续显示。工程里可以把推理完成后的文件路径交给预览模块,预览模块不需要知道模型内部的张量、倍率或加速方式。这样处理有一个好处:模型替换了,只要输出是一种受支持的图片格式,预览代码不必整体重写。

演示屏幕把“源文件已选中”“增强结果文件可读取”“新的 PixelMap 已交给页面”“旧对象可以释放”拆成四个不同状态。不能因为输出文件出现,就认定页面已经使用它;更不能因为新位图解码结束,就推断旧位图已完全离开绘制树。

对 SRS-208,我们定义展示终态 PREVIEW_READY,页面显示 原图 640×360、增强图 1600×900、rev=12,以及 旧图待释放 1。最后这个数字是资源管理队列的长度,而不是设备内存监测工具的数据。演示允许暂时多持有一个旧对象,用空间换取所有权的确定性;预算不足时则应减少并行预览,而不是盲目释放正在绘制的资源。

二、ImageSource 只能负责解码期,不能被拿来做缓存

有一种常见写法是先创建 ImageSource,立即发起异步 createPixelMap(),接着就 release()。这不是一次完整的解码事务。华为 Image Kit 的官方常见错误说明指出,同一图像源的异步操作与源对象释放发生竞争可能导致崩溃;因此先 await,再释放图像源,应该成为固定边界。

下面这段代码要解决的是从已有沙箱图片文件得到独立 PixelMap,并保证成功或失败都会释放解码源。路径由业务层提供,示例不包括相册授权,也不负责验证图片是否来自可信模型。

import { image } from '@kit.ImageKit';

export async function decodeForPreview(path: string): Promise<image.PixelMap> {
  const source: image.ImageSource = image.createImageSource(path);
  let map: image.PixelMap | undefined;
  try {
    map = await source.createPixelMap({ editable: false });
    const info: image.ImageInfo = await map.getImageInfo();
    if (info.size.width <= 0 || info.size.height <= 0) {
      throw new Error('EMPTY_IMAGE');
    }
    return map;
  } catch (error) {
    map?.release();
    throw error;
  } finally {
    source.release();
  }
}

source 与 map 的所有权分离:前者只覆盖解码过程,后者作为结果交给更长寿命的预览槽。editable:false 表示当前展示不修改像素;如果后续要裁剪或编码,必须重新明确可编辑性和并发关系。getImageInfo() 取得的是位图真实尺寸,不应该用 UI 上显示的“1600×900”字符串替代接口返回值的校验。异常分支负责销毁已经创建、却不合格的位图;至于路径不存在、文件损坏、解码格式不支持,则由调用方把异常映射成友好的错误状态。

三、替换预览前先做修订号门禁

图片增强通常耗时较长。用户先打开 A 图,又切到 B 图,A 图的解码最后完成,这时直接赋值便会让 B 页面闪回 A。这个问题和单纯的内存回收不同:即便每一个对象都及时释放,错误的提交顺序仍会出现。

SuperSwap 为每次预览请求生成递增修订号。页面示例选中 SRS-208 的 rev=12,旧请求只允许释放自己的位图,不能提交状态。下面的类是预览逻辑的独立片段,publish() 由页面封装为状态更新;它不包含任何超分推理接口。

import { image } from '@kit.ImageKit';

export class PreviewSlot {
  private revision: number = 0;
  private active?: image.PixelMap;
  private retired: image.PixelMap[] = [];
  private closed: boolean = false;

  async replace(path: string, publish: (next: image.PixelMap) => void): Promise<void> {
    const ticket: number = ++this.revision;
    let next: image.PixelMap = await decodeForPreview(path);
    if (this.closed || ticket !== this.revision) {
      next.release();
      return;
    }
    const previous: image.PixelMap | undefined = this.active;
    this.active = next;
    if (previous) this.retired.push(previous);
    publish(next);
  }

  invalidate(): void { this.revision++; }
}

在这段实现里,retired 不是泄漏容器,而是有意保留的旧帧所有权队列。只要旧 Image 组件可能仍引用它,就不能按“新对象已赋值”简单等价于“旧对象可释放”。代码只处理成功路径的提交逻辑,生产中还需在外层 try/catch 转换错误,并在不同会话切换时确保 closed 标志有效。revision 是业务修订号,不是 SDK 自带的请求 ID;没有任何证据证明第 12 次请求一定比第 11 次更快。

四、把资源状态和用户能看到的状态分开

演示页面的状态面板同时展示三组信息:文件维度的原图尺寸与增强图尺寸、调度维度的 rev=12、渲染维度的 PREVIEW_READY。这些字段要分开写入日志。否则日志只有一行“加载成功”,排查时无法知道究竟是模型已输出、文件已解码,还是界面真正接受了新位图。

下面的 DevEco 白色主题配图只表现演示结构:左侧是 SuperSwap 工程目录,中间是 PreviewSlot.ets 的修订号检查,右侧是 PreviewPage,底部 HiLog 使用预设的三行状态样例。图片不是 DevEco 真实编译或设备性能证据。

当页面显示 PREVIEW_READY,并不意味着推理耗时已经测出,也不意味着旧位图已经安全释放。我们只允许用户在状态面板里确认这次显示意图被接受,同时把“旧图待释放 1”保持可见。把资源队列长度展示出来,是提醒开发者:UI 切换有自己的时序,不能用一次 setState 抹去原生对象生命周期问题。

五、释放要在视图解绑之后,而不是回调刚触发时

官方 Image Kit 常见问题还提到,在异步编码期间修改或释放 PixelMap 可能引起原生层崩溃。预览端也是同一个原则:释放前要先确认所有使用者都停止使用它。对示例而言,Image 的数据绑定、后续可能启动的编码和用户切页,都是位图使用者。

下面的收尾方法只允许在页面已将位图从视图解绑、相关异步调用已结束之后调用。注意这段方法本身不提供“渲染帧已完成”的系统通知;业务不能仅凭 aboutToDisappear() 触发就无条件认为前提成立。

import { image } from '@kit.ImageKit';

// PreviewSlot 的补充方法:须由确认解绑的业务时机触发。
releaseAfterDetach(): void {
  this.closed = true;
  this.invalidate();
  this.active?.release();
  this.active = undefined;
  this.retired.forEach((oldMap: image.PixelMap) => oldMap.release());
  this.retired = [];
}

这段代码是类内方法示意,应放回上一节的 PreviewSlot 类体中,而不是作为文件顶层函数。业务首先把页面 Image 的 PixelMap 引用清空,确保后续操作不会再使用这些对象;如果仍有正在执行的 packToFile 或图片编辑任务,需要等待它们结束。由于 replace() 本身可能尚在 await 解码,真正的工程版本还应建立待完成任务计数或显式关闭协议,不能仅依赖 revision 做原生对象完成态判断。

六、演示验收不等于性能结论

手机图给出 SRS-208 的一个可视化终态:原图 640×360、增强图 1600×900、rev=12、PREVIEW_READY,旧图队列长度为 1。这是预设输入与状态演示,并不表示示例已经在某款设备上完成真实超分推理。若要用于性能文章,应另附真实设备型号、构建版本、样本张数、解码耗时、内存峰值,以及图像增强算法的实际指标。

真正值得验收的是几条可以重复执行的边界路径:连续选择两个结果文件时,旧回包不会反向覆盖新图;解码失败时,旧图仍然可用;页面关闭后,后续异步结果只进入释放流程,不再写页面状态;图像编码或缩放任务持有资源时,不提前释放 PixelMap;资源解绑后,活动对象和旧帧队列都有明确出口。

如果目标设备内存较小,还要给解码结果设置尺寸上限,并根据真实像素格式估计内存预算。不能把磁盘文件的几百 KB 当成解码后原生内存的大小。多缓冲是一种防竞态策略,不是允许无限保存位图的借口。对实际产品,我更愿意牺牲一次切图动画的顺滑度,也不愿在图像增强与渲染都异步的地方留下不受控的释放时机。

资料核对:华为开发者文档《Image Kit常见崩溃报错问题》、 《使用PixelMap完成图像变换》。本文只采用有官方文档依据的 ImageSource、createPixelMap、getImageInfo 和 release 等能力。所有截图、任务号与尺寸均为教学演示值,未进行实机编译、推理或性能检测。

  • 官方资料:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/image-common-mistakes
  • 官方资料:https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/image-transformation
Logo

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

更多推荐