分布式数据对象详解引言在上一篇《应用接续原理》中我们看到发送端在onContinue里做了一件关键的事把编辑现场包装成一个对象调用distributedDataObject.create创建、setSessionId绑定会话、save(targetDevice)同步到目标设备。这个对象就是本文的主角——分布式数据对象distributedDataObject。它承担着把标题、正文、位置、附件描述等结构化数据从设备 A 搬到设备 B的核心职责是本工程接续数据同步的主通道。很多初学者容易把接续想成把整个界面截图发过去或者把数据库整库复制过去。实际不是这样。HarmonyOS 给出的答案是一个可以跨设备自动同步的对象。你像操作本地对象一样读写它的属性系统在后台替你完成属性级同步。本文就从是什么、怎么用、本项目怎么用三个层面把它讲透。一、什么是分布式数据对象分布式数据对象是 HarmonyOS 分布式数据管理Data Management提供的一种数据同步抽象。它把一组键值属性封装成一个对象多个设备上的多个对象只要会话 IDsessionId一致就会被组织进同一个会话组组内任意一端的属性变化都会自动同步到组内其他端。它和传统数据方案的关键区别对象即接口不用写 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:Recordstring,Object|undefined):PromiseAbilityConstant.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.etsasyncrestoreDistributedObject(want:Want,launchParam:AbilityConstant.LaunchParam):Promisevoid {if(launchParam.launchReason!AbilityConstant.LaunchReason.CONTINUATION) {return;// 普通启动直接返回不做接续还原}// 1. 用空 ContentInfo 创建对象数据稍后从远端同步过来letmailInfo:ContentInfonewContentInfo(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.setOrCreatestring(CommonConstants.CONTINUE_PAGE_URL, currContinuePageUrl); }与发送端对称地看发送端用实数据创建对象接收端用空壳创建对象发送端save主动推接收端只setSessionId等着收。两边对象属性会自动合流——这正是分布式数据对象的魔力你不需要手写任何网络传输代码只要把会话 ID 对上数据自己会流过来。五、status 事件online / offline / restoredon(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把数据包装成对象genSessionIdsetSessionId把两端对象归入同一会话组save持久化并定向同步on(status)让接收端在restored状态确认数据可用。它擅长同步可序列化的结构化数据本工程的标题、正文、位置开关、附件描述但不适合直接承载大文件——那些交给分布式文件系统。理解了对象与会话这两个概念下一篇《接续数据还原》中接收端从对象里取回 6 个字段写进 AppStorage的代码就顺理成章了。