HarmonyOS 应用开发之分布式数据对象跨设备数据同步

场景介绍

传统方式下,设备之间的数据同步,需要开发者完成消息处理逻辑,包括:建立通信链接、消息收发处理、错误重试、数据冲突解决等操作,工作量非常大。而且设备越多,调试复杂度也将同步增加。

其实设备之间的状态、消息发送进度、发送的数据等都是“变量”。如果这些变量支持“全局”访问,那么开发者跨设备访问这些变量就能像操作本地变量一样,从而能够自动高效、便捷地实现数据多端同步。

分布式数据对象即实现了对“变量”的“全局”访问。向应用开发者提供内存对象的创建、查询、删除、修改、订阅等基本数据对象的管理能力,同时具备分布式能力。为开发者在分布式应用场景下提供简单易用的JS接口,轻松实现多设备间同应用的数据协同,同时设备间可以监听对象的状态和数据变更。满足超级终端场景下,相同应用多设备间的数据对象协同需求。与传统方式相比,分布式数据对象大大减少了开发者的工作量。

基本概念

  • 分布式内存数据库
    分布式内存数据库将数据缓存在内存中,以便应用获得更快的数据存取速度,不会将数据进行持久化。若数据库关闭,则数据不会保留。

  • 分布式数据对象
    分布式数据对象是一个JS对象型的封装。每一个分布式数据对象实例会创建一个内存数据库中的数据表,每个应用程序创建的内存数据库相互隔离,对分布式数据对象的“读取”或“赋值”会自动映射到对应数据库的get/put操作。

    分布式数据对象的生命周期包括以下状态:

    • 未初始化:未实例化,或已被销毁。
    • 本地数据对象:已创建对应的数据表,但是还无法进行数据同步。
    • 分布式数据对象:已创建对应的数据表,设备在线且组网内设置同样sessionId的对象数>=2,可以跨设备同步数据。若设备掉线或将sessionId置为空,分布式数据对象退化为本地数据对象。

运作机制

图1 分布式数据对象运作机制

分布式数据对象生长在分布式内存数据库之上,在分布式内存数据库上进行了JS对象型的封装,能像操作本地变量一样操作分布式数据对象,数据的跨设备同步由系统自动完成。

JS对象型存储与封装机制

  • 为每个分布式数据对象实例创建一个内存数据库,通过SessionId标识,每个应用程序创建的内存数据库相互隔离。

  • 在分布式数据对象实例化的时候,(递归)遍历对象所有属性,使用“Object.defineProperty”定义所有属性的set和get方法,set和get中分别对应数据库一条记录的put和get操作,Key对应属性名,Value对应属性值。

  • 在开发者对分布式数据对象进行“读取”或者“赋值”的时候,都会自动调用到set和get方法,映射到对应数据库的操作。

表1 分布式数据对象和分布式数据库的对应关系

分布式对象实例 对象实例 属性名称 属性值
分布式内存数据库 一个数据库(sessionID标识) 一条数据库记录的key 一条数据库记录的value

跨设备同步和数据变更通知机制

分布式数据对象,最重要的功能就是对象之间的数据同步。可信组网内的设备可以在本地创建分布式数据对象,并设置sessionID。不同设备上的分布式数据对象,通过设置相同的sessionID,建立对象之间的同步关系。

如下图所示,设备A和设备B上的“分布式数据对象1”,其sessionID均为session1,这两个对象建立了session1的同步关系。

图2 对象的同步关系

一个同步关系中,一个设备只能有一个对象加入。比如上图中,设备A的“分布式数据对象1”已经加入了session1的同步关系,所以设备A的“分布式数据对象2”就加入失败了。

建立同步关系后,每个Session有一份共享对象数据。加入了同一个Session的对象,支持以下操作:

(1)读取/修改Session中的数据。

(2)监听数据变更,感知其他设备对共享对象数据的修改。

(3)监听状态变更,感知其他设备的加入和退出。

同步的最小单位

关于分布式数据对象的数据同步,值得注意的是,同步的最小单位是“属性”。比如,下图中对象1包含三个属性:name、age和parents。当其中一个属性变更时,则数据同步时只需同步此变更的属性。

图3 数据同步视图

对象持久化缓存机制

分布式对象主要运行在应用程序的进程空间。当调用分布式对象持久化接口时,通过分布式数据库对对象进行持久化和同步,进程退出后数据也不会丢失。

该场景是分布式对象的扩展场景,主要用于以下情况:

  • 在设备上创建持久化对象后APP退出,重新打开APP,创建持久化对象,加入同一个Session,数据可以恢复到APP退出前的数据。

  • 在设备A上创建持久化对象并同步后持久化到设备B后,A设备的APP退出,设备B打开APP,创建持久化对象,加入同一个Session,数据可以恢复到A设备退出前的数据。

资产同步机制

在分布式对象中,可以使用资产类型来描述本地实体资产文件,分布式对象跨设备同步时,该文件会和数据一起同步到其他设备上。当前只支持资产类型,不支持资产类型数组。如需同步多个资产,可将每个资产作为分布式对象的一个根属性实现。

融合资产冲突解决机制

当分布式对象中包含的资产和关系型数据库中包含的资产指向同一个实体资产文件,即两个资产的Uri相同时,就会存在冲突,我们把这种资产称为融合资产。若想解决融合资产的冲突,需要先进行资产的绑定。当应用退出session后,绑定关系随之消失。

约束限制

  • 不同设备间只有相同bundleName的应用才能直接同步。

  • 分布式数据对象的数据同步发生在同一个应用程序下,且同sessionID之间。

  • 不建议创建过多的分布式数据对象,每个分布式数据对象将占用100-150KB内存。

  • 每个分布式数据对象大小不超过500KB。

  • 设备A修改1KB数据,设备B收到变更通知,50ms内完成。

  • 单个应用程序最多只能创建16个分布式数据对象实例。

  • 考虑到性能和用户体验,最多不超过3个设备进行数据协同。

  • 如对复杂类型的数据进行修改,仅支持修改根属性,暂不支持下级属性修改。资产同步机制中,资产类型的数据支持下一级属性修改。

  • 支持JS接口间的互通,与其他语言不互通。

接口说明

以下是分布式对象跨设备数据同步功能的相关接口,大部分为异步接口。异步接口均有callback和Promise两种返回形式,下表均以callback形式为例,更多接口及使用方式请见分布式数据对象。

接口名称 描述
create(context: Context, source: object): DataObject 创建并得到一个分布式数据对象实例。
genSessionId(): string 创建一个sessionId,可作为分布式数据对象的sessionId。
setSessionId(sessionId: string, callback: AsyncCallback<void>): void 设置同步的sessionId,当可信组网中有多个设备时,多个设备间的对象如果设置为同一个sessionId,就能自动同步。
setSessionId(callback: AsyncCallback<void>): void 退出所有已加入的session。
on(type: 'change', callback: (sessionId: string, fields: Array<string>) => void): void 监听分布式数据对象的数据变更。
off(type: 'change', callback?: (sessionId: string, fields: Array<string>) => void): void 取消监听分布式数据对象的数据变更。
on(type: 'status', callback: (sessionId: string, networkId: string, status: 'online' | 'offline' ) => void): void 监听分布式数据对象的上下线。
off(type: 'status', callback?: (sessionId: string, networkId: string, status: 'online' |'offline' ) => void): void 取消监听分布式数据对象的上下线。
save(deviceId: string, callback: AsyncCallback<SaveSuccessResponse>): void 保存分布式数据对象。
revokeSave(callback: AsyncCallback<RevokeSaveSuccessResponse>): void 撤回保存的分布式数据对象。
bindAssetStore(assetKey: string, bindInfo: BindInfo, callback: AsyncCallback<void>): void 绑定融合资产。

开发步骤

跨设备数据同步

以一次分布式数据对象同步为例,说明开发步骤。

  1. 导入@ohos.data.distributedDataObject模块。

    import distributedDataObject from '@ohos.data.distributedDataObject';
    
  2. 请求权限。

    1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限,配置方式请参见声明权限。
    2. 同时需要在应用首次启动时弹窗向用户申请授权,使用方式请参见向用户申请授权。
  3. 创建并得到一个分布式数据对象实例。

    Stage模型示例:

    // 导入模块
    import distributedDataObject from '@ohos.data.distributedDataObject';
    import UIAbility from '@ohos.app.ability.UIAbility';
    import { BusinessError } from '@ohos.base';
    import window from '@ohos.window';
    
    class ParentObject {
      mother: string
      father: string
    
      constructor(mother: string, father: string) {
        this.mother = mother
        this.father = father
      }
    }
    class SourceObject {
      name: string | undefined
      age: number | undefined
      isVis: boolean | undefined
      parent: Object | undefined
    
      constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined, parent: ParentObject | undefined) {
        this.name = name
        this.age = age
        this.isVis = isVis
        this.parent = parent
      }
    }
    
    class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage) {
        let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
        let source: SourceObject = new SourceObject("jack", 18, false, parentSource);
        let localObject: distributedDataObject.DataObject = distributedDataObject.create(this.context, source);
      }
    }
    

    FA模型示例:

    // 导入模块
    import distributedDataObject from '@ohos.data.distributedDataObject';
    import featureAbility from '@ohos.ability.featureAbility';
    // 获取context
    let context = featureAbility.getContext();
    class ParentObject {
      mother: string
      father: string
      constructor(mother: string, father: string) {
        this.mother = mother
        this.father = father
      }
    }
    class SourceObject {
      name: string | undefined
      age: number | undefined
      isVis: boolean | undefined
      parent: ParentObject | undefined
      constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined, parent: ParentObject | undefined) {
        this.name = name
        this.age = age
        this.isVis = isVis
        this.parent = parent
      }
    }
    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
    let source: SourceObject = new SourceObject("jack", 18, false, parentSource);
    // 创建对象,该对象包含4个属性类型:string、number、boolean和Object
    let localObject: distributedDataObject.DataObject = distributedDataObject.create(context, source);
    
  4. 加入同步组网。同步组网中的数据对象分为发起方和被拉起方。

    // 设备1加入sessionId
    let sessionId: string = '123456';
    
    localObject.setSessionId(sessionId);
    
    // 和设备1协同的设备2加入同一个session
    
    // 创建对象,该对象包含4个属性类型:string、number、boolean和Object
    let remoteSource: SourceObject = new SourceObject(undefined, undefined, undefined, undefined);
    let remoteObject: distributedDataObject.DataObject = distributedDataObject.create(this.context, remoteSource);
    // 收到status上线后remoteObject同步数据,即name变成jack,age变成18
    remoteObject.setSessionId(sessionId);
    
  5. 监听对象数据变更。可监听对端数据的变更,以callback作为变更回调实例。

    localObject.on("change", (sessionId: string, fields: Array<string>) => {
      console.info("change" + sessionId);
      if (fields != null && fields != undefined) {
        for (let index: number = 0; index < fields.length; index++) {
          console.info(`The element ${localObject[fields[index]]} changed.`);
        }
      }
    });
    
  6. 修改对象属性,对象属性支持基本类型(数字类型、布尔类型、字符串类型)以及复杂类型(数组、基本类型嵌套等)。

    localObject["name"] = 'jack1';
    localObject["age"] = 19;
    localObject["isVis"] = false;
    let parentSource1: ParentObject = new ParentObject('jack1 mom', 'jack1 Dad');
    localObject["parent"] = parentSource1;
    

    说明:

    针对复杂类型的数据修改,目前仅支持对根属性的修改,暂不支持对下级属性的修改。

// 支持的修改方式
let parentSource1: ParentObject = new ParentObject('mom', 'Dad');
localObject["parent"] = parentSource1;
// 不支持的修改方式
localObject["parent"]["mother"] = 'mom';
  1. 访问对象。可以通过直接获取的方式访问到分布式数据对象的属性,且该数据为组网内的最新数据。

    console.info(`name:${localObject['name']}`); 
    
  2. 删除监听数据变更。可以指定删除监听的数据变更回调;也可以不指定,这将会删除该分布式数据对象的所有数据变更回调。

    // 删除变更回调
    localObject.off('change', (sessionId: string, fields: Array<string>) => {
      console.info("change" + sessionId);
      if (fields != null && fields != undefined) {
        for (let index: number = 0; index < fields.length; index++) {
          console.info("changed !" + fields[index] + " " + localObject[fields[index]]);
        }
      }
    });
    // 删除所有的变更回调
    localObject.off('change'); 
    
  3. 监听分布式数据对象的上下线。可以监听对端分布式数据对象的上下线。

    localObject.on('status', (sessionId: string, networkId: string, status: 'online' | 'offline') => {
      console.info("status changed " + sessionId + " " + status + " " +  networkId);
      // 业务处理
    });
    
  4. 保存和撤回已保存的数据对象。

    // 保存数据对象,如果应用退出后组网内设备需要恢复对象数据时调用
    localObject.save("local").then((result: distributedDataObject.SaveSuccessResponse) => {
      console.info(`Succeeded in saving. SessionId:${result.sessionId},version:${result.version},deviceId:${result.deviceId}`);
    }).catch((err: BusinessError) => {
      console.error(`Failed to save. Code:${err.code},message:${err.message}`);
    });
    
    // 撤回保存的数据对象
    localObject.revokeSave().then((result: distributedDataObject.RevokeSaveSuccessResponse) => {
      console.info(`Succeeded in revokeSaving. Session:${result.sessionId}`);
    }).catch((err: BusinessError) => {
      console.error(`Failed to revokeSave. Code:${err.code},message:${err.message}`);
    });
    
  5. 删除监听分布式数据对象的上下线。可以指定删除监听的上下线回调;也可以不指定,这将会删除该分布式数据对象的所有上下线回调。

    // 删除上下线回调
    localObject.off('status', (sessionId: string, networkId: string, status: 'online' | 'offline') => {
      console.info("status changed " + sessionId + " " + status + " " + networkId);
      // 业务处理
    });
    // 删除所有的上下线回调
    localObject.off('status');
    
  6. 退出同步组网。分布式数据对象退出组网后,本地的数据变更对端不会同步。

    localObject.setSessionId(() => {
      console.info('leave all session.');
    });
    

跨设备资产同步

分布式对象中加入资产类型属性,可以触发资产同步机制,将资产类型属性所描述的文件同步到其他设备。持有资产文件的设备为发起端,得到资产文件的设备为接收端。

  1. 导入@ohos.data.distributedDataObject@ohos.data.commonType模块。

    import distributedDataObject from '@ohos.data.distributedDataObject';
    import commonType from '@ohos.data.commonType';
    
  2. 请求权限。

    1. 需要申请ohos.permission.DISTRIBUTED_DATASYNC权限。
    2. 同时需要在应用首次启动时弹窗向用户申请授权。
  3. 发起端创建包含资产的分布式对象并加入组网。

    import UIAbility from '@ohos.app.ability.UIAbility';
    import type window from '@ohos.window';
    import distributedDataObject from '@ohos.data.distributedDataObject';
    import commonType from '@ohos.data.commonType';
    import type { BusinessError } from '@ohos.base';
    
    class Note {
      title: string | undefined
      text: string | undefined
      attachment: commonType.Asset | undefined
    
      constructor(title: string | undefined, text: string | undefined, attachment: commonType.Asset | undefined) {
        this.title = title;
        this.text = text;
        this.attachment = attachment;
      }
    }
    
    class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage) {
        let attachment: commonType.Asset = {
          name: 'test_img.jpg',
          uri: 'file://com.example.myapplication/data/storage/el2/distributedfiles/dir/test_img.jpg',
          path: '/dir/test_img.jpg',
          createTime: '2024-01-02 10:00:00',
          modifyTime: '2024-01-02 10:00:00',
          size: '5',
          status: commonType.AssetStatus.ASSET_NORMAL
        }
        // 创建一个自定义笔记类型,其中包含一张图片资产
        let note: Note = new Note('test', "test", attachment);
        let localObject: distributedDataObject.DataObject = distributedDataObject.create(this.context, note);
        localObject.setSessionId('123456');
      }
    }
    
  4. 接收端创建分布式对象并加入组网

    let note: Note = new Note(undefined, undefined, undefined);
    let receiverObject: distributedDataObject.DataObject = distributedDataObject.create(this.context, note);
    receiverObject.on('change', (sessionId: string, fields: Array<string>) => {
      if (fields.includes('attachment')) {
        // 接收端监听到资产类型属性的数据变更时,代表其所描述的资产文件同步完成
        console.info('attachment synchronization completed');
      }
    });
    receiverObject.setSessionId('123456');
    
  5. 若资产为融合资产,可以创建绑定信息,绑定融合资产,以解决融合资产的冲突。

    const bindInfo: distributedDataObject.BindInfo = {
      storeName: 'notepad',
      tableName: 'note_t',
      primaryKey: {
        'uuid': '00000000-0000-0000-0000-000000000000'
      },
      field: 'attachment',
      assetName: attachment.name
    }
    
    localObject.bindAssetStore('attachment', bindInfo, (err: BusinessError) => {
      if (err) {
        console.error('bindAssetStore failed.');
      }
      console.info('bindAssetStore success.');
    });
    

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:https://gitee.com/MNxiaona/733GH/blob/master/jianshu
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容