
本文使用 MetaDock 演示工程页面MetadataPage任务MD-2011。12 条IMG-001至IMG-012是人工生成的元数据记录不访问真实相册界面、HiLog 与回执只用于说明线程间协议并非 DevEco 编译记录或真实设备日志。一、日志里先出现了一个不该出现的 16我在准备离线相册索引的演示页时故意把总任务量设成 12 条每次从 Worker 回传 4 条理论上只会收到块0、1、2。如果接收方简单地在每次onmessage里执行count chunk.length一旦块1因业务重试被重复投递页面就会从 12 变成 16。看上去像是“Worker 多算了”其实错在宿主线程没有约定消息身份。这不是浏览器式的线程共享数组问题。ArkTS Worker 通过消息通信传递可序列化数据宿主线程通常拿到的是消息内容而不是一个可任意共享修改的 UI 状态容器。Worker 的职责是生产分块页面的职责是识别当次会话可以接收哪些分块。两个职责如果只靠“收到一条就加一次”连接结果必然依赖偶然的投递顺序。我给案例取名 MetaDock把范围限制在MD-2011一次索引任务。会话号固定为session8上一轮session7的消息即使迟到也必须抛弃块大小是 4三个合法块完成后indexed12/12接收端允许块1再到一次但不能让计数变成 16。场景足够小才能把“合并”与“只是显示进度”这两个动作彻底分开。二、用会话号和块号建立协议不靠时间猜新旧一条块消息需要携带session、chunkIndex、items结束消息需要携带总条数。不要在协议里只放百分比百分比不知道哪些项已经真正合并更没法判断重复投递。chunkIndex是 0、1、2 这种确定的整数session是宿主线程分配的代次不是墙上时钟也不是Date.now()的随机替代品。接收端使用Setnumber记录已接受块的编号。协议的另一半是输入的稳定 ID。用IMG-001这样的人工主键而非数组下标后续即使改成按文件路径排序或分批补扫也能区分“同一条又来一次”和“新条目恰好占用了旧位置”。真实项目里的文件标识应考虑权限、相册更新、路径暴露与隐私脱敏不能直接把本例的字符串拼接当成生产级资源定位。先明确消息类型与一个纯合并器。以下代码放到entry/ets/common/MetaProtocol.ets供 Worker 和页面分别导入。该函数只变更普通内存结构不在 Worker 回调里直接拼 UI如果块号重复或会话不符就返回 false。为简化示例输入只存 ID 和像素宽度避免无意义地牵涉图像解码 API。export interface ImageMeta { id: string; width: number; } export interface MetaChunk { type: string; session: number; chunkIndex: number; items: ImageMeta[]; } export class ChunkMerger { private readonly session: number; private seen: Setnumber new Setnumber(); private received: Mapstring, ImageMeta new Mapstring, ImageMeta(); constructor(session: number) { this.session session; } accept(chunk: MetaChunk): boolean { if (chunk.type ! chunk || chunk.session ! this.session || chunk.chunkIndex 0 || this.seen.has(chunk.chunkIndex)) { return false; } this.seen.add(chunk.chunkIndex); chunk.items.forEach((item: ImageMeta): void { if (item.id.length 0) { this.received.set(item.id, item); } }); return true; } count(): number { return this.received.size; } }这里Map还保护了同一块内部的重复 ID。注意计数来自去重后的键数而非消息里宣称的数量真正可上架的索引服务还应校验块号范围、ID 格式、宽高上限和 total 的可信来源。演示合并器只完成理解 Worker 协议所需的最小工作故意没有实现真正的相册权限申请和文件读取。三、Worker 负责分块取消指令要能插进处理流程ThreadWorker 创建以后宿主线程用postMessage发出 start 指令。Worker 接受的输入是一个快照而不是 UI 线程的State数组。为了让取消消息有机会被处理演示里的分块发送采用setTimeout分次推进而不是一口气用很长的同步循环占满 Worker 事件循环。这不是性能保证只是避免取消指令连被处理的机会都没有。下面位于entry/ets/workers/MetadataWorker.ets的示例展示消息协议。12 条记录是演示输入块大小固定为 4生产环境不应该一次性将大批媒体内容复制进消息而应分批传必要元数据。取消会话时清理计时器之后的 pump 不能再继续发出块。import { MessageEvents, ThreadWorkerGlobalScope, worker } from kit.ArkTS; import { ImageMeta } from ../common/MetaProtocol; const port: ThreadWorkerGlobalScope worker.workerPort; let activeSession: number -1; let timerId: number -1; port.onmessage (event: MessageEvents): void { const message event.data as { type: string; session: number }; if (message.type cancel) { if (message.session activeSession) { activeSession -1; if (timerId 0) { clearTimeout(timerId); timerId -1; } } return; } if (message.type ! start) { return; } if (timerId 0) { clearTimeout(timerId); } activeSession message.session; const ids: ImageMeta[] Array.from({ length: 12 }, (_, index: number): ImageMeta ({ id: IMG-${String(index 1).padStart(3, 0)}, width: 4000 })); let nextIndex: number 0; const pump (): void { if (message.session ! activeSession) { return; } if (nextIndex 3) { port.postMessage({ type: done, session: activeSession, total: 12 }); timerId -1; return; } port.postMessage({ type: chunk, session: activeSession, chunkIndex: nextIndex, items: ids.slice(nextIndex * 4, (nextIndex 1) * 4) }); nextIndex 1; timerId setTimeout(pump, 0); }; timerId setTimeout(pump, 0); };分块发送不等于文件处理本身已经完成。这里的done只表示演示消息生产结束能否在页面显示“已索引”还要看宿主线程是否完整收齐三个块。setTimeout时间间隔在系统调度下没有实时性保证中途页退场时页面仍需主动终止 Worker而不能仅靠发送 cancel。示例中引用的 Worker 文件路径必须与项目配置一致不能直接复制到没有 worker 构建配置的工程就期待自动运行。四、宿主线程收到消息先验身份再改可见计数接收侧代码是entry/ets/pages/MetadataPage.ets组件中的状态成员和生命周期片段外层Entry Component struct MetadataPage及build结构按项目模板补齐。接收侧的陷阱在于 UI 状态变化得很直观日志打一行、进度加四看起来一直正常。真正应该先执行的是merger.accept返回 true 以后才能读取新的唯一 ID 数量。老会话的回执和重复块都应该安静地消失不参与当前状态。为了方便读者复现我把三块的进度固定为4/12、8/12、12/12。import { MessageEvents, worker } from kit.ArkTS; import { ChunkMerger, MetaChunk } from ../common/MetaProtocol; State indexed: number 0; State status: string 等待开始; private readonly session: number 8; private merger: ChunkMerger new ChunkMerger(8); private indexWorker: worker.ThreadWorker | null null; private leaving: boolean false; aboutToAppear(): void { this.leaving false; this.indexWorker new worker.ThreadWorker(entry/ets/workers/MetadataWorker.ets); this.indexWorker.onmessage (event: MessageEvents): void { if (this.leaving) { return; } const payload event.data as MetaChunk; if (payload.type chunk this.merger.accept(payload)) { this.indexed this.merger.count(); this.status 已索引 ${this.indexed}/12; } }; this.status 索引中 · session 8; this.indexWorker.postMessage({ type: start, session: this.session }); } aboutToDisappear(): void { this.leaving true; if (this.indexWorker ! null) { this.indexWorker.postMessage({ type: cancel, session: this.session }); this.indexWorker.terminate(); this.indexWorker null; } }注意onmessage只处理chunk演示页的完成提示由三个块都被合并之后派生没有直接使用 Worker 的done覆盖进度。如果生产任务需要重新启动必须换一个新的 session并创建对应的合并器旧Set不能沿用。线程的创建和终止并不是可以随便反复触发的轻操作批量列表的长期运行场景还应根据任务时长选择 TaskPool 或复用受控 Worker避免让每一个卡片都开一个线程。五、固定的三块消息比随机压测更容易定位错误先发送session8的第 0 块接收端显示4/12紧接着是第 1 块显示8/12。这时手工重放第 1 块界面必须保持8/12。最后送达第 2 块显示12/12再投递session7的旧块即使其中包含四条新的字符串也不应该改变结果。这个脚本能检验的是消息合并逻辑不是 Worker 消息队列的真实顺序或机器性能。演示消息理论唯一条数页面可见状态session 8 / chunk 04已索引 4/12session 8 / chunk 18已索引 8/12session 8 / chunk 1 重放8维持 8/12session 8 / chunk 212已索引 12/12session 7 / chunk 2 迟到12维持 12/12手机屏幕示意选择最终的12/12显示任务MD-2011与会话8并列出“重复块 1 已忽略”和“旧会话 7 已忽略”两个结果标签。顶部时间、5G、Wi-Fi 和电池数字都是构图内容不是设备采样值。图像与文章写同一组数字是为了读者能一眼对照验证路径而不是用“看上去很真”的截图给出不存在的测试证据。六、不要把线程结束当成状态已经提交Worker 生命周期比单次消息更长组件生命周期却会因为页面退场中断。aboutToDisappear的示例既标记leaving也执行terminate()前者是 UI 回写门禁后者是线程资源收口。因为关闭是异步环境下的系统行为界面仍不应依赖“Terminate 一调用就绝无任何旧回调”的推测。请求取消与结果拒绝要做成两道防线。如果要把这个最小例子改成真实相册索引首先要增加权限判断、目录和媒体更新监听、异常重试上限、分块大小背压以及隐私合规处理。尤其不要把文件全路径与拍摄位置直接放进诊断日志。Map的增长也需要配额、分页持久化或分阶段释放不能因为本例只有 12 条就忽略大图集的内存上界。另外当 Worker 报错或异常退出应给用户可恢复的失败状态而不是把indexed停在一半却仍显示任务运行中。七、把结论收在协议上而不是收在“用了多线程”回过头看导致 16/12 的原因不是 Worker 本身不可靠而是接收端缺少会话代次和已处理块集合。MetaDock 用session8、chunkIndex0..2、固定块大小 4 和稳定图片 ID把重复块与旧任务隔离开最终计数只能由真正合并过的 12 个唯一 ID 派生。这样的协议在接入真实元数据扫描之前就能用纯逻辑测试验证减少把业务错误误归因于线程调度的时间。本篇参考华为官方《使用Worker处理关联的同步任务》2026-08及《Worker向宿主线程进行消息插队通信》2026-09-09https://developer.huawei.com/consumer/cn/doc/doccenter-capabilities/sync-task-development https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/worker-postmessageatfront 。示例只使用普通postMessage不依赖 API 26 的新插队能力完整 SDK 编译、worker 配置验证和真机压力测试仍需开发者在目标工程执行。