news 2026/8/25 19:09:16

HarmonyOS社交通讯应用开发 23: 分布式数据对象详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
HarmonyOS社交通讯应用开发 23: 分布式数据对象详解

分布式数据对象详解

引言

在上一篇《应用接续原理》中我们看到,发送端在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.etsonContinue中:

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数据已从分布式数据库恢复完成。这是接收端最关心的状态——只有收到它,才说明mainTitletextContentattachments等属性可以安全读取。

项目只在status === 'restored'时取数据,就是对"数据可用时机"的正确把握:online只代表"连上了",不代表"数据到了";restored才代表"数据到了"。如果业务在online时就读取对象属性,很可能读到空值,这是分布式数据对象最常见的踩坑点。

六、机制细节:属性级同步是怎么做到的

理解了 API,再往下一层看看同步机制,这对正确使用很有帮助。

属性即同步单元。分布式数据对象的每个字符串键都是一个独立属性。create时 source 里的字段会成为对象的初始属性;之后obj['key'] = valuedelete 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),让onContinuerestoreDistributedObject共享同一个对象引用;回调里还做了if (!this.distributedObject) return;判空。自己动手时别把对象声明成局部变量,否则回调里拿不到。
  • save 前确保数据就绪。发送端await save()等待落库;如果不等就返回AGREE,接收端可能先到restored却拿到不完整数据。接续对"最终一致"有容忍度,但首次还原的完整性还是尽量保证。

八、数据如何到达:同步链路一图流

把发送端和接收端串起来看,一次接续的数据同步链路是:

  1. 发送端genSessionId()生成会话 ID,写入wantParam
  2. 发送端create对象 →setSessionId入组 →save(targetDevice)持久化并推送。
  3. 系统把wantParam投递给目标设备,以CONTINUATION原因拉起应用。
  4. 接收端create空对象 → 从wantParam取出会话 ID →setSessionId入组。
  5. 两端对象合流:接收端收到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"的代码就顺理成章了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/25 19:07:07

HarmonyOS社交通讯应用开发 22 :权限申请机制

权限申请机制 引言 发布页能定位、能读剪贴板、能跨设备传文件&#xff0c;背后全靠权限支撑。HarmonyOS 的权限体系分为两类&#xff1a; system_grant&#xff08;系统授权&#xff09;&#xff1a;安装时静默授予&#xff0c;用户无感知&#xff0c;如访问网络&#xff1b;u…

作者头像 李华
网站建设 2026/8/25 18:51:35

Unity 2023 RPG游戏开发:从零搭建可交互的对话、任务与背包系统

1. 从零到一&#xff1a;一个完整RPG游戏的核心骨架是什么&#xff1f;如果你刚接触Unity&#xff0c;或者想从零开始做一个能玩、能分享的RPG游戏&#xff0c;最怕的不是代码有多难&#xff0c;而是不知道从哪里下手。网上教程要么太散&#xff0c;要么太深&#xff0c;做完一…

作者头像 李华
网站建设 2026/8/25 18:48:29

东华大学考研复试机试:动态规划、图论与字符串处理实战

1. 项目背景与核心价值作为一名计算机专业考研过来人&#xff0c;我深知东华大学复试机试环节的重要性。OJ&#xff08;Online Judge&#xff09;在线编程平台是检验考生算法能力和编码熟练度的关键战场&#xff0c;而"每日3题打卡"正是我去年备战期间总结出的高效训…

作者头像 李华
网站建设 2026/8/25 18:46:55

KeySteer:基于Windows原生OCR的全局键盘导航工具部署与实战指南

这次我们来看一个能让你用键盘“点击”屏幕上任意文字的工具——KeySteer。它不是一个传统的OCR软件&#xff0c;而是一个用Rust编写的、基于Windows原生OCR API的全局键盘导航工具。简单来说&#xff0c;它能把屏幕上任何区域的文字识别出来&#xff0c;然后你只需要按几个键&…

作者头像 李华
网站建设 2026/8/25 18:46:46

动手实测 Defuddle:拆解 Obsidian 创始人的网页内容提取引擎

动手实测 Defuddle&#xff1a;拆解 Obsidian 创始人的网页内容提取引擎本文所有内容均基于对 GitHub 源码的阅读和实际命令行测试&#xff0c;无任何厂家供稿或转载。从一次需求说起 上周在做一个知识库抓取工具时&#xff0c;需要把网页正文提取出来转成 Markdown。用了 Mozi…

作者头像 李华
网站建设 2026/8/25 18:40:59

AI Agent驱动开发实战:基于Cursor Origin的智能代码托管与自动化工作流

如果你最近关注AI编程工具&#xff0c;可能会发现一个现象&#xff1a;很多开发者开始讨论“AI Agent”和“代码托管平台”的结合。这背后其实是一个关键趋势&#xff1a; AI正在从“代码补全助手”向“自主执行任务的智能体&#xff08;Agent&#xff09;”演进&#xff0c;而…

作者头像 李华