ArkData Preferences:文搜图模型换版后缓存隔离与失效回收【鸿蒙心迹】
同一张照片,索引算法升级前后给出的语义标签可能不一样。界面上的症状往往不吓人:用户输入“夜景”,结果列表很快出现,却夹着旧版本算出的“室内照”。如果缓存命中逻辑只看查询词,旧数据便会被当作新模型的结果继续展示。这里的麻烦不在搜索速度,而在结果属于哪一套解释规则。我把这个问题收缩成一个可以独立审查的模块:不实现向量推理,不假装调用某个未经核实的文搜图模型 API,而是处理索引侧已经返回的结果 ID 如何进入、离开本地缓存。演示项目叫 TextImageScope,固定批次 TXTIMG_20261017_06。旧模型 txt_embed_v2 留下十二个结果,新模型 txt_embed_v3 对同一图库修订号 42 重新给出九个结果。目标是绝不让旧结果闪现在新版本页面上。
## 一、先界定缓存里究竟存什么本篇的缓存不是图库资产数据库,也不是图片向量库。向量、缩略图和图片访问权限各自有生命周期,Preferences 并不适合承载大尺寸二进制内容。它保存的是当前可展示的一组结果 ID 以及判定这些 ID 有效的上下文。具体采用一个 JSON 字符串,里面带有 modelTag、albumRev、itemIds 和写入时间。数据库表或搜索服务仍由上层实现。为什么不把 modelTag、albumRev、itemIds 拆成三个键?因为发生进程退出时,三次写入可能只完成了一部分。单个序列化值无法代替数据库事务,却能缩小多键不一致窗口。写入后调用 flush(),等待持久化完成再通知页面“已重建”。这个约束比“有就读、没有就算”多了一步,但更接近真实发布环境的需要。字段组合也不能凭感觉设置。modelTag 对应业务发布的算法或标签词典版本;albumRev 是图库内容变化后由业务维护的单调修订号,不是系统提供的图片库版本号。同一个模型、同一个修订号才允许命中。查询词、筛选条件如果也影响结果集合,实际项目需要一并进入缓存作用域。本篇为了单独讨论模型换版,演示把查询固定成“夜景”。## 二、读取入口先判断作用域,再决定是否显示要解决的问题是冷启动时的“旧列表闪现”。如果先把缓存数组交给 @State,随后才比较版本,哪怕最终删除,用户也会看到几帧不应该出现的内容。因此 readScope() 返回空命中时,界面应停留在“需重新索引”,绝不把缓存当成临时占位图。arktsimport { preferences } from '@kit.ArkData';import { common } from '@kit.AbilityKit';interface CacheEnvelope { modelTag: string; albumRev: number; itemIds: string[]; savedAt: number;}export class CacheScopeStore { private store?: preferences.Preferences; async open(ctx: common.UIAbilityContext): Promise<void> { this.store = await preferences.getPreferences(ctx, { name: 'text_image_scope' }); } async readScope(modelTag: string, albumRev: number): Promise<string[] | null> { if (!this.store) { throw new Error('STORE_NOT_OPEN'); } const raw = await this.store.get('cache_envelope', ''); if (typeof raw !== 'string' || !raw) { return null; } try { const entry = JSON.parse(raw) as CacheEnvelope; if (entry.modelTag !== modelTag || entry.albumRev !== albumRev || !Array.isArray(entry.itemIds)) { return null; } return entry.itemIds.filter((id: string) => typeof id === 'string'); } catch (_) { return null; } }}````getPreferences()` 的 `Options` 重载与 `get()`、`put()`、`flush()` 均可在 ArkData 官方接口文档查到;本文不绑定臆造的 SDK 小版本。`get` 的缺省值是空串,返回 `ValueType`,所以先判断类型再解析。解析异常按失效处理,而不是将损坏的缓存继续送到渲染层。真正产品还应校验数组上限、ID 格式以及图库权限是否变化。注意这里没有在 `catch` 中悄悄把非法内容写回存储。一旦恢复流程也出错,连原始损坏现场都可能丢掉。调试时最好记录“字段校验失败”而不是完整缓存文本,避免把照片标识或用户查询词打进公开日志。## 三、重建提交采用一个值,并防止慢任务覆盖快任务另一个麻烦是时间顺序。模型升级后触发一次重建,用户马上删除了一张照片,图库修订号从 `42` 变为 `43`。较慢的旧任务稍后返回,如果不检查当前请求代次,它就能覆盖新结果。处理策略很明确:业务层发起一次任务就递增 `generation`,所有返回值必须带着启动时的代次回来。下段代码解决“慢结果覆盖”与“写入回执提前出现”的问题。`itemIds` 假定来自上游索引服务,本文不制造模型推理成功的假调用;`currentGeneration()` 由发起任务的页面或任务控制器提供。arktsasync function commitScope( pref: preferences.Preferences, modelTag: string, albumRev: number, itemIds: string[], requestGen: number, currentGeneration: () => number): Promise { if (requestGen !== currentGeneration()) { return false; } const snapshot: CacheEnvelope = { modelTag, albumRev, itemIds: […itemIds], savedAt: Date.now() }; await pref.put(‘cache_envelope’, JSON.stringify(snapshot)); await pref.flush(); return requestGen === currentGeneration();}这段代码把同一批结果放进单个值,再等待 `flush()`。返回布尔值决定界面是否发布 `REBUILT`,不等同于“系统具有跨操作事务”。尤其要注意:**末尾再次检查代次并不能保证跨两个并发写者的磁盘顺序**。生产版应在单写队列中串行执行这段提交,或者使用支持事务和条件更新的数据库。这里的代次主要解决 UI 不显示已过时的结果,不能被宣传成数据库并发控制方案。失效回收也不是直接 `clear()`。Preferences 文件里未来可能放入显示选项或日志开关,清空全文件会扩大误伤面。演示只保留键 `cache_envelope`,必要时调用 `delete('cache_envelope')` 并 `flush()`。更大规模的图片索引应改用关系型存储,避免序列化大数组造成同步成本。## 四、状态机写在屏幕上,便于复查这个 Demo 的页面只显示一次固定输入。启动先得到 `STALE_MODEL`,说明查到了十二个旧结果但禁止展示;随后上游返回九个新结果,业务提交成功才变为 `REBUILT`。两个状态之间不插入一个假的“命中成功”。日志只记录批次 ID、命中与否和代次,不记原始搜索语句。为避免读者把状态文本误认为系统内置枚举,下面的 `STALE_MODEL`、`REBUILT` 和 `generation` 都是业务字段。它们能帮助解释状态,并不对应某个 HarmonyOS 系统回调。arktsinterface SearchReply { generation: number; ids: string[]; }class SearchResultGate { private generation: number = 0; private pageActive: boolean = true; nextRequest(): number { return ++this.generation; } leavePage(): void { this.pageActive = false; ++this.generation; } accept(reply: SearchReply): string[] | null { if (!this.pageActive || reply.generation !== this.generation) { return null; } return reply.ids; }}// 演示: generation=3 对应当前任务;离开页面后旧任务返回会被忽略。```这不是靠“取消 Promise”实现的:后台计算可能继续完成,但不再拥有页面发布权。若上游索引库提供原生取消能力,应在页面离开时另外调用取消或释放接口。这里没有编造那种接口名。对于未被回收的异步请求,还需要在外层统一处理拒绝与超时,避免产生未捕获异常。
## 五、如何验证,而不是看一张成功截图我建议按三个阶段检查。先用固定 JSON 写入 txt_embed_v2 / 42 / 12条,退出应用并再次进入;页面应给出 STALE_MODEL,展示数为零。再切到 txt_embed_v3 / 42 / 9条,等待 flush() 后重启;如果没有改图库修订号,应允许读取九条。最后把 albumRev 改为 43,即使模型不变,也必须再次失效。再加入一份格式不完整的 JSON,确认页面不会崩溃。图中的 TXTIMG_20261017_06、generation=3、REBUILT 和“九条结果”均是静态演示数据。DevEco 界面是示意,HiLog 也是设计用日志;目前没有在真实设备运行此 Demo。这样标注不是客套,而是把业务逻辑可推导出的结果与设备性能、系统行为的真实验证分开。
最值得盯的是“进程重启后立刻显示了什么”。缓存治理问题往往表现为偶发错误,而不是持续崩溃。断网、图库权限撤销、删除资产、模型资源未下载完全、时间回拨都会影响这条链路。比如 savedAt 可以用于时效性审查,却不应该单靠设备墙上时间判断模型有效;核心仍是模型身份和图库修订。对于跨设备一致性,本方案也没有默认保证。## 六、把边界留在结论里这套实现回答的是:文搜图模型换版以后,旧缓存能否在错误的语义空间里继续显示。它让缓存读入前完成作用域匹配,让结果提交与 UI 状态发布分开,并用代次拦截页面已经放弃的结果。它不实现特征提取,不负责图片扫描,也不声称提升检索准确率。后续若接入真正的文搜图 SDK,应把真实索引版本、图库变更事件和权限状态纳入 readScope() 的失效条件,并用事务存储替代日益庞大的列表缓存。参考:华为官方 ArkData Preferences 接口文档(getPreferences、get、put、flush、delete):https://developer.huawei.com/consumer/en/doc/harmonyos-references/js-apis-data-preferences 。本文示例需结合目标 SDK 的类型定义再在 DevEco 中编译验证。
更多推荐



所有评论(0)