HarmonyOS 7 ImageSRAnalyzer:超分导出临时编码与失败回滚【鸿蒙心迹】
超分页面显示“处理完成”,不等于图片已经可交付。ImageSRAnalyzer.process() 返回的 PixelMap 只是一份内存结果;后面还有编码、文件同步、最终名称替换和资源释放。如果在编码进行到 68% 时沙箱空间耗尽,直接写目标文件会留下一张名字正确、内容却不完整的 JPEG。
这篇文章用 SharpExportLab 复盘一条“先编码到临时文件,验证后再提交”的导出链。页面是 ExportDesk,协调器是 SrExportCoordinator。任务 SR-1712-412 在 17:12 进入编码,代次 412,JPEG 质量 95,临时文件 sr_412.jpg.part,目标文件 sr_412.jpg。演示停在 68%,状态是 ENCODING_TEMP,还不能标记导出成功。

一、“超分成功”和“文件可交付”中间还有一条窄桥
HarmonyOS 7 的图像超分能力从 API 26.0.0 开始提供,ImageSRAnalyzer.create() 创建分析器,process() 返回包含 PixelMap 的响应,destroy() 用于销毁分析器。这一段解决的是画质重建,并不替应用决定结果文件什么时候对外可见。
ImagePacker.packToFile() 接收 PixelMap、文件描述符与 PackingOption。官方的图像常见错误说明特别强调:异步编码期间不要修改 PixelMap,要 await 编码完成后再释放 PixelMap 和 ImagePacker。如果只是启动 Promise 就进入 finally,编码器仍在读取像素,业务层却已经把底层资源释放,结果不只是失败,还可能触发崩溃。
文件可交付至少要通过四道门:超分处理成功;临时文件编码成功;文件长度和解码预检通过;临时名称成功移到最终名称。页面上的进度条可以展示前三段的估算,但只有第四道门结束后,按钮才能变成“分享”。
Demo 把状态分成 PREPARING、SR_RUNNING、SR_READY、ENCODING_TEMP、VERIFYING_TEMP、COMMITTING、COMMITTED、ROLLING_BACK和 FAILED。这些名称都是应用状态,不冒充系统枚举。界面上 68% 对应 ENCODING_TEMP,不是框架回调给出的真实字节进度。
二、超分与导出用同一代次串起来
第一段代码解决一个很常见的竞态:用户选择 A 图后启动超分,随后又切到 B 图,A 的迟到结果不能覆盖 B 的页面。协调器为每次任务分配代次,并在 process() 返回后再次核对。
import { imageSuperResolution, visionBase } from '@kit.CoreVisionKit';
import { image } from '@kit.ImageKit';
private generation: number = 411;
private analyzer?: imageSuperResolution.ImageSRAnalyzer;
private output?: image.PixelMap;
state: string = 'IDLE';
async runSr(input: image.PixelMap): Promise<void> {
const current = ++this.generation; // 本次为 412
this.state = 'SR_RUNNING';
const analyzer = await imageSuperResolution.ImageSRAnalyzer.create();
this.analyzer = analyzer;
try {
const request: visionBase.Request = { inputData: input };
const response = await analyzer.process(request);
if (current !== this.generation) {
response.pixelMap.release();
return;
}
this.output?.release();
this.output = response.pixelMap;
this.state = 'SR_READY';
} catch (error) {
this.state = 'FAILED';
throw error;
}
}
visionBase.Request 的字段要以当前 SDK 声明为准,示例只保留图像输入的必要结构。代次失效时要立即释放迟到 PixelMap,否则虽然 UI 没有采用它,原生图像内存仍然被占用。
这段代码没有在每次 runSr() 的 finally 里立即销毁分析器,是因为 Demo 允许用户预览结果后再导出。真正的页面离开或任务终止会调用统一 dispose(),释放 output,然后 await analyzer.destroy()。顺序必须与所有在飞行 Promise 协调,不能一退页就与 process() 抢销毁。
三、临时文件先承受失败,最终名称只接受通过品
第二段代码解决编码中途失败污染最终文件的问题。它不直接打开 sr_412.jpg,而是创建 sr_412.jpg.part。只有 packToFile() 返回后才关闭文件,进入 VERIFYING_TEMP。
import fs from '@ohos.file.fs';
import { image } from '@kit.ImageKit';
async encodeTemp(pixelMap: image.PixelMap, dir: string): Promise<string> {
const tempPath = `${dir}/sr_412.jpg.part`;
if (fs.accessSync(tempPath)) {
fs.unlinkSync(tempPath);
}
const file = fs.openSync(tempPath,
fs.OpenMode.CREATE | fs.OpenMode.READ_WRITE);
let packer: image.ImagePacker | undefined;
this.state = 'ENCODING_TEMP';
try {
packer = image.createImagePacker();
const options: image.PackingOption = {
format: 'image/jpeg',
quality: 95
};
await packer.packToFile(pixelMap, file.fd, options);
fs.fsyncSync(file.fd);
this.state = 'VERIFYING_TEMP';
return tempPath;
} catch (error) {
this.state = 'ROLLING_BACK';
throw error;
} finally {
fs.closeSync(file);
packer?.release();
}
}
await packToFile() 是这段代码的生命线。文件与 ImagePacker 都在它结束后才释放,而 PixelMap 此时仍由协调器持有,等验证和提交完成再统一收口。fs.fsyncSync() 把已写入的文件数据同步到存储介质,它不替代后续的解码验证,也不能把异常流程变成成功。
示例用 accessSync() 处理上一次遗留的同名 .part。真正项目还要根据业务决定是删除还是保留取证;不应在不知道文件归属的情况下清理整个目录。如果编码失败,异常往上抛,外层只删除本代次的临时文件。
工程目录中,service/SrExportCoordinator.ets 编排代次,service/TempImageCommitter.ets 负责临时文件,pages/ExportDesk.ets 只消费快照,model/ExportSnapshot.ets 保存任务号、状态、进度和路径。下面的 DevEco Studio 风格图按本文数据生成,它是说明图,不是真实 IDE 或设备测试证据。

四、验证通过之前,不给临时文件改正式名
第三段代码解决“编码 Promise 成功,但文件仍不值得信任”的问题。示例先读取文件状态,再用 ImageSource 解码一次并取得图像信息。只有长度大于零、解码成功、代次仍然有效,才用 fs.moveFile() 移到最终路径。
async verifyAndCommit(tempPath: string, finalPath: string,
boundGeneration: number): Promise<void> {
const stat = fs.statSync(tempPath);
if (stat.size <= 0 || boundGeneration !== this.generation) {
throw new Error('TEMP_FILE_REJECTED');
}
const source = image.createImageSource(tempPath);
try {
const info = await source.getImageInfo();
if (info.size.width <= 0 || info.size.height <= 0) {
throw new Error('DECODE_PROBE_FAILED');
}
this.state = 'COMMITTING';
await fs.moveFile(tempPath, finalPath, 0);
if (boundGeneration !== this.generation) {
throw new Error('LATE_COMMIT');
}
this.state = 'COMMITTED';
} finally {
source.release();
}
}
moveFile 的 mode=0 允许覆盖目标同名文件。这不是一条无条件的推荐:如果用户需要保留历史版本,就应该使用新文件名或把冲突作为交互选择。Demo 覆盖的是同一任务的重试产物,不是用户的任意图片。
提交后的代次再检查只是诊断栅栏,不能倒流时间。如果业务允许用户在提交中切换任务,更稳妥的做法是让所有提交操作进入串行队列,进入 COMMITTING 后暂停更换输入。不能指望于文件已经移动后再用一个 if 恢复旧世界。
手机运行图显示任务 SR-1712-412、代次 412、编码质量 95、临时文件名和 68% 进度。红色标注专门圈出 ENCODING_TEMP,提醒这一刻“目标文件尚未可见”。

五、回滚的目标是恢复可解释状态,不是把日志擦干净
异常可以发生在三个区域。超分失败时没有输出 PixelMap,只需销毁分析器并恢复选图入口。编码失败时已经有 .part,要先关闭描述符和编码器,再删除本代次临时文件。提交失败时则要区分临时文件仍在、最终文件已经替换,还是路径本身不可用。
Demo 给每个阶段写一条事件账本:SR_READY、TEMP_OPENED、ENCODE_PROGRESS、TEMP_SYNCED、DECODE_PROBED、COMMIT_STARTED、COMMIT_DONE、ROLLBACK_DONE。事件带任务号、代次和文件名,但不记录用户原图路径之外的敏感数据。
失败后不应该把进度归零就算结束。如果页面只显示 0%,用户和测试者看不出是未开始还是已回滚。本文保留失败瞬间的 68%,状态转为 ROLLED_BACK,同时明确标出临时文件已删除、最终文件未变更。
详情图与运行图明显不同。它展示一次沙箱空间不足的故障注入:17:12:42 编码到 68%,17:12:43 返回 NO_SPACE,关闭资源,删除 sr_412.jpg.part,最终 sr_412.jpg 没有被替换。这是演示账本,不声称是真机实测。

六、测试不要只看最后有没有一张图
第一组测试注入正常的 PixelMap,检查状态必须经过 SR_READY -> ENCODING_TEMP -> VERIFYING_TEMP -> COMMITTING -> COMMITTED,不允许跳过验证。第二组在 packToFile() 期间注入空间不足,期望 .part 被删除,原来的正式文件保持哈希不变。
第三组构造长度大于零但无法解码的临时文件,证明只查 stat.size 不足以放行。第四组在验证期间增加代次,迟到任务应该被拒绝。第五组反复进入页面,记录 PixelMap、ImagePacker、ImageSource、文件描述符和分析器是否全部成对收口。
还要把“页面销毁”与“应用任务取消”分开。如果产品允许导出离开页面后继续,协调器就不能挂在页面实例上;如果不允许,离开页面要终止新建阶段,等待正在运行的编码返回后再释放。无论哪种,都不应该在运行中强行释放底层图像对象。
存储空间也应该在创建临时文件前做预判,但预判不能取代错误处理。JPEG 体积与图像内容、质量、尺寸和编码器实现有关,业务层只能给出保守预算,不能根据一张样本的压缩率认定必然够用。而且在检查和实际写入之间,其他任务仍可能消耗空间。所以“预判可用”只允许进入编码,packToFile() 的真实返回仍是最终事实。
应用冷启动时还要扫描未完成的 .part,但不要将“出现临时文件”直接解释为崩溃。进程被系统终止、用户强制停止、磁盘写入失败都可能留下同样痕迹。恢复器根据文件名中的任务与代次查询事件账本:已经 COMMITTED 却留下临时文件时可删除;停在 ENCODING_TEMP 时标记异常中断;根本找不到任务记录时转人工复核。
对外分享又是下一条独立生命周期。COMMITTED 只代表应用沙箱中的文件通过本文门禁,不代表相册写入、跨设备传输或网络上传已经成功。这些流程应持有自己的任务号、权限检查和重试记录,不反向修改导出任务的真实结果。
七、导出成功是一条证据,不是一句 Toast
超分带来的新图像只是导出链的输入。把临时文件作为失败隔离区,把解码预检作为提交门,把最终移动作为唯一可见点,页面才能准确回答“文件到底好没好”。
SR-1712-412 在 68% 失败时,正式文件不受影响,临时文件有明确的回滚记录,图像对象和编码器按异步边界释放。这比“导出失败,请重试”多了一些状态,但也少了一张无法解释的坏图。
对使用者而言,这套设计的价值不在于看到更多状态名,而在于失败后仍能明确知道原图、上一版导出和本次临时产物各自处于什么位置,不需要靠重复点击来猜测结果。
参考资料:
更多推荐





所有评论(0)