HarmonyOS 7 Core Vision Kit + Image Kit:超分批处理的 PixelMap 预算、失败降级与结果原子替换【鸿蒙心迹】

发布时间:2026/10/1 14:55:29
HarmonyOS 7 Core Vision Kit + Image Kit:超分批处理的 PixelMap 预算、失败降级与结果原子替换【鸿蒙心迹】 作者李游这次做的不是“把一张图变清晰”的展示页而是一个会连续处理十二张旧照片的批任务。单张调用一直正常批量跑到第七张却开始抖动偶尔还会把上一张预览留在页面上。真正需要收拾的是 PixelMap 的所有权、内存峰值和结果提交时机。一、单张成功不代表批处理可以直接排队Demo 叫ClearBatch Lab任务编号固定为sr_20261001_06。输入图统一缩放到 1280×720再交给图像超分能力生成 2560×1440 的结果。最初的实现很直白从相册取十二张图循环创建输入 PixelMap调用分析器得到输出后立刻更新预览。前两张看不出问题。处理到第七张时页面滚动开始掉帧第九张偶发能力调用失败更麻烦的是失败后列表仍显示“已完成”点进去却是上一张图片。HiLog 中没有业务异常因为 Promise 的确 resolve 过只是 UI 持有的对象已经不再属于当前条目。我把链路按对象拆开后发现一次任务至少可能同时存在四份大对象解码后的输入、超分返回值、列表缩略图和当前预览。1280×720 的 RGBA 数据约 3.5 MB2560×1440 约 14 MB如果十二项无节制并发峰值不是十二乘十四这么简单解码、推理中间态和 UI 引用都会叠加。这次验收不再用“十二张都能出图”作为终点而是固定四条数据12/12有明确终态峰值不超过188 MB失败降级1张最终只保留1个预览 PixelMap。页面离开后预览也必须释放。二、先给 PixelMap 建一本账项目目录没有把所有逻辑塞进页面engine隔离超分能力budget管内存令牌pipeline负责状态机store只接收已提交结果。这样做的好处是UI 重建不会重新创建整条处理链。entry/src/main/ets ├── pages/ClearBatchPage.ets ├── components/SrQueuePanel.ets ├── engine/SrEngineAdapter.ets ├── budget/PixelBudget.ets ├── pipeline/SrBatchPipeline.ets ├── model/SrJob.ets └── store/PreviewStore.ets第一个要解决的问题是“并发数相同图片尺寸不同时内存仍然失控”。固定开两个任务并不等于固定内存所以预算器按估算字节数发放令牌而不是按任务个数计数。exportclassPixelBudget{privateusedBytes:number0privatereadonlywaiters:Array()void[]constructor(privatereadonlylimitBytes:number){}asyncacquire(bytes:number):Promisevoid{while(this.usedBytesbytesthis.limitBytes){awaitnewPromisevoid((resolve)this.waiters.push(resolve))}this.usedBytesbytes}release(bytes:number):void{this.usedBytesMath.max(0,this.usedBytes-bytes)this.waiters.shift()?.()}getusedMB():number{returnMath.round(this.usedBytes/1024/1024)}}acquire()发生在解码前申请值包含输入、预计输出和一段安全余量。拿不到令牌的条目保持WAITING_MEMORY不会提前解码后再排队。release()必须放在任务的finally成功、失败和取消都走同一出口如果只在成功分支释放失败一次以后预算就会永久减少。这个实现是 Demo 版因此等待队列只做先进先出。正式项目还要处理页面优先级例如当前可见条目可以插队但不能饿死后台任务。另一个边界是单张图片估算值已经超过总预算此时不能永远等待应该在入队前直接降采样或拒绝并给出可理解的原因。预算估算不能只拿宽乘高。解码目标的像素格式、行对齐、倍率和能力内部的临时缓冲都会改变实际占用。我在 Demo 中用“输入字节 输出字节 百分之二十五余量”作为保守值再用实机峰值反推系数。它不是精确的内存分析器却能把不可控的十二路抢占变成可解释的两到三路推进。如果图片带旋转信息先读元数据再计算宽高避免用未旋转尺寸低估输出。队列面板还区分“等待内存”和“正在处理”。这不是文案细节用户看到第八张停留在等待可以继续浏览已有结果若所有条目都显示处理中他会把预算限流误判为卡死。调试日志同步记录等待时长超过三秒时输出当前占用者的条目 ID便于定位某个任务是否拿到令牌后迟迟没有释放。三、能力对象包在适配器里避免页面感知版本差异Core Vision Kit 在 HarmonyOS 7 / API 26 提供图像超分能力输出仍以 PixelMap 进入后续显示和保存链路。页面不直接调用版本相关接口而是依赖SrEngine。适配器内部负责创建分析器、调用实际能力并统一错误码这样能力签名变化不会蔓延到队列和 UI。下面这段代码解决“输入和输出究竟由谁释放”的问题。规则很简单调用者移交输入对象后流水线负责释放返回的输出只有在原子提交成功后才交给PreviewStore否则仍由当前方法释放。asyncrunOne(item:SrItem):PromiseSrResult{constestimatethis.estimateBytes(item,2)awaitthis.budget.acquire(estimate)letinput:image.PixelMap|undefinedletoutput:image.PixelMap|undefinedtry{item.stateSrJobState.DECODINGinputawaitthis.decoder.decode(item.uri,1280,720)item.stateSrJobState.ENHANCINGoutputawaitthis.engine.enhance(input,2)constcommittedthis.store.commitIfCurrent(item.id,item.generation,output)if(!committed){awaitoutput.release()outputundefinedreturn{state:SrJobState.STALE,degraded:false}}outputundefineditem.stateSrJobState.COMPLETEDreturn{state:item.state,degraded:false}}catch(error){returnawaitthis.fallback(item,error)}finally{awaitinput?.release()awaitoutput?.release()this.budget.release(estimate)}}output undefined不是多余赋值而是所有权转移的标记。提交成功后PreviewStore成为唯一持有者提交失败时当前方法释放输出。这样不会出现 store 和 pipeline 都释放同一个 PixelMap也不会因为忘记清空局部变量导致finally误释放正在显示的结果。generation 用来处理同一条目被重新执行的情况。用户在第六张上点了“重试”旧请求晚回来时commitIfCurrent()会发现代次不一致并拒绝提交。它可以丢弃晚到结果却不能替代资源释放因此拒绝以后仍要主动release()。四、降级不是把失败伪装成成功批任务里有一张图片故意注入能力不可用错误。第一版 catch 里直接返回原图列表仍标成COMPLETED这会让后续质量统计失真。现在把终态拆成COMPLETED、DEGRADED、FAILED和CANCELLED原图回退属于DEGRADED并记录fallbackCount 1。下面的代码解决“降级结果覆盖旧预览”和“重复重试累计对象”的问题。回退图先生成候选对象只有当前 generation 仍有效才替换替换完成后释放旧预览。privateasyncfallback(item:SrItem,reason:unknown):PromiseSrResult{constcandidateawaitthis.decoder.decode(item.uri,1280,720)constoldthis.store.swapIfCurrent(item.id,item.generation,candidate)if(oldundefined){awaitcandidate.release()item.stateSrJobState.STALEreturn{state:item.state,degraded:false}}awaitold?.release()item.stateSrJobState.DEGRADEDitem.messagethis.errorMapper.toMessage(reason)this.metrics.fallbackCountreturn{state:item.state,degraded:true}}这里的“原子”不是数据库事务而是业务上的不可分割替换校验代次、换入新对象、取出旧对象必须由 store 在一次同步方法里完成。若先更新 ID、稍后再更新 PixelMapArkUI 重绘可能刚好读到半成品。正式项目不能对所有错误都降级。输入损坏、解码失败没有可展示原图应进入FAILED能力暂时不可用可以回退用户取消则是CANCELLED不应弹错误。错误分类先于 UI 文案否则“已取消”很容易被统计成失败。保存到沙箱也放在提交之后。候选输出先写临时文件校验尺寸和编码结果再用最终文件名替换中途失败只删除临时文件不覆盖上一版可用结果。PixelMap 的原子替换与文件的原子替换是两层事务前者保证页面不读半成品后者保证应用重启后不读到零字节文件。两层任意一层失败条目状态都不能提前标绿。DevEco Studio 图里左侧就是上述目录中间打开SrBatchPipeline.ets右侧模拟器显示ClearBatch Lab底部 HiLog 固定为jobsr_20261001_06、queue12、input1280x720、output2560x1440、done12/12、fallback1、peak188MB、retainedPreview1 stateCOMPLETED。图片与正文使用同一组状态和数值。五、性能数据要看峰值也要看峰值之后能否回落调试时我先跑了无预算版本十二项同时解码峰值越过 400 MB完成后仍停在 230 MB 左右。加入令牌以后处理速度没有线性下降因为真正的推理能力本来也不能无限并行峰值稳定在 188 MB队列从WAITING_MEMORY逐项进入DECODING。更关键的是回落。任务完成后保留一个 2560×1440 预览其余输入、输出候选和缩略图中间态都释放。页面切到后台时不立即销毁当前预览避免短暂返回重新解码页面真正离开、store 无观察者后再释放最后一个对象。这个策略把“可见性”和“所有权”分开生命周期更清楚。为了验证重复调用我连续执行十轮每轮十二张并在第三轮中途取消。usedBytes每轮都回到零retainedPreview在页面内稳定为一退出页面后为零。若数字只增不减即使系统暂时没有 OOM也说明引用链还没有断干净。取消测试放在三个阶段等待预算时取消任务应从 waiters 移除解码后取消要释放输入并归还令牌能力返回后、提交前取消则要释放候选输出并拒绝更新预览。第一版只覆盖了第二种所以列表偶尔会留下永远等待的条目。修复后取消不是简单设置一个布尔值而是让每个资源边界都检查同一个 token并且由统一出口写入终态。前后台切换也单独测了一轮。短暂进入后台时正在推理的单项允许完成但不创建下一项回到前台后队列继续。若系统回收进程任务清单从持久化数据恢复所有未提交条目重新进入待处理绝不复用上次进程留下的句柄或 PixelMap 引用。这样会多算一次却比猜测 native 中间态是否仍有效可靠得多。TaskPool 适合放解码前的元数据计算和纯数据整理但 PixelMap 是否能跨线程传递要按当前 API 的可传输规则验证不能把普通对象、NativeBinding 对象和 ArrayBuffer 混为一谈。本 Demo 把能力调用留在受控服务中只把轻量状态回传页面减少序列化成本和线程归属误判。六、手机页保留能解释结果的数据运行页没有放十二张大图而是用当前预览、批次进度和资源指标回答三个问题结果是否真的放大、失败是否被识别、内存是否收住。06:18 的最终页面显示输入 1280×720、输出 2560×1440、12/12 完成、1 张降级、峰值 188 MB、保留预览 1、状态COMPLETED。红色细箭头只标出“降级 1 张”和“保留预览 1”因为这两项最容易被一个全绿的完成状态掩盖。用户可以继续点开降级项查看原因也可以重试单项重试会增加 generation不会重新执行整个批次。七、这条链路真正交付的是可控性图像超分最容易写成能力展示选图、调用、显示。但放进相册修复、商品图增强或离线素材处理后内存、取消、晚到结果和失败分级才决定它能否长期运行。ClearBatch Lab最终形成了明确边界预算器决定何时开始适配器隔离能力流水线管理所有权store 只接收当前代次的完整结果。这套 Demo 仍有边界。它没有实现断点续跑进程被回收后只保存任务清单不保存 PixelMap也没有把峰值阈值写死到所有设备而是示例采用 188 MB 的观测结果。正式项目应根据设备内存等级、图片格式和输出倍率动态配置并用真机压力测试校准。质量验收也不能只放大看锐度。我给每个结果保留输入尺寸、输出尺寸、耗时、降级原因和 generation方便定位“图变清楚了但条目对应错了”这类数据问题。批任务最终报告中12/12表示十二项都有终态不等于十二项全部使用了超分输出旁边的fallback1必须一起阅读。这种口径能避免运营侧把降级当成百分之百能力成功率。我最后留下两条日志断言任何条目结束后都不能处于DECODING或ENHANCING页面释放后预算必须为零、预览计数必须为零。十二张图片看起来都变清晰只是功能完成资源和状态都能收口才算工程完成。参考资料Core Vision Kit API 26 变更图像超分使用 PixelMap 完成图像变换TaskPool 使用规范