Harmony os 技术实战|拼豆制图20:用 finally 关闭 ImageSource、PixelMap 与文件句柄

标签:Harmony os、ArkTS、ImageKit、PixelMap、资源释放

图片转换成功一次,并不能证明资源管理正确。真正容易暴露问题的是连续操作:用户从系统图库依次选择多张高分辨率照片,前几次生成正常,之后速度下降、内存峰值持续抬高,甚至在某次解码时退出。页面看见的只是一条"图片解析失败",根因却可能是上一次 PixelMap 或文件句柄仍未释放。

拼豆制图的图像入口同时处理三类需要明确收口的对象:ImageSource 负责解码,PixelMap 保存目标尺寸像素,URI 直读失败时还会打开一个只读文件句柄作为回退。任何一步都可能抛错,因此释放不能只写在成功路径末尾。

当前服务用一个 try/finally 包围"创建图源 → 解码 70×70 → 读取 RGBA 缓冲区 → 生成普通格子",并按依赖逆序释放 PixelMap、ImageSource、File。本文把这条资源作用域写完整。

本文重点解决:

  • 系统选择器 URI 为什么优先直接交给 createImageSource
  • 何时回退到 fileIo.openSync 与文件描述符解码。
  • 如何在解码阶段把原图约束到 70×70、RGBA_8888。
  • 为什么缓冲区必须在资源释放前完整读取。
  • finally 如何覆盖创建失败、解码失败、读取失败和算法失败。
  • 为什么释放顺序应与创建依赖相反。
  • 如何保证页面最终只收到普通 Pattern,不长期持有系统图像对象。

先画出对象的创建与依赖顺序

一次图片转换的资源关系为:

text 复制代码
URI
 ├─ 直接创建 ImageSource
 └─ 失败时打开 File(fd) → 创建 ImageSource
                         ↓
                    创建 PixelMap
                         ↓
                    读取 ArrayBuffer
                         ↓
                    生成 BeadCell[]

三个系统资源有不同所有者:

对象 创建位置 使用结束点 释放方式
fileIo.File URI 直读失败后的回退分支 ImageSource 不再需要 fd fileIo.closeSync(fd)
image.ImageSource URI 或 fd PixelMap 与读取流程结束 release()
image.PixelMap 目标尺寸解码 像素已读入 ArrayBuffer release()

ArrayBuffer 和 BeadCell[] 是普通数据,不需要跟随上述系统对象长期存在。资源边界的目标就是尽快把系统对象转换成普通业务数据,然后立即释放原生资源。

URI 优先直接交给 ImageSource

系统选择器返回的 URI 不一定等同于普通磁盘路径。当前服务先尝试直接创建图源:

typescript 复制代码
let imageSource: image.ImageSource | undefined = undefined;

try {
  imageSource = image.createImageSource(uri);
} catch (error) {
  imageSource = undefined;
}

这一步不对 URI 做字符串裁剪,也不擅自移除协议前缀。ImageKit 负责解释它支持的输入形式,业务层只保留原始选择结果。

局部 catch 的职责非常窄:直读失败不立即结束整个转换,而是允许进入文件描述符回退。它不在这里拼接最终用户文案,也不把失败伪装成成功。

文件描述符只作为明确回退分支

当 URI 直读没有得到 ImageSource,服务尝试只读打开:

typescript 复制代码
let imageFile: fileIo.File | undefined = undefined;

if (imageSource === undefined) {
  imageFile = fileIo.openSync(
    uri,
    fileIo.OpenMode.READ_ONLY
  );
  imageSource = image.createImageSource(imageFile.fd);
}

这里创建了新的资源责任:只要 openSync 成功,最终就必须关闭 fd,无论后续 createImageSourcecreatePixelMap 还是 readPixelsToBuffer 在哪里失败。

文件对象不能被塞进 Pattern 或页面状态。它只服务于本次解码作用域,离开方法后不再有合法用途。

三个变量都从 undefined 开始记录所有权

完整方法在进入 try 前声明:

typescript 复制代码
let imageSource: image.ImageSource | undefined = undefined;
let pixelMap: image.PixelMap | undefined = undefined;
let imageFile: fileIo.File | undefined = undefined;

这不是多余的防御代码。它让 finally 能判断每个对象是否真正创建成功:

  • URI 直读失败时,imageSource 仍为 undefined。
  • openSync 抛错时,imageFile 仍可能没有值。
  • createPixelMap 失败时,pixelMap 不存在,但 ImageSource 仍需释放。

若变量只在成功分支的局部块内声明,finally 无法访问它们;若假设三个对象一定存在,清理代码本身又会在失败路径抛错。

解码时把像素规模限制在 70×70

图纸目标固定为 70×70,因此没有必要让算法长期持有原图分辨率的 PixelMap:

typescript 复制代码
pixelMap = await imageSource.createPixelMap({
  desiredSize: {
    width: size,
    height: size
  },
  desiredPixelFormat: image.PixelMapFormat.RGBA_8888
});

desiredSize 把缩放放在解码边界,desiredPixelFormat 让后续通道解释明确。理论上的目标缓冲区为:

text 复制代码
70 × 70 × 4 = 19600 bytes

这不等于原始图片在解码过程中没有任何内存成本,但它保证交给纯算法的 PixelMap 与 ArrayBuffer按施工目标受控,而不是随相册照片尺寸无限增长。

创建结果仍要做空值保护

当前代码在解码后保留保护分支:

typescript 复制代码
if (pixelMap === undefined) {
  throw new Error('无法解码图片像素');
}

同样,回退后仍确认图源存在:

typescript 复制代码
if (imageSource === undefined) {
  throw new Error('无法创建图片源');
}

这些错误发生在服务边界,页面不应该继续使用上一次图纸冒充本次生成结果。外层转换函数会把失败包装为"图片解析失败",而已有图纸与选中状态由页面状态机决定是否保留。

资源释放与业务恢复是两件事:finally 确保系统对象收口,页面的错误分支确保用户可以重试,二者缺一不可。

缓冲区长度从 PixelMap 读取

PixelMap 创建成功后,服务获取真实字节数:

typescript 复制代码
const pixelBytes = pixelMap.getPixelBytesNumber();
const buffer = new ArrayBuffer(pixelBytes);
await pixelMap.readPixelsToBuffer(buffer);

不要只用 size * size * 4 猜测分配长度,再假设系统对象完全符合预期。getPixelBytesNumber() 是当前 PixelMap 对自身数据量的说明。

进入纯函数前仍可校验:

typescript 复制代码
const requiredBytes = size * size * 4;
if (buffer.byteLength < requiredBytes) {
  throw new Error('RGBA 像素缓冲区长度不足');
}

读取完成后,ArrayBuffer 已经拥有算法所需字节,PixelMap 不需要再被页面或 Pattern 持有。

纯算法只接收 ArrayBuffer

资源层与算法层的交接点为:

typescript 复制代码
return ImageConvertService.createCellsFromBuffer(
  buffer,
  size,
  size
);

后续函数只操作普通字节:

typescript 复制代码
private static createCellsFromBuffer(
  buffer: ArrayBuffer,
  width: number,
  height: number
): BeadCell[] {
  const pixels = new Uint8Array(buffer);
  const result: BeadCell[] = [];
  // RGBA → 空格或实体色号
  return result;
}

这种边界有三个收益:

  1. 颜色算法可以用人工缓冲区单独测试。
  2. PixelMap 的释放不受页面生命周期影响。
  3. Pattern 只包含普通数组、字符串和数字,便于缓存与导出。

不要为了在详情页显示原图而把 PixelMap 加进 Pattern。若确实需要原图预览,应建立独立、可释放的媒体展示流程。

finally 覆盖所有中途失败位置

当前作用域结构为:

typescript 复制代码
try {
  // 1. 创建 ImageSource
  // 2. 必要时打开 File
  // 3. 创建 PixelMap
  // 4. 读取 ArrayBuffer
  // 5. 生成 BeadCell[]
  return cells;
} finally {
  if (pixelMap !== undefined) {
    await pixelMap.release();
  }
  if (imageSource !== undefined) {
    await imageSource.release();
  }
  if (imageFile !== undefined) {
    fileIo.closeSync(imageFile.fd);
  }
}

无论 return cells 正常执行,还是任一步抛错,finally 都会运行。它覆盖的不只是系统 API 失败,还包括 createCellsFromBuffer 内的算法异常。

把释放代码复制到多个 catch 分支,很容易新增一个失败出口却忘记同步;统一 finally 才能让资源作用域闭合。

释放顺序与创建依赖相反

创建顺序通常是:

text 复制代码
File → ImageSource → PixelMap

释放顺序则是:

text 复制代码
PixelMap → ImageSource → File

PixelMap 由 ImageSource 解码得到,ImageSource 在回退路径中又依赖文件描述符。先释放最下游对象,再关闭它的上游来源,能避免仍在使用的对象失去依赖。

这与嵌套作用域的退出顺序一致:最后创建的资源最先离开。即使某条路径没有 File,undefined 判断也会自然跳过。

释放调用需要 await 的对象应等待完成后再进入下一步,不能把 promise 留到函数已经返回后再由未知上下文处理。

回退分支失败也必须关闭已经打开的 File

最容易漏掉的场景是:

text 复制代码
URI 直读失败
→ openSync 成功
→ createImageSource(fd) 抛错

此时 PixelMap 与 ImageSource 都没有成功,File 却已经打开。因为 imageFile 在外层变量中保存,finally 仍能关闭:

typescript 复制代码
if (imageFile !== undefined) {
  fileIo.closeSync(imageFile.fd);
}

若把 closeSync 只写在 PixelMap 读取成功之后,这条失败路径会泄漏句柄。资源校验必须逐个阶段制造异常,而不是只反复跑成功图片。

同理,PixelMap 创建成功、缓冲区读取失败时,PixelMap 与 ImageSource 仍会按顺序释放。

错误包装不能吞掉真正阶段

外层转换函数目前统一补充业务上下文:

typescript 复制代码
static async convertFromPickedImage(
  uri: string,
  source: Pattern
): Promise<ImageConvertResult> {
  try {
    const chartCells = await createCellsFromImage(uri, 70);
    // 生成预览、统计与 Pattern
    return result;
  } catch (error) {
    const err = error as Error;
    throw new Error(`图片解析失败:${err.message}`);
  }
}

页面得到的是可理解的业务阶段,内部错误仍保留在 message 中。更细化时可以把失败分成 SOURCE_OPEN_FAILEDPIXELMAP_DECODE_FAILEDPIXEL_READ_FAILEDCELL_BUILD_FAILED,但不要通过字符串包含关系反推类型。

无论如何分类,资源释放必须在内部 finally 先完成,再把错误交给上层。页面只负责状态与重试动作,不应该尝试释放 ImageSource。

Pattern 不保存 URI、PixelMap 或文件对象

成功结果只包含普通业务字段:

typescript 复制代码
return {
  pattern: {
    id: `user-generated-${Date.now()}`,
    title: '我的图片图纸',
    width: 70,
    height: 70,
    previewCells,
    chartCells,
    colorStats,
    beadCount,
    colorCount
  },
  usedFallback: false,
  message: '已根据图片生成 70 x 70 编号图纸'
};

Pattern 可以在页面、收藏和生成记录之间传递,不会延长系统图像对象的生命周期。URI 只在用户重新选择或展示来源时才需要,当前图纸施工本身不依赖它。

纯业务模型也让失败恢复更容易:本次解码失败时,页面可以保留上一张 Pattern,因为它不依赖已经关闭的 PixelMap。

连续转换要观察趋势而不是单次峰值

资源回归建议固定执行:

  1. 连续选择并转换 10 张不同图片。
  2. 在第 3、6、9 次穿插一次取消选择。
  3. 加入一张无法解码或内容异常的文件。
  4. 每次完成后立即打开编号图,再返回创建页。
  5. 记录成功次数、失败阶段、耗时趋势与内存趋势。

单次转换出现峰值并不自动表示泄漏。真正值得关注的是完成并释放后,基线是否随着次数持续抬高,以及失败路径后下一次转换是否明显更慢。

当前文章没有用未经执行的数字宣称内存改善。实际结论应来自目标设备或模拟环境中的连续操作记录,并区分系统缓存、瞬时峰值和无法回落的增长。

用故障注入覆盖四个释放阶段

建议建立下表:

故障点 已创建对象 finally 应执行
URI 直读失败、回退打开也失败 无或部分 File 关闭已成功的 File
fd 创建 ImageSource 失败 File 关闭 File
createPixelMap 失败 ImageSource、可选 File 释放 ImageSource,关闭 File
readPixelsToBuffer 失败 PixelMap、ImageSource、可选 File 三者逆序释放
格子算法失败 PixelMap、ImageSource、可选 File 三者逆序释放
全部成功 三者按路径存在 返回前全部释放

纯构建无法证明这些运行时路径。可以通过替换测试输入、注入可控错误或封装资源工厂来观察释放计数;至少要保证下一次转换仍能立即执行。

ArkTS 层的目标不是捕获所有底层内存细节,而是让每个创建动作都有唯一、可读的关闭位置。

常见资源故障沿所有权定位

现象 先看哪里 常见根因 修复方向
第二次转换明显更慢 上一次 finally PixelMap 或 ImageSource 未释放 统一闭合资源作用域
URI 能选中却无法打开 直读与 fd 回退 把 URI 当普通路径裁剪 原值直读,失败再回退
多次失败后文件打不开 File 关闭路径 fd 创建图源失败时漏关 外层保存 File 并 finally 关闭
PixelMap 读取后仍被页面持有 Pattern 字段 系统对象跨层传递 只返回 ArrayBuffer 派生数据
缓冲区尾部异常 字节数来源 手工分配长度不匹配 使用 getPixelBytesNumber
大图转换峰值过高 解码目标尺寸 先完整解码再缩放 在 createPixelMap 指定 70×70
失败日志只写"生成失败" 错误边界 阶段被过度吞并 保留 source/decode/read/build 上下文
释放后仍访问对象 调用层职责 页面保存了 imageSource 引用 系统对象限制在服务方法内

排查时为每个对象回答三个问题:在哪里创建、谁拥有、在哪个 finally 关闭。无法回答其中一个,就说明生命周期边界仍然模糊。

小结

稳定的图片转换不是"成功后调用一次 release",而是让所有路径都处于同一个资源作用域。拼豆制图先用 URI 创建 ImageSource,必要时打开 File 并通过 fd 回退;随后按 70×70、RGBA_8888 解码 PixelMap,把像素读入 ArrayBuffer,再交给纯函数生成 4900 个普通格子。

finally 无论成功、解码失败、读取失败还是算法失败都会执行,并按 PixelMap → ImageSource → File 的逆序释放。页面最终只收到 Pattern,不持有原生图像对象。只有把这种所有权写清,连续选图、取消、失败重试和多次生成才不会把上一次资源带进下一次操作。

相关推荐
独守一片天13 小时前
HarmonyOS 鸿蒙碰一碰与跨端协同体验怎么设计?
华为·harmonyos
yuhulkjv33516 小时前
Gemini鸿蒙版导出word格式的终极解法:AI 导出鸭如何重构AI内容落地链路
人工智能·ai·word·harmonyos·ai导出鸭
whyutianict_vv18 小时前
从 Web 前端到 HarmonyOS ArkTS:一次 AI 鸿蒙全栈智能体开发的迁移实录
前端·人工智能·harmonyos
m0_7496902319 小时前
【寻迹校园 HarmonyOS NEXT 实战 01】从校园痛点到可上架 MVP:失物招领应用产品设计
人工智能·深度学习·移动开发·harmonyos·arkts·arkui·产品设计
m0_7496902319 小时前
【寻迹校园 HarmonyOS NEXT 实战 03】NavPathStack 路由实战:18 个页面如何集中管理
华为·移动开发·harmonyos·arkui·navigation·navpathstack
m0_7496902320 小时前
【寻迹校园 HarmonyOS NEXT 实战 02】多模块工程拆解:entry、HAR 与 HSP 如何分工
华为·harmonyos·arkts·软件架构·har·hsp
用户09340777351421 小时前
HarmonyOS WPS Open SDK 实践:用 SdkConstants 判断当前 HAR 形态
harmonyos
独守一片天1 天前
HarmonyOS鸿蒙新生态智能体意图框架怎么设计?
华为·harmonyos
梦想不只是梦与想1 天前
鸿蒙应用api的兼容性:参数配置(二)
harmonyos·sdk版本·sdkversion