这次遇到的问题不是“同步失败”,而是同步成功以后页面刷得太勤。

KvPulseGuardLab 是一个跨设备笔记 Demo,手机和平板共享 note_sync_v2。最初每收到一次远端 dataChange,页面就重新查询一次 note/。数据量小时没感觉,后来一次编辑会同时改标题、正文、更新时间、标签和版本号,同步到本机后短时间连续出现多次变化通知,页面就开始反复 query 和重建列表。

我最后把链路改成三层:dataChange 只收集变化 Key;120 ms 窗口合并连续事件;真正刷新以前再检查业务版本水位,旧结果不能覆盖新结果。

本次固定数据:Session kv_watch_20261001_19,Store note_sync_v2,Subscribe REMOTE,Raw Events 14,Effective Refresh 3,Coalesced 11,Changed Keys 6,Last Version 42,Query Rows 6,Active Listeners 1,State COALESCED。

一、dataChange 只当变化信号,不直接当刷新命令

DistributedKVStore 的 ChangeNotification 会提供 insert、update、delete 条目。我的第一版代码在回调里直接刷新 UI,相当于底层一次同步拆成多少通知,页面就刷新多少次。

现在 observer 只做采集:

import { distributedKVStore } from '@kit.ArkData'

private store?: distributedKVStore.SingleKVStore
private changedKeys: Set<string> = new Set()
private rawEvents: number = 0
private activeListeners: number = 0

private readonly onRemoteChange = (
  change: distributedKVStore.ChangeNotification
): void => {
  this.rawEvents++

  const entries = [
    ...change.insertEntries,
    ...change.updateEntries,
    ...change.deleteEntries
  ]

  entries.forEach(item => this.changedKeys.add(item.key))
  this.scheduleRefresh()
}

attach(store: distributedKVStore.SingleKVStore): void {
  this.store = store

  store.on(
    'dataChange',
    distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE,
    this.onRemoteChange
  )

  this.activeListeners = 1
}

这里专门订阅 SUBSCRIBE_TYPE_REMOTE,因为页面本地编辑已经由自己的 ViewModel 更新,不需要再绕一圈远端刷新链。

本地、远端都要监听时,更适合拆成两条管线。

二、120 ms 的作用,是让同一批远端同步先“落稳”

我用的是尾触发 debounce:120 ms 内只要还有新 dataChange,就继续延后真正查询。

private refreshTimer: number = -1
private coalesced: number = 0
private effectiveRefresh: number = 0
private lastVersion: number = 0

private scheduleRefresh(): void {
  if (this.refreshTimer !== -1) {
    clearTimeout(this.refreshTimer)
    this.coalesced++
  }

  this.refreshTimer = setTimeout(() => {
    this.refreshTimer = -1
    void this.flushChangedKeys()
  }, 120)
}

private async flushChangedKeys(): Promise<void> {
  if (!this.store || this.changedKeys.size === 0) {
    return
  }

  const versionValue = await this.store.get('meta/version')
  const version = Number(versionValue.value)

  if (version <= this.lastVersion) {
    this.changedKeys.clear()
    return
  }

  const rows = await this.store.getEntries('note/')

  this.lastVersion = version
  this.effectiveRefresh++
  this.publishRowsToView(rows)
  this.changedKeys.clear()
  this.state = 'COALESCED'
}

最终 14 次原始事件只触发 3 次有效刷新,因此 Coalesced=11。

120 ms 不是系统规定,只是当前 Demo 的工程参数。关键点是把“数据通知频率”和“UI 刷新频率”拆开。

版本水位同样很重要。第 41 版 query 如果比第 42 版更晚返回,页面不能被旧结果覆盖。这里的 meta/version 是业务自己的页面水位,不是 DistributedKVStore 的自定义冲突策略。

三、Changed Keys 还能继续帮我们缩小查询范围

这次只查询 note/ 前缀下 6 条记录。数据规模更大时,应把 insert / update / delete 分开做局部刷新。

需要注意,deleteEntries 中的 Key 已经不存在,不能再无脑调用 get(key)。这也是为什么我保留 ChangeNotification 的三类条目,而不是只维护一个普通字符串数组。

四、页面退场必须把 observer 和 debounce timer 一起回收

回调风暴还有一种假象:实际上是页面进出几次后重复注册了 listener。

DistributedKVStore 提供成对的 on/off。我的解绑逻辑同时清 observer、timer 和临时 Key 集合:

detach(): void {
  if (!this.store) {
    return
  }

  this.store.off(
    'dataChange',
    this.onRemoteChange
  )

  if (this.refreshTimer !== -1) {
    clearTimeout(this.refreshTimer)
    this.refreshTimer = -1
  }

  this.changedKeys.clear()
  this.activeListeners = 0
}

只 off listener 还不够。页面退出前已经挂起的 120 ms timer 仍可能醒来继续 query,所以 timer 也必须一起停。

如果此时已经有一次异步 query 在执行,还应再加 page generation,防止迟到结果写回已销毁页面。

五、调试时我只看“原始事件”和“真实刷新”的差值

工程拆成 KvPulsePage.ets、KvChangeCoalescer.ets、VersionWatermark.ets 和 NoteSnapshot.ets。

HiLog 固定保留:subscribe REMOTE listener=1、dataChange raw=14、collect changedKeys=6、coalesced=11 debounce=120ms、query prefix=note/ rows=6、lastVersion=42。

六、最终结果不是“同步成功”,而是 14 次事件只刷新 3 次

运行页里:

  • Raw Events:14
  • Effective Refresh:3
  • Coalesced:11
  • Changed Keys:6
  • Last Version:42
  • Query Rows:6
  • Active Listeners:1

Raw Events=14 证明底层变化确实很多;Effective Refresh=3 证明 UI 没跟着抖;Active Listeners=1 又能排除重复订阅。

七、从 Demo 到正式项目还要补什么

多设备同时编辑时,业务版本规则仍要单独设计;observer 也应集中管理,不要让每个卡片各订一份。

最后,跨设备同步和 UI 刷新是两回事。DistributedKVStore 负责把数据带到本机,dataChange 负责告诉应用“有变化”,而“什么时候刷、一次刷多少、旧结果能不能覆盖新结果”仍然是应用自己的工程职责。

最终链路固定为:

REMOTE_EVENT → BUFFERING → QUERYING → COALESCED

拆开这些层以后,跨设备同步就变成可量化、可限流、可验收的刷新管线。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐