分布式数据对象详解
引言
在上一篇《应用接续原理》中我们看到,发送端在onContinue里做了一件关键的事:把编辑现场包装成一个对象,调用distributedDataObject.create创建、setSessionId绑定会话、save(targetDevice)同步到目标设备。这个对象就是本文的主角——分布式数据对象(distributedDataObject)。它承担着"把标题、正文、位置、附件描述等结构化数据从设备 A 搬到设备 B"的核心职责,是本工程接续数据同步的"主通道"。
很多初学者容易把接续想成"把整个界面截图发过去",或者"把数据库整库复制过去"。实际不是这样。HarmonyOS 给出的答案是:一个可以跨设备自动同步的对象。你像操作本地对象一样读写它的属性,系统在后台替你完成属性级同步。本文就从"是什么、怎么用、本项目怎么用"三个层面把它讲透。
一、什么是分布式数据对象
分布式数据对象是 HarmonyOS 分布式数据管理(Data Management)提供的一种数据同步抽象。它把一组键值属性封装成一个"对象",多个设备上的多个对象只要会话 ID(sessionId)一致,就会被组织进同一个会话组,组内任意一端的属性变化都会自动同步到组内其他端。
它和传统数据方案的关键区别:
- 对象即接口:不用写 SQL、不用拼 JSON、不用关心网络传输,读写属性即可,
obj.title = 'xxx'这样的代码在远端设备上同样生效。 - 属性级同步:粒度到单个属性,改了哪个字段就同步哪个字段,不是整表整库搬运。
- 基于分布式数据库(RDB)落地:数据会持久化到各端的分布式数据库中,设备离线后再上线,
restored事件会把历史数据补回来,因此它天然适合"接续"这种"先落库、再还原"的场景。
当然它也有边界:同步的数据应当可序列化(基本类型、字符串、以及可被序列化的对象数组等),不适合承载大体积二进制。所以本工程把图片、视频的实体文件交给分布式文件系统(第 26 篇),分布式数据对象只同步文本字段与附件的描述信息(Asset)——各司其职,这个"双通道"设计是接续示例的标准姿势。
HarmonyOS 的分布式数据能力其实是一族方案,选型时容易混淆,放在一起对比更清晰:
| 方案 | 形态 | 适用场景 | 本工程的角色 |
|---|---|---|---|
| 分布式数据对象 | 内存对象,属性级同步 | 接续、实时协同、状态同步 | 主通道:正文与附件描述 |
| 分布式键值库(KV) | 键值数据库,持久化 | 配置同步、简单状态 | 未使用 |
| 分布式关系型数据库(RDB) | 关系表,结构化查询 | 复杂业务数据、多表关联 | 未使用 |
| 分布式文件系统 | 文件目录同步 | 大文件、媒体、文档 | 附件通道:图片/视频实体 |
为什么接续场景优先选分布式数据对象?因为接续的数据是"一份现场快照",形态天然是一个对象(本工程的 ContentInfo),而对象 API 读写最贴合"打包现场 → 还原现场"的心智;同时restored状态事件又能精确回答"数据什么时候可用"——这是 KV/RDB 方案需要额外轮询或回调才能获得的语义。
二、核心 API 全景
用分布式数据对象通常走四步:创建 → 绑会话 → 监听状态 → 保存/还原。对应到本工程:
| 步骤 | API | 作用 |
|---|---|---|
| 创建 | distributedDataObject.create(context, source) | 用初始数据 source 创建对象 |
| 绑会话 | genSessionId()/setSessionId(id) | 生成/加入会话组 |
| 监听 | obj.on('status', callback) | 感知 online/offline/restored 状态 |
| 保存 | obj.save(deviceId) | 持久化并同步到指定设备 |
其中create的第一个参数是UIAbilityContext(本工程传this.context),第二个参数是初始数据对象。source可以是任意可序列化对象,创建后obj会以 source 的属性作为自己的属性,之后对obj属性的增删改都会触发同步。
关于create的返回值再补充两点。其一,create返回的DataObject类型在distributedDataObject.DataObject下(项目里声明为private distributedObject: distributedDataObject.DataObject | undefined),它同时具备"属性容器"与"事件源"两种身份——既是读写属性的对象,又是on('status')的注册主体,理解这个双重身份有助于读懂两端代码。其二,create是同步返回的,save/setSessionId才是异步操作(返回 Promise),所以项目里create直接赋值、setSessionId/save用.catch收尾错误,节奏分明。初学者最容易犯的错是把create当异步 await——实际上它立即返回可用的对象。
三、发送端:创建、绑会话、保存
先看发送端(设备 A)的完整使用,代码在entry/src/main/ets/entryability/EntryAbility.ets的onContinue中:
asynconContinue(wantParam:Record<string,Object|undefined>):Promise<AbilityConstant.OnContinueResult> {// 生成全局唯一的会话 ID,并随 want 带给目标设备letsessionId:string= distributedDataObject.genSessionId(); wantParam.distributedSessionId= sessionId;// ... 组装 assets、ContentInfo 等(详见第 27 篇)...// 1. 创建分布式数据对象,source 是压平后的 ContentInfoletsource = contentInfo.flatAssets();this.distributedObject= distributedDataObject.create(this.context, source);// 2. 把对象加入 sessionId 对应的会话组this.distributedObject.setSessionId(sessionId).catch((err: BusinessError) =>{ hilog.info(DOMAIN,TAG,FORMAT,`SetSessionId failed. Cause code:${err.code}, message:${err.message}`); });// 3. 保存:把数据持久化到分布式数据库,并同步到目标设备awaitthis.distributedObject.save(wantParam.targetDeviceasstring).catch((err: BusinessError) =>{ hilog.info(DOMAIN,TAG,FORMAT,`Failed to save. Code:${err.code}, message:${err.message}`); });returnAbilityConstant.OnContinueResult.AGREE; }三个关键点:
genSessionId()的妙用。会话 ID 是发送端和接收端"接头"的暗号。发送端生成后必须把它放进wantParam,因为接收端只能通过 want 参数拿到它(接收端代码里是want.parameters?.distributedSessionId)。注意不要自己拼字符串当会话 ID——genSessionId()保证全局唯一,避免不同会话之间串数据。
setSessionId让对象"入组"。一个对象创建后还没有归属,只有setSessionId之后才加入会话组。发送端和接收端各自创建一个对象、各自调用setSessionId(同一个 ID),两个对象就成了"一对",属性互相镜像。官方文档称之为"加入会话组"。
save(targetDevice)是"持久化 + 定向同步"。参数targetDevice是系统接续时在wantParam里预填的目标设备标识。save会把对象数据写进本端分布式数据库,并主动向目标设备推送一份。注意它返回 Promise,项目用await等待落库完成,确保onContinue返回AGREE时数据已经就绪,接收端"即刻可还原"。
四、接收端:创建、绑会话、监听状态
接收端(设备 B)的使用在restoreDistributedObject中,代码同样位于EntryAbility.ets:
asyncrestoreDistributedObject(want:Want,launchParam:AbilityConstant.LaunchParam):Promise<void> {if(launchParam.launchReason!==AbilityConstant.LaunchReason.CONTINUATION) {return;// 普通启动直接返回,不做接续还原}// 1. 用空 ContentInfo 创建对象(数据稍后从远端同步过来)letmailInfo:ContentInfo=newContentInfo(undefined,undefined, [],undefined,undefined,undefined,undefined);this.distributedObject= distributedDataObject.create(this.context, mailInfo);// 2. 注册状态监听:online / offline / restored 三态try{this.distributedObject.on('status',(sessionId:string, networkId:string, status:'online'|'offline'|'restored') =>{ hilog.info(DOMAIN,TAG,FORMAT,`status changed, sessionId:${sessionId}`); hilog.info(DOMAIN,TAG,FORMAT,`status changed, status:${status}`); hilog.info(DOMAIN,TAG,FORMAT,`status changed, networkId:${networkId}`);if(status ==='restored') {// 数据已恢复,取回各属性(详见第 25 篇)... } }); }catch(err) { ... }// 3. 从 want 中取会话 ID 并加入同一会话组letsessionId:string= want.parameters?.distributedSessionIdasstring;this.distributedObject.setSessionId(sessionId).catch((err: BusinessError) =>{ hilog.info(DOMAIN,TAG,FORMAT,`SetSessionId failed. Cause code:${err.code}, message:${err.message}`); });// 4. 记录接续后要恢复的页面letcurrContinuePageUrl = want.parameters?.currContinuePageUrlasstring;AppStorage.setOrCreate<string>(CommonConstants.CONTINUE_PAGE_URL, currContinuePageUrl); }与发送端对称地看:发送端用"实数据"创建对象,接收端用"空壳"创建对象;发送端save主动推,接收端只setSessionId等着收。两边对象属性会自动合流——这正是分布式数据对象的魔力:你不需要手写任何网络传输代码,只要把会话 ID 对上,数据自己会"流"过来。
五、status 事件:online / offline / restored
on('status')是接收端感知数据进度的唯一窗口,回调携带三个参数:sessionId(会话标识)、networkId(对端设备网络标识)、status(状态)。状态有三种:
- **
online**:对象加入会话组成功、与对端建立了连接。此时数据可能还没同步完,界面不宜立刻依赖数据。 - **
offline**:与对端的连接断开(如设备离开、网络切换)。接续场景下应用应做好"数据暂时拿不到"的兜底。 restored:数据已从分布式数据库恢复完成。这是接收端最关心的状态——只有收到它,才说明mainTitle、textContent、attachments等属性可以安全读取。
项目只在status === 'restored'时取数据,就是对"数据可用时机"的正确把握:online只代表"连上了",不代表"数据到了";restored才代表"数据到了"。如果业务在online时就读取对象属性,很可能读到空值,这是分布式数据对象最常见的踩坑点。
六、机制细节:属性级同步是怎么做到的
理解了 API,再往下一层看看同步机制,这对正确使用很有帮助。
属性即同步单元。分布式数据对象的每个字符串键都是一个独立属性。create时 source 里的字段会成为对象的初始属性;之后obj['key'] = value或delete obj['key']都会在本地生效并异步同步到会话组内的其他端。同步是属性粒度的:发送端改了mainTitle,只同步mainTitle这一个键,其他键不受影响。这带来一个推论:键的命名要稳定。发送端写mainTitle,接收端必须读mainTitle,一旦拼写不一致,两端数据就对不上。本工程用ContentInfo的字段名统一约束了键名(第 27 篇),从源头杜绝了拼写漂移。
数据类型的限制。属性值支持基本类型、字符串、可序列化的对象与数组。这正是第 27 篇flatAssets()把"附件数组"展开成attachments0/1/2...的原因之一:数组本身能同步,但把数组元素拆成独立属性后,每个附件可以独立地作为同步单元被管理,粒度更细、冲突更少。而PixelMap这类内存对象不能直接作为属性值——所以媒体实体走分布式文件系统,对象里只放 Asset 描述。
冲突处理。当会话组内多个端同时修改同一属性时,分布式数据管理有一套冲突消解策略(如时间戳裁决),保证各端最终收敛到一致。对本工程而言,接续是"一端写、一端读"的单向流,几乎不存在并发写冲突,这也是接续场景用分布式数据对象非常顺手的另一个原因——不需要复杂的冲突处理代码。
七、常见问题与注意事项
结合本项目与常见踩坑,整理几条使用要点:
- 会话 ID 必须取自 wantParam。接收端的
setSessionId参数只能来自want.parameters?.distributedSessionId,不能自己新生成——生成一个新的 ID 就等于加入了一个发送端不在的会话组,数据永远到不了。发送端则必须把 ID 写进wantParam,这是两端唯一的"接头暗号"。 - 监听要先于 setSessionId。接收端的正确顺序是:先
on('status')注册监听,再setSessionId入组。若顺序颠倒,入组瞬间可能已经触发restored,而监听尚未就位,事件就丢了。本工程restoreDistributedObject严格遵循"先监听、后入组"。 - 对象是 Ability 的成员变量。项目把
distributedObject声明为EntryAbility的私有成员(private distributedObject: distributedDataObject.DataObject | undefined),让onContinue与restoreDistributedObject共享同一个对象引用;回调里还做了if (!this.distributedObject) return;判空。自己动手时别把对象声明成局部变量,否则回调里拿不到。 - save 前确保数据就绪。发送端
await save()等待落库;如果不等就返回AGREE,接收端可能先到restored却拿到不完整数据。接续对"最终一致"有容忍度,但首次还原的完整性还是尽量保证。
八、数据如何到达:同步链路一图流
把发送端和接收端串起来看,一次接续的数据同步链路是:
- 发送端
genSessionId()生成会话 ID,写入wantParam。 - 发送端
create对象 →setSessionId入组 →save(targetDevice)持久化并推送。 - 系统把
wantParam投递给目标设备,以CONTINUATION原因拉起应用。 - 接收端
create空对象 → 从wantParam取出会话 ID →setSessionId入组。 - 两端对象合流:接收端收到
restored状态,属性全部可用。
整个过程,应用层只写了约二十行代码,其余全部由分布式数据管理框架完成——跨设备组网、加密传输、数据库落盘、断点续传,都藏在create/setSessionId/save/on这四个 API 背后。这也是为什么官方推荐"接续用分布式数据对象":它把跨设备同步的复杂度收敛成了对象属性操作。
值得一提的细节是:发送端onContinue里还通过wantParam.mediaUriArray = JSON.stringify(...)放了一份媒体列表的轻量备份,接收端即使还没等到restored,也能先拿到一份媒体描述信息兜底。这是分布式数据对象"异步到达"特性下常见的防御性设计。
九、调试与验证方法
分布式同步是黑盒,出问题时怎么定位?本工程其实埋好了三个抓手,初级开发者可以直接复用:
第一,hilog 日志。项目在关键节点都打了日志:发送端save前后、setSessionId失败、接收端status changed的会话 ID/状态/网络 ID、以及attachments的 JSON 内容。用 DevEco Studio 的 Log 面板按EntryAbility过滤,可以完整还原一次接续的同步轨迹——先确认两端会话 ID 一致,再确认接收端收到了restored,然后核对attachments内容是否与发送端一致。
第二,status 三态的观测顺序。正常接续时接收端日志应依次出现online→(数据合流)→restored。如果只看到online没有restored,说明会话组数据没有同步过来——优先检查发送端save是否成功、targetDevice是否有效;如果连online都没有,说明setSessionId失败或设备没有组网,回到第 23 篇讲的前提条件排查。
第三,restored只触发一次的陷阱。status事件中的restored语义是"数据从数据库恢复完成",通常在入组后触发一次;之后若远端又有新属性变更,并不会重新触发restored,而是直接合入对象属性。所以本工程把"取数"放在restored里是"取初始现场";如果业务需要持续感知远端修改,应另行监听属性变化事件,并注意与restored的语义区分——这是分布式数据对象进阶使用最容易混淆的点。
小结
分布式数据对象是本工程接续数据同步的"主通道",核心心智模型是"对象即数据、会话即通道":create把数据包装成对象,genSessionId+setSessionId把两端对象归入同一会话组,save持久化并定向同步,on('status')让接收端在restored状态确认数据可用。它擅长同步可序列化的结构化数据(本工程的标题、正文、位置开关、附件描述),但不适合直接承载大文件——那些交给分布式文件系统。理解了对象与会话这两个概念,下一篇《接续数据还原》中接收端"从对象里取回 6 个字段写进 AppStorage"的代码就顺理成章了。