ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 Media Library Kit + RDB:文搜图增量索引的变更游标、删除墓碑与断点补偿【鸿蒙心迹】

HarmonyOS 7 Media Library Kit + RDB:文搜图增量索引的变更游标、删除墓碑与断点补偿【鸿蒙心迹】 文搜图 Demo 跑到第二周最先暴露的不是识别准确率而是一个很朴素的问题用户已经删掉的照片搜索结果里为什么还在刚编辑过的照片又为什么始终搜不到。我最早的实现很直白。应用启动后读取相册给每张图生成标签和向量再把结果写进本地数据库。相册只有两百多张图时这种全量重建没什么压力。测试机放进六千张素材以后每次冷启动都重新扫描页面要等很久端侧模型也在重复处理没有变化的图片。于是我把它改成“按修改时间查询增量”。速度确实下来了新的问题也跟着出现修改时间只能找到新增和编辑过的资产已经从图库删除的图片不会回来告诉我“我被删了”。只做增量查询本地索引会越来越像一个只进不出的仓库。本篇使用的 Demo 叫PhotoDelta Lab。这次同步任务固定为sync_20261001_12起始游标v3148共发现 27 项变化其中新增 6 项、更新 18 项、删除 3 项。运行页面在 15:18 显示APPLYING / 24 of 27 / 88%最后写入 24 条有效索引和 3 条删除墓碑再把游标推进到v3175。这里讨论的不是文搜图模型怎么调用而是模型前面那层经常被忽略的数据工程如何知道该处理哪些图、如何让删除生效、任务中断后从哪里继续以及旧回调为什么不能覆盖新一轮同步。一、全量扫描的问题不只是慢文搜图索引通常至少保存四类数据图库资产标识、修改时间、可搜索文本或标签、向量与缩略图缓存。全量重建意味着每次都要重新打开 PixelMap、执行预处理、调用视觉能力并写数据库。耗时只是表面真正难处理的是任务运行期间图库仍可能发生变化。比如全量扫描到第 3500 张时用户删除了前面已经处理过的一张图。如果本轮任务没有变更边界最终数据库仍会写回这张图。下一次启动又要重新扫一遍才能纠正索引状态在两个全量任务之间并不可靠。当前实现给每轮同步分配独立的syncId并把“读取图库变化”和“提交索引变化”分成两个阶段。读取阶段固定上界提交阶段只处理本轮产生的变更集。图库后续发生的变化留给下一轮不在运行中不断扩张任务范围。同步状态不是一个isLoading而是IDLE - SCANNING - RECONCILING - APPLYING - COMMITTING - COMPLETED \- FAILEDSCANNING读取新增和修改资产RECONCILING对比当前图库资产集合与本地索引APPLYING生成文本与向量COMMITTING在同一事务里写索引、墓碑和新游标。只有事务提交成功页面才显示完成。二、修改时间游标要留出回看窗口如果简单保存“上一次看到的最大修改时间”下一轮用大于号查询很容易漏掉同一时间粒度内的多张照片。不同接口返回的时间单位也要确认不能把毫秒游标直接拿去和秒字段比较。我现在保存两部分信息逻辑游标v3148用于任务追踪底层还保存modifiedAfterMs。查询时把时间向前回看两秒再由本地的assetKey modifiedAt去重。这样会重复读到少量资产却不会因为时间边界漏图。下面这段代码解决的是“增量查询漏掉同一时间片资产”的问题。它只负责读取候选项不在这里直接更新最终游标。import{photoAccessHelper}fromkit.MediaLibraryKitimport{dataSharePredicates}fromkit.ArkDataimporttype{common}fromkit.AbilityKitexportinterfaceDeltaAsset{uri:stringdisplayName:stringmodifiedAtMs:number}exportasyncfunctionqueryChangedAssets(context:common.UIAbilityContext,cursorMs:number):PromiseDeltaAsset[]{consthelperphotoAccessHelper.getPhotoAccessHelper(context)constpredicatesnewdataSharePredicates.DataSharePredicates()constrewindSecondsMath.floor((cursorMs-2000)/1000)predicates.greaterThan(photoAccessHelper.PhotoKeys.DATE_MODIFIED,rewindSeconds)constoptions:photoAccessHelper.FetchOptions{fetchColumns:[photoAccessHelper.PhotoKeys.URI,photoAccessHelper.PhotoKeys.DISPLAY_NAME,photoAccessHelper.PhotoKeys.DATE_MODIFIED],predicates}constresultawaithelper.getAssets(options)constassets:DeltaAsset[][]try{letassetawaitresult.getFirstObject()while(asset){assets.push({uri:asset.uri,displayName:asset.displayName,modifiedAtMs:Number(asset.get(photoAccessHelper.PhotoKeys.DATE_MODIFIED))*1000})assetawaitresult.getNextObject()}}finally{result.close()}returnassets}FetchResult必须关闭。当前 Demo 的候选项只有几十条不关闭可能暂时看不出问题同步变成周期任务后游标、查询结果和文件描述符都会积累。正式项目还要根据授权范围决定可见资产集合不能默认应用能看到用户图库中的全部内容。这里的asset.uri用作本地关联键不把图片内容复制进 RDB。索引表只保存必要的元数据、标签、向量引用和状态。真正读取图片时还要处理资产已经不可访问、云端内容尚未下载以及格式变化等情况。三、删除只能通过集合核对找出来修改时间查询永远不会返回已经不存在的资产。要删除本地索引必须定期做一次集合核对拿当前可见的资产键集合与本地处于ACTIVE的索引集合比较。只在本地存在的键写成TOMBSTONE。为什么不立即物理删除因为索引可能还有缩略图缓存、向量文件和正在展示的搜索结果。先写墓碑可以让搜索查询立刻过滤它同时把真正的文件清理由低优先级任务完成。如果删除动作中途失败下次仍能从墓碑继续而不是重新猜测哪些文件应该移除。这段代码解决的是“相册删除后搜索结果残留”的问题。墓碑写入和索引更新放在同一事务避免只更新一半。exportasyncfunctionreconcileAndCommit(store:relationalStore.RdbStore,syncId:string,currentAssetKeys:Setstring,changed:IndexDraft[],nextCursor:SyncCursor):PromiseSyncSummary{awaitstore.beginTransaction()try{constactiveKeysawaitIndexDao.listActiveAssetKeys(store)constdeletedKeysactiveKeys.filter(key!currentAssetKeys.has(key))for(constkeyofdeletedKeys){awaitIndexDao.markTombstone(store,key,syncId)}for(constdraftofchanged){awaitIndexDao.upsertActive(store,draft,syncId)}awaitSyncDao.saveCursor(store,nextCursor)awaitstore.commit()return{indexed:changed.length,tombstones:deletedKeys.length}}catch(error){awaitstore.rollBack()throwerror}}在sync_20261001_12中新增和更新一共 24 项集合核对找到 3 个只存在于本地索引的资产因此页面显示“有效索引 24、删除墓碑 3”。如果事务提交失败v3148不会推进。下一次任务仍会重新看到这 27 项变化代价是重复计算一部分换来的是不会丢变更。墓碑也有边界。用户只是暂时撤销权限或云图库不可用时当前可见集合可能突然变小这不应该被解释为大规模删除。PhotoDelta Lab 设置了保护阈值当可见资产数量相对上次快照下降超过 30%任务进入SUSPENDED提示检查权限与媒体库可用性不批量写墓碑。四、中断恢复不能只记“处理了 24 张”我曾经只保存processedCount。任务处理到 24/27 后被系统回收重新进入时从第 25 项继续。问题是候选数组每次查询顺序可能不同“第 25 项”没有稳定含义前 24 项也可能已经发生修改。当前检查点记录变更集摘要、已完成的资产键以及本轮固定上界。恢复时先验证摘要再跳过已完成键。变更集不一致就放弃热恢复回到SCANNING重新构建候选项但仍保留已经生成且版本匹配的向量缓存。下面的协调器解决的是“旧任务回调覆盖新任务状态”和“重复点击启动两轮同步”的问题。generation用来隔离过期回调inFlight把多个入口合并为同一轮任务。exportclassDeltaSyncCoordinator{privategeneration:number0privateinFlight?:PromiseSyncSummarystate:SyncStateSyncState.IDLEstart(request:SyncRequest):PromiseSyncSummary{if(this.inFlight)returnthis.inFlightconstcurrentthis.generationthis.inFlightthis.run(request,current).finally((){if(currentthis.generation)this.inFlightundefined})returnthis.inFlight}privateasyncrun(request:SyncRequest,generation:number):PromiseSyncSummary{this.stateSyncState.SCANNINGconstdeltaawaitthis.reader.read(request.cursor)this.assertCurrent(generation)this.stateSyncState.RECONCILINGconstplanawaitthis.planner.build(delta)this.assertCurrent(generation)this.stateSyncState.APPLYINGconstdraftsawaitthis.indexer.apply(plan.changed,request.checkpoint)this.assertCurrent(generation)this.stateSyncState.COMMITTINGreturnthis.repository.commit(plan,drafts)}privateassertCurrent(value:number):void{if(value!this.generation)thrownewError(STALE_SYNC_GENERATION)}}页面销毁时不直接把数据库事务当作页面资源释放。同步控制器归属于应用级仓库页面只是订阅进度页面退出后可以停止 UI 更新但已经进入COMMITTING的短事务应完成。模型推理与 PixelMap 则要遵循各自生命周期取消后及时释放不能因为索引任务“还能后台跑”就长期持有大图。图中的工程把GalleryDeltaReader、TombstonePlanner、DeltaSyncCoordinator和 RDB DAO 分开。右侧模拟器停在APPLYING底部日志固定显示sync_20261001_12、cursor v3148、24/27与tombstones3。这时看到卡顿可以直接判断发生在索引生成而不是图库查询或事务提交。五、运行结果要能解释“27 项变化”最终运行页没有只显示一个 88% 进度条而是把这轮变化拆开新增 6、更新 18、删除 3已处理 24/27当前游标从v3148准备推进到v3175。这样即使页面停在 88%开发者也知道剩下 3 项不是三张待识别照片而是等待提交的墓碑。15:18 的运行日志如下[PhotoDelta] syncsync_20261001_12 cursorv3148 stateSCANNING [PhotoDelta] added6 updated18 deleted3 total27 [PhotoDelta] stateAPPLYING processed24/27 progress88% [PhotoDelta] commit active24 tombstones3 nextCursorv3175提交完成后搜索层只查询stateACTIVE的索引记录三条墓碑立即从结果中消失。清理任务随后删除对应缩略图与向量文件再把墓碑标记为PURGED。如果清理失败用户看不到已删除照片磁盘任务则可以稍后重试。六、正式项目还需要守住的边界第一授权范围变化不等于删除。资产可见性突然下降时暂停核对重新确认权限和 Media Library 可用状态。第二修改时间不是内容版本的绝对证明。某些编辑可能生成新资产也可能更新原资产索引键、修改时间和内容摘要要共同决定是否复用缓存。第三游标必须与事务一起提交。先推进游标再写索引一旦崩溃就会永久跳过变化先写索引但游标未提交最多重复执行结果仍可通过幂等 upsert 收敛。第四图库变更监听适合触发“需要同步”不适合直接在回调里跑模型。短时间多次变更应合并页面前台、定时检查和图库通知最终都进入同一个协调器。第五搜索结果也要防旧数据。用户正在查看结果时资产被写入墓碑详情页再次读取前要核对状态不能只相信进入页面时传来的对象。七、文搜图真正难的是索引一直可信从全量扫描改成增量同步代码量并没有减少反而多了游标、事务、墓碑、检查点和回调隔离。可一旦把这些状态拆清楚模型调用就变成整条链路中最稳定的一段。实际用下来我更在意的不是一次同步省了多少秒而是用户新增、编辑、删除图片以后搜索结果能否在下一轮准确收敛。sync_20261001_12的 27 项变化都能解释失败后也知道从v3148重来还是从检查点恢复这才是文搜图从展示 Demo 走向长期可用功能的分界线。参考资料Media Library KitPhotoAccessHelper 概述Media Library KitPhotoAccessHelper 类型参考ArkData关系型数据库开发
返回列表