ArkUI Swiper:超分结果图集缓存窗口与切页确认【鸿蒙心迹】
图像超分完成以后,产品通常不会直接让用户下载文件,而是先给一组候选结果:放大细节、比较边缘、选择最终版本。这个看起来像相册的小页面,实际上有两个容易被混淆的负担:组件预渲染范围和业务持有的图片资源。二者都叫“缓存”,却不共享同一个释放时机。
假设一个演示任务 SR-VIEW-042 已经有三张待比较的成品,编号分别是 R-101、R-102、R-103。页面默认定位第二张,页码显示 2/3。用户快速左右滑动,或者收到一批新的超分结果时,最怕发生的并非滑动不流畅,而是标题已经变成新图,旧图的异步加载回调却又改回了旧预览。
本文只讨论超分完成后的浏览与交付界面,不假定手机执行了任何超分模型推理。文中的图像、日志、任务编号和界面均为可复现的方案演示,不是 DevEco 编译或真机性能测量证据。

一、先把“缓存”这个词拆开
1. Swiper 负责的是相邻子组件
官方 Swiper 组件提供 cachedCount,用于设置预加载的子组件数量,也提供 onChange,用于收到当前索引变化事件。对于三张预览图,设置 cachedCount(1),表达的是当前页前后预留一个相邻页面的渲染窗口;它不等于应用只会占用一张或三张图片的固定内存,更不是自动替我们销毁所有 PixelMap。
这里选用本地媒体资源作为预览来源,将解码与算法输入排除在本次实验外。这样做的好处是,切页异常能够限定在 ArkUI 页面、业务状态和延迟回包,而不需要先怀疑超分耗时。示例的 R-101、R-102、R-103 是业务键,不是系统生成的图片 ID。
2. 业务层还需要一个“当前选择”
页面只保存 currentIndex=1 还不够,因为服务端或本地筛选可能重排结果。索引 1 是一个位置,而 R-102 才是用户真正选中的对象。当前批次没有重排需求,但仍保留 selectedId,使后续替换结果数组时能找到同一张图;找不到则退回第一项,并明确提示,而不是默默选择另一张。
因此,本例规定三个相互独立的事实:当前业务对象 selectedId=R-102,可见位置 currentIndex=1,页面代次 epoch=7。只有前两项一起变化,页面才算完成切换;旧代次的结果一律没有提交权。
二、把页面结构控制在一个可解释的范围
为了解决页码与图片对象错位,先在 CompareGalleryPage.ets 定义静态结果集和索引回调。source 指向工程里真实存在时应补齐的三张资源图;下列 $r 路径是 Demo 的资源约定,不是 SDK 自带超分样本。
interface SRPreview {
id: string;
label: string;
source: Resource;
}
@Entry
@Component
struct CompareGalleryPage {
@State currentIndex: number = 1;
@State selectedId: string = 'R-102';
private controller: SwiperController = new SwiperController();
private items: SRPreview[] = [
{ id: 'R-101', label: '文字边缘', source: $r('app.media.sr_a') },
{ id: 'R-102', label: '建筑纹理', source: $r('app.media.sr_b') },
{ id: 'R-103', label: '树叶层次', source: $r('app.media.sr_c') }
];
private onPageChanged(index: number): void {
if (index < 0 || index >= this.items.length) return;
this.currentIndex = index;
this.selectedId = this.items[index].id;
console.info(`SRGallery page=${this.selectedId} index=${index} epoch=7`);
}
build() {
Column({ space: 14 }) {
Text('SRGallery · 超分结果图集').fontSize(23).fontWeight(FontWeight.Bold)
Text('任务 SR-VIEW-042').fontSize(14)
Swiper(this.controller) {
ForEach(this.items, (item: SRPreview) => {
Column({ space: 8 }) {
Image(item.source).objectFit(ImageFit.Contain).width('96%').height(300)
Text(`${item.id} · ${item.label}`)
}
}, (item: SRPreview) => item.id)
}
.index(this.currentIndex)
.cachedCount(1)
.loop(false)
.onChange((index: number) => this.onPageChanged(index))
Text(`${this.currentIndex + 1}/${this.items.length} · ${this.selectedId}`)
}.width('100%').height('100%').padding(16)
}
}
这里特意把 onChange 的校验放在更新状态之前。即便未来接入远端结果分页,也不能默认任何回调索引都有效。ForEach 的键使用稳定 id 而不是数组索引,目的是减少同一图片因排序变化产生的不必要组件重建。静态数组本身没有监听能力;如果实际产品支持动态替换,应使用可观察状态并配合稳定键做一次完整状态迁移。
代码中 cachedCount(1) 只是 ArkUI 组件策略。图片数量、文件大小、实际解码驻留仍受图片来源和加载策略影响。不能从这个参数推算出所谓“节省 66% 内存”,也不能把示意界面的一次滑动理解为性能验收。
三、邻页预算不等于预先解码三张图
在实际项目里,我更愿意把“计划保留哪些候选”写成一段纯函数,再把真正的解码、网络请求或文件读取交给独立服务。这样 UI 决定可见窗口,业务决定加载队列,两层预算不会在同一处争抢状态。
下面的函数解决一个具体问题:当当前索引是 1、半径是 1,计划集合应该是 R-101,R-102,R-103;来到首项则只有 R-101,R-102。该函数不接触设备资源,所以单元测试可以仅比较业务 ID。
export function planVisibleIds(ids: string[], center: number, radius: number): string[] {
if (ids.length === 0 || center < 0 || center >= ids.length) return [];
const safeRadius: number = Math.max(0, Math.floor(radius));
const left: number = Math.max(0, center - safeRadius);
const right: number = Math.min(ids.length - 1, center + safeRadius);
const planned: string[] = [];
for (let i = left; i <= right; i++) {
planned.push(ids[i]);
}
return planned;
}
const plan: string[] = planVisibleIds(['R-101', 'R-102', 'R-103'], 1, 1);
// plan: R-101,R-102,R-103;这是业务计划,不代表资源已解码。
console.info(`SRGallery neighbors=${plan.filter((id: string) => id !== 'R-102').join(',')}`);
调用方可以把 plan 用于发出预览请求,超出范围的请求可以标记为不再需要。但取消一个业务任务与释放底层图像对象不是一回事:如果后续接入自行创建的 PixelMap,应当在最后一个持有者释放引用后调用对应释放接口;如果使用的是普通 Image($r(...)),也不应擅自模拟系统内部缓存的释放时机。
这也解释了调试上经常出现的错觉:onChange 已经成功、页码也更新了,内存曲线却没有立即下降。它可能是组件复用、图片解码缓存或运行时回收策略的综合结果,不能只根据 cachedCount 判断泄漏。
四、把旧回包挡在页面代次外
1. 只取消请求并不可靠
假设用户从 R-101 快速滑到 R-102,恰好上一张缩略图的异步回调晚到。是否真正支持取消取决于底层预览服务;即便调用取消,也可能已经在回调队列里。最稳妥的提交合同是:先检查回包所属的 epoch,再更新当前预览状态。
下面用应用层协调器表达这个规则。它不依赖虚构的图像超分 API,requestPreview 由业务项目提供。示例按 epoch=7 处理新请求,epoch=6 的旧结果被标记为 ignored。若页面销毁,close() 会使所有在途结果失去提交资格。
export class GalleryCommitGate {
private epoch: number = 7;
private closed: boolean = false;
private selectedId: string = 'R-102';
switchTo(id: string): number {
this.selectedId = id;
this.epoch++;
return this.epoch;
}
accept(id: string, requestEpoch: number): boolean {
const valid: boolean = !this.closed &&
requestEpoch === this.epoch && id === this.selectedId;
if (!valid) {
console.info(`SRGallery stale=${id} epoch=${requestEpoch} ignored`);
}
return valid;
}
close(): void {
this.closed = true;
this.epoch++;
}
}
协调器故意只判断“可否提交”,并不替调用方销毁真实像素对象。拿到旧结果的业务服务仍必须自行释放其持有的缓冲、文件句柄或解码资源。示例默认展示态 epoch=7,诊断记录中的 epoch=6 是构造出来的过期事件,并不是测得的线程竞态。

五、怎样调试才不会被演示数据误导
建议把验收拆成两层。第一层完全使用模型事件,明确输入、预期状态和日志;第二层才放到目标设备,观察 ArkUI 组件树、内存、滑动卡顿与图片释放。两层不能相互冒充。
对于 SR-VIEW-042,模型层先选 R-102,要求界面显示 2/3、cachedCount=1、epoch=7。之后分别向提交门发送 R-101/epoch=6 和 R-102/epoch=7,前者应拒绝、后者可以被当前页面接受。演示 HiLog 只保留三条关键消息:SRGallery page=R-102 index=1 epoch=7、SRGallery neighbors=R-101,R-103、SRGallery stale=R-101 epoch=6 ignored。
做实际工程验收时,至少还要补齐连续快速滑动、页面返回重入、结果集合变空、资源加载失败、应用转后台,以及高分辨率图片导致的内存峰值。若加载服务带有独立缓存,需要用它自己的命中率、容量上限和释放日志进行校验。本文展示的屏幕并没有运行上述压力实验。

六、结果与适用边界
这个方案能给出确定的设计结果:页面选中对象由稳定 ID 维护,Swiper 只承担子组件预加载,相邻资源计划由独立纯函数推导,异步结果必须经过代次门禁。这样写的价值,是把一个“翻图偶发错乱”的模糊现象拆成可以独立验证的几条契约。
它不能证明某台设备上的超分推理耗时、内存节省率或实际帧率,也没有集成任何未核对的 Core Vision Kit 推理接口。后续如果要做真正的 SR 推理,只需把结果文件通过受控仓库交给图集组件,不应把模型会话直接绑定到 Swiper 的 onChange。
值得强调的是,cachedCount 与 onChange 属于官方 ArkUI Swiper 能力,而 planVisibleIds、GalleryCommitGate 都是本文自定义的业务辅助逻辑。具体 API 与限制以目标 SDK 的 Swiper 官方参考 和 Swiper 应用场景说明 为准。准备投入产品时,还应通过 DevEco 的实际预览与真机验证来确认资源使用,不能凭演示配图代替证据。
更多推荐



所有评论(0)