HarmonyOS 7 ImageSRAnalyzer:超分结果租约与销毁栅栏
图像超分的界面很容易写成三个动作:选图、处理、展示。真正需要反复判断的却是第四个动作——上一张结果什么时候可以释放。用户连续选择两张图时,第一个 process() 可能后返回;页面切走时,分析器可能仍有任务;新 PixelMap 替换旧 PixelMap 时,如果释放顺序写反,Image 组件可能读到已经无效的资源。
HarmonyOS 7 / API 26 的 Core Vision Kit 新增 imageSuperResolution 命名空间,当前接口声明包含 ImageSRAnalyzer.create()、process(request)、返回对象中的 pixelMap,以及 destroy()。这套 API 给出了创建、处理和销毁边界,但业务仍要回答谁拥有返回的 PixelMap、迟到结果由谁回收、销毁时如何等待在途任务。
本文用 SRLeaseBench 演示一个结果租约层。任务 ID 为 SR-1132-509,样例输入 fixture_city_720p 为 1280×720,演示夹具把返回结果标记为 2560×1440;页面代次为 17,当前持有 1 份结果,累计释放 3 份,拒绝 1 个迟到结果。三阶段流程完成两段,显示 67%,状态为 ANALYZED_WAITING_SWAP。所有数字都是可核对的演示契约,不冒充真实超分性能或设备实测。

一、把 PixelMap 看成资源,不要只当图片变量
ArkTS 变量被覆盖,不等于底层图像资源已经正确结束生命周期。官方 Image Kit 指导明确要求:在 PixelMap 的异步操作完成、对象不再需要后调用 release()。超分返回的 PixelMap 同样需要清晰所有权,否则连续处理会把大块像素内存留到不可预测的回收时机。
最简单的页面通常有三个引用:输入 PixelMap、分析结果 PixelMap、Image 组件正在显示的 PixelMap。它们可能不是同一个对象,也不应该共享一个“页面销毁时一起 release”的粗糙策略。输入资源由解码阶段拥有;process() 返回结果先由任务拥有;只有通过代次检查并完成 UI 交换后,结果所有权才转给显示租约。
租约的含义不是锁,而是一条交接规则:新结果进入页面前先验代次;UI 引用更新后,再释放旧结果;页面离开时先阻止新交接,等待在途任务结算,最后释放当前结果并销毁分析器。任何未被接纳的返回值,都由产生它的任务立即释放。
这个顺序还有一个现实原因。用户操作频率和算法完成顺序没有必然关系。第 17 代请求发出后,用户可能立刻发起第 18 代;若第 17 代最后才返回,不能因为它“成功”就覆盖第 18 代页面。成功只说明能力调用完成,不说明结果仍属于当前界面。
二、创建分析器时先约定唯一销毁路径
第一段代码解决“多个按钮各自创建分析器,最后没人知道该销毁哪一个”的问题。AnalyzerOwner 只允许一个活动实例,并把状态限制为 IDLE、READY、DESTROYING、DESTROYED。get() 在销毁开始后拒绝继续提供实例。
import { imageSuperResolution } from '@kit.CoreVisionKit'
type OwnerState = 'IDLE' | 'READY' | 'DESTROYING' | 'DESTROYED'
class AnalyzerOwner {
private analyzer?: imageSuperResolution.ImageSRAnalyzer
private createTask?: Promise<imageSuperResolution.ImageSRAnalyzer>
private state: OwnerState = 'IDLE'
async get(): Promise<imageSuperResolution.ImageSRAnalyzer> {
if (this.state === 'DESTROYING' || this.state === 'DESTROYED') {
throw new Error(`analyzer unavailable: ${this.state}`)
}
if (this.analyzer) return this.analyzer
if (!this.createTask) {
this.createTask = imageSuperResolution.ImageSRAnalyzer.create()
}
this.analyzer = await this.createTask
this.state = 'READY'
return this.analyzer
}
async destroy(): Promise<void> {
if (this.state === 'DESTROYED' || this.state === 'DESTROYING') return
this.state = 'DESTROYING'
const analyzer = this.analyzer ?? await this.createTask
if (analyzer) await analyzer.destroy()
this.analyzer = undefined
this.createTask = undefined
this.state = 'DESTROYED'
}
}
这里没有在 get() 失败后无限重试。创建失败可能来自能力不可用、设备限制或环境问题,业务应捕获错误、提供降级路径并记录错误码,而不是在按钮回调里递归创建。destroy() 也必须幂等;页面生命周期、异常收口和主动退出可能同时请求销毁,重复调用不应再次操作已释放实例。
当前公开变更资料只确认 process() 接受 visionBase.Request,没有理由在文章里猜测不存在的请求字段。SRLeaseBench 把请求构造放在单独的 RequestFactory 中,实际项目应按当前 API 参考和 SDK 类型声明创建 Request。生命周期层只接收一个已经合法构造的 Request,不侵入其内部结构。
三、任务拥有返回值,页面只接纳当前代次
第二段代码解决“迟到成功结果覆盖新页面”的问题。每次选择新图片都会增加 generation。任务返回后先核对页面是否仍活跃、代次是否一致;不满足时立即释放返回 PixelMap。只有当前代次可以进入 pendingSwap。
import { image } from '@kit.ImageKit'
import { visionBase } from '@kit.CoreVisionKit'
interface PendingResult {
generation: number
pixelMap: image.PixelMap
}
class SRLeaseController {
private owner = new AnalyzerOwner()
private generation: number = 16
private active: boolean = true
private current?: image.PixelMap
private pendingSwap?: PendingResult
private inFlight = new Set<Promise<void>>()
releasedCount: number = 0
lateDropped: number = 0
submit(request: visionBase.Request): number {
const generation = ++this.generation
const task = this.run(generation, request)
this.inFlight.add(task)
void task.finally(() => this.inFlight.delete(task))
return generation
}
private async run(generation: number, request: visionBase.Request): Promise<void> {
const analyzer = await this.owner.get()
const response = await analyzer.process(request)
const result = response.pixelMap
if (!this.active || generation !== this.generation) {
result.release()
this.releasedCount += 1
this.lateDropped += 1
return
}
this.pendingSwap = { generation, pixelMap: result }
}
}
任务集合保存的是结算 Promise,用于销毁栅栏;finally 必须移除自身,否则完成任务也会长期滞留。实际代码还要在 run() 周围捕获 BusinessError,区分创建失败、处理失败和页面取消。失败路径没有返回 PixelMap 时不能调用 release;已经得到结果后发生 UI 交换错误,则必须由 pending 租约回收。
lateDropped=1 表示演示中确实有一份结果被代次检查拒绝,不代表底层算法可取消。逻辑拒绝只能防止旧结果进入 UI,不能减少已经发生的计算。若能力后续提供取消接口,再把取消与代次校验组合;在当前已核实接口之外,不虚构 cancel()。
项目结构中,AnalyzerOwner.ets 管理能力实例,SRLeaseController.ets 管理代次和 PixelMap,RequestFactory.ets 对接当前 SDK 请求类型,SRLeasePage.ets 只渲染状态。DevEco Studio 风格配图是根据这份结构生成的技术演示,不是实际 IDE 截图。

四、UI 交换完成后,才能释放旧结果
pendingSwap 并不等于页面已经安全显示新图。如果收到结果就立刻释放 current,而 UI 仍在这一帧引用旧 PixelMap,可能出现闪烁、空白或资源访问错误。更稳妥的做法是把交换拆成“准备、提交、清理”三步。
第三段代码解决“新旧 PixelMap 释放顺序不明确”和“页面退出时仍有任务返回”的问题。commitSwap() 先把新对象设为当前,再把旧对象放到下一次 UI 调度后释放。示例使用一个抽象的 afterNextFrame,实际项目应接入已经验证的 UI 帧回调或组件更新时机,不能把任意 setTimeout(0) 当作渲染完成证明。
async commitSwap(afterNextFrame: (cb: () => void) => void): Promise<boolean> {
const pending = this.pendingSwap
if (!pending || pending.generation !== this.generation || !this.active) {
return false
}
const old = this.current
this.current = pending.pixelMap
this.pendingSwap = undefined
afterNextFrame(() => {
if (old) {
old.release()
this.releasedCount += 1
}
})
return true
}
async close(): Promise<void> {
if (!this.active) return
this.active = false
this.generation += 1
await Promise.allSettled([...this.inFlight])
if (this.pendingSwap) {
this.pendingSwap.pixelMap.release()
this.pendingSwap = undefined
this.releasedCount += 1
}
if (this.current) {
this.current.release()
this.current = undefined
this.releasedCount += 1
}
await this.owner.destroy()
}
关闭顺序不能反过来。若先销毁分析器,再等待在途 process(),底层行为可能进入未定义区;若先释放页面结果但没有把 active 置为 false,下一份返回值又会进入 pending。正确顺序是关闭接纳入口、使旧代次失效、等待任务结算、回收两类 PixelMap、最后销毁分析器。
等待全部任务也需要超时策略。示例为了突出所有权没有展开超时;生产代码应记录等待预算,超时后进入可诊断的 CLOSE_TIMEOUT,并依据官方能力保证决定是否继续销毁。不能在不知道底层状态时简单忽略 Promise,也不能伪造“资源已释放”日志。
五、67% 表示交换尚未提交
运行页将流程拆为三个阶段:DECODED、ANALYZED、SWAPPED。演示已经完成前两步,因此显示 67%,不是算法推理进度。当前状态为 ANALYZED_WAITING_SWAP,说明结果已进入 pending 租约,但 UI 交换仍等待下一帧提交。

页面展示任务 SR-1132-509、夹具 fixture_city_720p、输入 1280×720、演示输出 2560×1440、generation 17、活动租约 1。顶部状态栏固定为 11:32、Wi‑Fi、5G、信号和 74% 电量。红色标注只圈出 67% 与 ANALYZED_WAITING_SWAP,提醒读者不要把“分析完成”写成“界面已完成”。
输入与输出尺寸来自测试夹具,不是本文对超分倍率作出的通用承诺。真实返回结果应通过 PixelMap 图像信息读取,并将实际尺寸写入日志。不同输入、设备或能力版本的输出策略应以当前 API 文档和实际结果为准。
六、诊断页要能对上每一次 release
详情页按租约列出资源:输入 PixelMap 由解码器持有;pending 结果属于 generation 17;当前显示租约尚未替换;历史累计释放 3;迟到结果拒绝并释放 1。销毁栅栏状态为 OPEN,在途任务为 0,因此允许提交 UI 交换。

03 与 04 的职责不同。总览页回答“流程到了哪一步”,诊断页回答“哪一方拥有哪份 PixelMap、为何还不能 destroy”。时间、任务 ID、generation 和 74% 电量保持一致,日志使用 create=PASS、process=PASS、pending=1、released=3、lateDropped=1、barrier=OPEN。
每次 release 日志应带资源 ID 与原因,如 replaced、late_generation、page_close、pending_abort。只打印总数无法定位重复释放。资源 ID可以由租约层生成,不要依赖 PixelMap 内部地址。释放后立即从对应字段移除引用,避免后续分支误判对象仍可用。
七、异常路径比成功路径更值得先画出来
创建分析器失败时,页面保持原图并显示能力不可用;process() 失败时,当前显示结果不能被清空;新结果成功但页面已经关闭时,任务负责立即释放;UI 交换失败时,pending 结果仍归租约层,不能转成 current;销毁失败时,记录状态和错误,不应把 owner 标成完全结束。
输入 PixelMap 的释放也要和请求生命周期对齐。若 visionBase.Request 在 process() 完成前仍引用输入资源,解码器不能在提交请求后立即 release。请求字段与复制语义必须以当前 SDK 文档为准;不确定时,至少把输入租约保持到 Promise 结算,再释放或交还上层。
页面后台化是否销毁分析器取决于产品策略。短时间切后台可能保留实例以降低重建成本,长期后台或内存压力下则应释放。无论选择哪种策略,都需要唯一 owner 和同一套栅栏,而不是在 onPageHide、aboutToDisappear、错误弹窗里分别调用 destroy。
八、与已有并发优化不是同一个问题
队列限流解决“同时跑多少任务”,电量门禁解决“何时允许启动”,回归检测解决“结果质量是否退化”。本文处理的是另一个维度:每一份成功返回的 PixelMap 由谁接收、何时进入 UI、何时释放、分析器何时可以销毁。即使并发度固定为 1,这个问题仍然存在。
同样,代次校验不是取消。它只是保证旧结果不会污染新页面。资源租约也不是内存优化的代名词;它首先建立可证明的所有权,然后才有资格谈峰值内存。没有所有权图,任何“及时 release”都可能过早或重复。
九、结论与边界
SRLeaseBench 最终把三个容易混淆的完成时刻分开:算法 Promise 完成、结果被当前页面接纳、UI 交换后旧资源释放。演示状态停在 ANALYZED_WAITING_SWAP,67% 正好说明第三步尚未提交,而不是用一个 100% 掩盖资源还在 pending。
当前一手资料确认了 API 26 中 ImageSRAnalyzer 的创建、处理、结果 PixelMap 与销毁接口,也确认 PixelMap 不再使用后应手动 release。本文没有补造倍率、取消、进度回调或隐藏参数。请求构造、设备支持和错误码仍应以项目使用的最新 SDK API 参考为准。
可以复用的工程结论只有四条:分析器由唯一 owner 管理;每次请求携带页面代次;未被接纳的结果由任务立即释放;关闭时先封入口、等结算、收 PixelMap、再 destroy。把这四条写进封装层,超分页面才不会在连续选图和页面退出时留下无法解释的资源状态。
十、参考资料
- 华为开发者联盟,Core Vision Kit API 26 变更清单(
ImageSRAnalyzer.create/process/destroy与ISPResponse.pixelMap):https://developer.huawei.com/consumer/en/doc/harmonyos-releases/js-apidiff-corevisionkit-7001 - 华为开发者联盟,Image Kit 图片解码与 PixelMap / ImageSource 释放指导:https://developer.huawei.com/consumer/en/doc/harmonyos-guides-V14/image-decoding-V14
- 华为开发者联盟,图像超分能力介绍:https://developer.huawei.com/consumer/cn/hiai/engine/image-super-resolution
更多推荐


所有评论(0)