标签: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,无论后续 createImageSource、createPixelMap 还是 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;
}
这种边界有三个收益:
- 颜色算法可以用人工缓冲区单独测试。
- PixelMap 的释放不受页面生命周期影响。
- 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_FAILED、PIXELMAP_DECODE_FAILED、PIXEL_READ_FAILED 和 CELL_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。
连续转换要观察趋势而不是单次峰值
资源回归建议固定执行:
- 连续选择并转换 10 张不同图片。
- 在第 3、6、9 次穿插一次取消选择。
- 加入一张无法解码或内容异常的文件。
- 每次完成后立即打开编号图,再返回创建页。
- 记录成功次数、失败阶段、耗时趋势与内存趋势。
单次转换出现峰值并不自动表示泄漏。真正值得关注的是完成并释放后,基线是否随着次数持续抬高,以及失败路径后下一次转换是否明显更慢。
当前文章没有用未经执行的数字宣称内存改善。实际结论应来自目标设备或模拟环境中的连续操作记录,并区分系统缓存、瞬时峰值和无法回落的增长。
用故障注入覆盖四个释放阶段
建议建立下表:
| 故障点 | 已创建对象 | 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,不持有原生图像对象。只有把这种所有权写清,连续选图、取消、失败重试和多次生成才不会把上一次资源带进下一次操作。