HarmonyOS ArkUI Image × ArkTS Promise:超分预览回包乱序与跨图串帧防护【鸿蒙心迹】
Demo:UltraPreview|任务编号:SR-206|方向:图像超分业务预览层
内容性质:基于官方组件资料编写的工程示例;文中日志、数据和图片是设计验证示意,未在真机或 DevEco Studio 中实测,也不代表系统自带超分推理接口。

两张图都是对的,为什么屏幕显示的却是错的
一个相册类应用需要在用户挑选照片后,异步生成高清预览。最容易遗漏的不是某个算法参数,而是结果究竟属于哪一次选择。设想用户先点了 IMG-621,紧接着点 IMG-622;第一份任务耗时较长,第二份先结束。若代码把任何完成回调都写入同一个 @State 图片字段,界面会短暂显示正确的第二张,随后被较晚返回的第一张覆盖。这不是超分精度不足,而是业务身份和异步完成顺序没有建立约束。复现也不需要真正跑推理模型,只需人为让第一次延迟、第二次提前结束,即可看清状态问题。
为避免对能力边界产生误解,本文把“超分”拆成两层:推理、增强或后台编码由应用自有实现负责;这里给出的示例只处理它们返回结果时的 预览提交通道。华为官方的 Image 组件支持使用资源与 PixelMap 作为数据源,ImageFit.Contain 可以把整张图片放在固定展示区域内。Image Kit 可以生成 PixelMap,但它不是通用的、自动存在于任何设备上的“超分函数”。本文不会杜撰 superResolution.run() 之类的系统 API。图上的山峰、任务编号及耗时都为人工构造,用于描述状态契约。
这次案例把工程命名为 UltraPreview,界面左边用列表选择候选图,右边展示预览。业务约定四个量:资源身份 assetId、当前请求代次 seq、页面是否仍可见 visible、已经承认的预览数据。任务编号 SR-206 只用来串联日志,不作为图片身份。初始先发出 IMG-621/seq=1,随即发出 IMG-622/seq=2,设定第二个回包先到:预期提交 IMG-622 READY;第一张稍后返回,则必须记录 discard 1 IMG-621,绝不能再覆盖预览。
先给回调装一道“身份栅栏”
很多实现只有 loading 布尔值;它能控制转圈,却无法告诉应用“哪一个请求才拥有屏幕”。这里把栅栏抽成纯业务类,并不依赖 HarmonyOS 框架。这样可以在工程里用构造数据进行单元测试,避免为了验证一次序号判断就打开真实相册。设计上的决定是:选择新图会让旧请求立即失去提交资格,但不承诺底层推理任务真的被取消。旧计算可能仍耗电、占内存,需要后续接入服务自己的取消协议。
// entry/src/main/ets/model/PreviewGate.ets
export class PreviewGate {
private seq: number = 0;
private selected: string = '';
private visible: boolean = true;
start(assetId: string): number {
this.visible = true;
this.selected = assetId;
this.seq += 1;
return this.seq;
}
isCurrent(seq: number, assetId: string): boolean {
return this.visible && seq === this.seq && assetId === this.selected;
}
hide(): void {
this.visible = false;
this.seq += 1; // 让隐藏前在途的回包全部失效
}
}
三个判断缺一不可。只比对代次,可能出现同一服务错把 B 的数据带着最新代次返回;只比较照片 ID,用户连续重复点同一张时,上一轮的结果仍会越过新一轮参数;只关心页面可见性,则导航回来后可能显示旧页面的历史任务。将这三项作为同一个判定条件,能把 UI 提交范围收窄到“页面仍然需要的那份资源”。另外不要把 seq 持久化成长期 ID,它只在这次页面会话里有意义,重新创建页面后应重新建立代次。
制作一个可控延迟的假服务,不冒充系统超分
为了说明回包乱序,示例定义应用自己的 MockPreviewService。它返回一个 ArkUI 可以显示的资源标识,不执行去噪、放大或神经网络推理。需要在示例工程 resources/base/media 下自行准备 demo_a.png、demo_b.png;这两个文件是业务测试素材,不是本素材包交付的六张文章插图。用于实际项目时,只替换服务实现,把推理完成结果包装为同样的结构,并按资源来源做好访问权限、文件可用性和内存释放。
// entry/src/main/ets/service/MockPreviewService.ets
export interface PreviewResult {
assetId: string;
displayResource: Resource;
}
export function requestPreview(assetId: string): Promise<PreviewResult> {
// 仅为构造延迟,950ms 与 140ms 不是实测耗时
const ms: number = assetId === 'IMG-621' ? 950 : 140;
return new Promise<PreviewResult>((resolve) => {
setTimeout(() => resolve({
assetId: assetId,
displayResource: assetId === 'IMG-621'
? $r('app.media.demo_a') : $r('app.media.demo_b')
}), ms);
});
}
这里刻意没有向服务塞进“超分质量档位、硬件加速器、设备白名单”等没有官方依据的参数。要扩展为真实管线,服务应另外携带 sourceRevision、输出尺寸、颜色空间与缓存键,并证明它们和所选原图对应。若服务返回 PixelMap,旧请求被判过期后还要适时调用 release();但释放前必须保证 UI 不再引用该 PixelMap 且所有异步操作已经结束。官方 Image Kit 文档对 PixelMap 释放顺序有说明。本文资源示例由框架管理,不手动释放 Resource,避免把两种生命期混为一谈。
ArkUI 只提交通过栅栏的结果
下面是 Index 页的核心片段。示例把调度、状态和渲染放在一处,便于定位竞态;生产项目可以把业务逻辑拆出页面。@State 负责触发预览和标签更新,Image 负责显示资源,不应该在 Image.onComplete 再改变“当前选中的业务图片”,因为图片真正加载完成的时序还可能与服务回包不同。任何改变身份的行为都先调用栅栏,随后再考虑改变展示状态。
// entry/src/main/ets/pages/Index.ets(需配套两张 media 测试图)
import { PreviewGate } from '../model/PreviewGate';
import { requestPreview } from '../service/MockPreviewService';
@Entry
@Component
struct Index {
private gate: PreviewGate = new PreviewGate();
@State currentAsset: string = 'IMG-621';
@State displayedAsset: string = '';
@State preview: Resource = $r('app.media.demo_a');
@State status: string = 'IDLE';
private async choose(assetId: string): Promise<void> {
const seq: number = this.gate.start(assetId);
this.currentAsset = assetId;
this.status = 'LOADING';
console.info(`SR-206 start ${seq} ${assetId}`);
try {
const result = await requestPreview(assetId);
if (!this.gate.isCurrent(seq, result.assetId)) {
console.warn(`SR-206 discard ${seq} ${result.assetId}`);
return;
}
this.preview = result.displayResource;
this.displayedAsset = result.assetId;
this.status = 'READY';
console.info(`SR-206 commit ${seq} ${result.assetId}`);
} catch (e) {
if (this.gate.isCurrent(seq, assetId)) this.status = 'ERROR';
console.error(`SR-206 request failed ${assetId}: ${String(e)}`);
}
}
onPageHide(): void { this.gate.hide(); }
build() {
Column({ space: 12 }) {
Text('超分候选预览 SR-206').fontSize(22)
Image(this.preview).width('92%').height(320)
.objectFit(ImageFit.Contain)
Text(`${this.displayedAsset} ${this.status}`)
Button('选择 IMG-621').onClick(() => { this.choose('IMG-621'); })
Button('选择 IMG-622').onClick(() => { this.choose('IMG-622'); })
}.width('100%').padding(16)
}
}

配图中选中了 IMG-622,状态为 READY,HiLog 展示先提交第二轮结果,再丢弃第一轮结果。这些信息与案例设定完全对应,但图片中的 IDE 文字只是排版演示,不应据此推断脚本已编译。更严格的工程记录应使用真实构建日志,标明 SDK、目标机型和复现步骤;素材创作阶段只能把上述日志当作“应当观察到”的示例。若真实运行日志出现 commit 1 IMG-621 晚于 commit 2 IMG-622,说明页面并没有真正实施代次保护。
调试时不要只看最终截图
最小验证至少包括四组事件。第一组按 621 → 622 快速选择,强制把第一请求延长,核对是否出现 start 1、start 2、commit 2、discard 1。第二组对同一张图片连续点击两次,确保最新代次才有提交权;同一 assetId 不表示两份处理结果可交换。第三组在服务还未返回时导航离开页面,调用 hide() 后的结果只能记录丢弃,不能对卸载中的页面写入图片。第四组制造异常:第二请求失败后应显示 ERROR,不要把第一请求的迟到结果当成兜底成功;真正的回退应重新创建一个新请求并生成新代次。
上述 setTimeout 仅用于形成确定的回包先后,它不模拟硬件算力、解码吞吐或者模型质量。增加日志时建议每一行携带 taskId + seq + assetId + action,并限制文件路径、用户图像描述等敏感信息的输出;在生产日志里打印原图的完整路径并没有调试必要。对图片加载耗时,可以额外用官方 Image 组件的加载事件观察 UI 层行为,但那与“后台算法结果先后顺序”不是同一个指标,两个时间轴应分别记录。

边界:过期数据不等于已取消的计算
这个方案有意只解决业务视图一致性,它没有真正停止图像超分任务,更不能保证 PixelMap、文件描述符或推理 Session 自动释放。若后端可取消,就把请求序号映射到取消令牌;否则需在旧请求返回后关闭流、清理临时文件、释放解码对象,并注意释放与渲染不能同时竞争使用对象。若结果落盘,文件名与缓存键必须纳入资产 ID 和算法版本,不能让 IMG-622 复用 IMG-621 的路径。还有一条容易被忽略:页面隐藏后再显示,不能只把 visible=true 就继续使用旧代次,应发起新选择。
对于真实的超分功能,本文没有提供模型集成、推理精度或内存峰值证明;要上线仍需根据目标 HarmonyOS SDK、设备性能、真实源图权限和具体第三方模型的文档单独验证。这里更适合作为把超分服务接入 ArkUI 前的一层可测试防线:哪张图当前被用户选中,哪次请求最后有效,哪份回包有权改变画面,三个问题回答明确,异步预览才不会串图。
官方资料核对与版本说明
- 华为开发者文档《显示图片 (Image)》(2026-10-16 更新):https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/arkts-graphics-display
- 华为开发者 API 《PixelMap》:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/arkts-apis-image-f
- Image Kit 官方能力目录:https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/image-kit
更多推荐



所有评论(0)