鸿蒙跨设备协同开发08——使用分布式数据对象接续应用

87 阅读6分钟

1、前言

本文是基于鸿蒙跨设备协同开发07——动态控制应用接续的进一步讨论。

我们在鸿蒙跨设备协同开发06——应用接续中有提到:

  • 为了接续体验,在onContinue回调中使用wantParam传输的数据需要控制在100KB以下(大数据量使用分布式数据对象进行同步)

当我们遇到需要迁移的数据大于100KB时,则需要分布式数据对象进行同步了。

需要注意:

如果是API11及之前版本,实现分布式数据对象同步功能时,需要申请 ohos.permission.DISTRIBUTED_DATASYNC 权限,如果是API 12及以上时,无需申请此权限

2、分布式数据对象介绍

分布式数据对象提供管理基本数据对象的相关能力,包括创建、查询、删除、修改、订阅等;同时支持相同应用多设备间的分布式数据对象协同能力。

分布式数据对象由 @ohos.data.distributedDataObject 模块提供能力。导入模块代码如下:

import { distributedDataObject } from '@kit.ArkData';

核心的API有:

// 创建一个分布式数据对象
create(context: Context, source: object): DataObject

// 随机创建一个sessionId
genSessionId(): string

创建一个分布式数据对象和随机创建一个sessionId示例如下:

// 创建一个分布式数据对象
// 导入模块
import { UIAbility } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { window } from '@kit.ArkUI';

let g_object: distributedDataObject.DataObject|null = null;
class SourceObject {
    name: string
    age: number
    isVisboolean
    constructor(name: string, age: number, isVis: boolean) {
        this.name = name
        this.age = age
        this.isVis = isVis
    }
}

class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: window.WindowStage) {
        let source: SourceObject = new SourceObject("jack", 18, false);
        // 创建一个分布式数据对象
        g_object = distributedDataObject.create(this.context, source);
    }
}


// 创建一个随机sessionId
let sessionId: string = distributedDataObject.genSessionId();

分布式数据对象的使用核心还是在于DataObject,DataObject表示一个分布式数据对象。它主要的API有:

// 设置sessionId,可信组网中有多个设备处于协同状态时,如果多个设备间的分布式对象设置为同一个sessionId,就能自动同步。
// callback接口和Promise接口
setSessionId(sessionId: string, callback: AsyncCallback<void>): void
setSessionId(sessionId?: string): Promise<void>

// 退出所有已加入的session
setSessionId(callback: AsyncCallback<void>): void

// 监听/取消监听 分布式数据对象的数据变更。
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

// 保存分布式数据对象(callback回调和Promise版本)
/*
 对象数据保存成功后,当应用存在时不会释放对象数据,当应用退出后,重新进入应用时,恢复保存在设备上的数据。
 有以下几种情况时,保存的数据将会被释放:
 - 存储时间超过24小时。
 - 应用卸载。
 - 成功恢复数据之后。
  【deviceId为 local 时,表示存储在本地设备】
 */
save(deviceId: string, callback: AsyncCallback<SaveSuccessResponse>): void
save(deviceId: string): Promise<SaveSuccessResponse>

// 撤回保存的分布式数据对象(callback回调和Promise版本)
// 如果对象保存在本地设备,那么将删除所有受信任设备上所保存的数据。
// 如果对象保存在其他设备,那么将删除本地设备上的数据。
revokeSave(callback: AsyncCallback<RevokeSaveSuccessResponse>): void
revokeSave(): Promise<RevokeSaveSuccessResponse>

/*
 绑定分布式对象中的单个资产与其对应的数据库信息,当前版本只支持分布式对象中的资产与关系型数据库的绑定。使用callback方式异步回调。
 当分布式对象中包含的资产和关系型数据库中包含的资产指向同一个实体资产文件,即两个资产的Uri相同时,就会存在冲突,我们把这种资产称为融合资产。如果需要分布式数据管理进行融合资产的冲突解决,需要先进行资产的绑定。当应用退出session后,绑定关系随之消失。
 */
bindAssetStore(assetKey: string, bindInfo: BindInfo, callback: AsyncCallback<void>): void
bindAssetStore(assetKey: string, bindInfo: BindInfo): Promise<void>


⭐️ 以保存数据为例,示例代码如下:

g_object.setSessionId("123456");

g_object.save("local").then((result: distributedDataObject.SaveSuccessResponse) => {
    console.info("save callback");
    console.info("save sessionId " + result.sessionId);
    console.info("save version " + result.version);
    console.info("save deviceId " + result.deviceId);
}).catch((err: BusinessError) => {
    console.info("save failed, error code = " + err.code);
    console.info("save failed, error message: " + err.message);
});

⭐️ 以监听数据变化为例,示例代码如下:

// 删除数据变更回调changeCallback
g_object.off("change", (sessionId: string, fields: Array<string>) => {
    console.info("change" + sessionId);
    if (g_object != null && fields != null && fields != undefined) {
        for (let index: number = 0; index < fields.length; index++) {
            console.info("changed !" + fields[index] + " " + g_object[fields[index]]);
        }
    }
});
// 删除所有的数据变更回调
g_object.off("change");

3、使用分布式数据对象接续基础数据

使用分布式数据对象接续与普通的应用接续相似(详见之前的文章鸿蒙跨设备协同开发06——应用接续),需要在源端onContinue()接口中进行数据保存,并在对端的onCreate()/onNewWant()接口中进行数据恢复。

**👉🏻 step 1:**在源端UIAbility的onContinue()接口中创建分布式数据对象并保存数据,执行流程如下:

  1. 在onContinue()接口中使用create()接口创建分布式数据对象,将所要迁移的数据填充到分布式数据对象数据中。

  2. 调用genSessionId()接口生成数据对象组网id,并使用该id调用setSessionId()加入组网,激活分布式数据对象。

  3. 使用save()接口将已激活的分布式数据对象持久化,确保源端退出后对端依然可以获取到数据。

  4. 将生成的sessionId通过want传递到对端,供对端激活同步使用。

示例代码如下:

import distributedDataObject from '@ohos.data.distributedDataObject';
import UIAbility from '@ohos.app.ability.UIAbility';
import { BusinessError } from '@ohos.base';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 业务数据定义
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
  }
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
    // ...
    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
    let source: SourceObject = new SourceObject("jack", 18, false, parentSource);

    // 创建分布式数据对象
    this.d_object = distributedDataObject.create(this.context, source);

    // 生成数据对象组网id,激活分布式数据对象
    let dataSessionId: string = distributedDataObject.genSessionId();
    this.d_object.setSessionId(dataSessionId);

    // 将组网id存在want中传递到对端
    wantParam['dataSessionId'] = dataSessionId;

    // 数据对象持久化,确保迁移后即使应用退出,对端依然能够恢复数据对象
    // 从wantParam.targetDevice中获取到对端设备的networkId作为入参
    await this.d_object.save(wantParam.targetDevice as string).then((result:
      distributedDataObject.SaveSuccessResponse) => {
      hilog.info(DOMAIN_NUMBER, TAG, `Succeeded in saving. SessionId: ${result.sessionId},
        version:${result.version}, deviceId:${result.deviceId}`);
    }).catch((err: BusinessError) => {
      hilog.error(DOMAIN_NUMBER, TAG, 'Failed to save. Error: ', JSON.stringify(err) ?? '');
    });
  }
}

注意

  • 分布式数据对象需要先激活,再持久化,因此必须在调用setSessionId()后再调用save()接口。

  • 对于源端迁移后需要退出的应用,为了防止数据未保存完成应用就退出,应采用await的方式等待save()接口执行完毕。【从API 12 起,onContinue()接口提供了async版本供该场景使用】

  • 当前,wantParams中“sessionId”字段在迁移流程中被系统占用,建议在wantParams中定义其他key值存储该分布式数据对象生成的id,避免数据异常

**👉🏻 step 2:**在对端UIAbility的onCreate()/onNewWant()中,通过加入与源端一致的分布式数据对象组网进行数据恢复。

  1. 创建空的分布式数据对象,用于接收恢复的数据;

  2. 从want中读取分布式数据对象组网id;

  3. 注册on()接口监听数据变更。在收到status为restore的事件的回调中,实现数据恢复完毕时需要进行的业务操作。

  4. 调用setSessionId()加入组网,激活分布式数据对象。

实例代码如下:

import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import UIAbility from '@ohos.app.ability.UIAbility';
import type Want from '@ohos.app.ability.Want';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 示例数据对象定义与上同

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      // ...
      // 调用封装好的分布式数据对象处理函数
      this.handleDistributedData(want);
    }
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      if (want.parameters !== undefined) {
        // ...
        // 调用封装好的分布式数据对象处理函数
        this.handleDistributedData(want);
      }
    }
  }

  handleDistributedData(want: Want) {
    // 创建空的分布式数据对象
    let remoteSource: SourceObject = new SourceObject(undefined, undefined, undefined, undefined);
    this.d_object = distributedDataObject.create(this.context, remoteSource);

    // 读取分布式数据对象组网id
    let dataSessionId = '';
    if (want.parameters !== undefined) {
      dataSessionId = want.parameters.dataSessionId as string;
    }
    // 添加数据变更监听
    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
      hilog.info(DOMAIN_NUMBER, TAG, "status changed " + sessionId + " " + status + " " + networkId);
      if (status == 'restored') {
        if (this.d_object) {
          // 收到迁移恢复的状态时,可以从分布式数据对象中读取数据
          hilog.info(DOMAIN_NUMBER, TAG, "restored name:" + this.d_object['name']);
          hilog.info(DOMAIN_NUMBER, TAG, "restored parents:" + JSON.stringify(this.d_object['parent']));
        }
      }

    // 激活分布式数据对象
    this.d_object.setSessionId(dataSessionId);
  }
}

注意:

  • 对端加入组网的分布式数据对象不能为临时变量,因为在分布式数据对象on()接口为异步回调,可能在onCreate()/onNewWant()执行结束后才执行,临时变量被释放可能导致空指针异常。可以使用类成员变量避免该问题。
  • 对端用于创建分布式数据对象的Object,其属性应在激活分布式数据对象前置为undefined,否则会导致新数据加入组网后覆盖源端数据,数据恢复失败。
  • 应当在激活分布式数据对象之前,调用分布式数据对象的on()接口进行注册监听,防止错过restore事件导致数据恢复失败。

4、使用分布式数据对象接续文档/文件

对于图片、文档等文件类数据,需要先将其转换为资产commonType.Asset类型,再封装到分布式数据对象中进行迁移。迁移实现方式与普通的分布式数据对象类似,下面仅针对差异部分进行说明。

**👉🏻 step 1:**在源端,将需要迁移的文件资产保存到分布式数据对象DataObject中,执行流程如下:

  1. 将文件资产拷贝到分布式文件目录下,相关接口与用法详见基础文件接口。

  2. 使用分布式文件目录下的文件创建Asset资产对象。

  3. 将Asset资产对象作为分布式数据对象的根属性保存。

随后,与普通数据对象的迁移的源端实现相同,可以使用该数据对象加入组网,并进行持久化保存。示例如下:

// 导入模块
import distributedDataObject from '@ohos.data.distributedDataObject';
import UIAbility from '@ohos.app.ability.UIAbility';
import { BusinessError } from '@ohos.base';
import window from '@ohos.window';
import { fs, file } from '@ohos.file.fs';
import commonType from '@ohos.data.commonType';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

// 数据对象定义
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
  attachment: commonType.Asset | undefined  // 新增资产属性

  constructor(name: string | undefined, age: number | undefined, isVis: boolean | undefined,
    parent: ParentObject | undefined, attachment: commonType.Asset | undefined) {
    this.name = name
    this.age = age
    this.isVis = isVis
    this.parent = parent
    this.attachment = attachment;
  }
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): Promise<AbilityConstant.OnContinueResult> {
    // ...

    // 1. 将资产写入分布式文件目录下
    let distributedDir: string = this.context.distributedFilesDir;  // 获取分布式文件目录路径
    let fileName: string = '/test.txt';                        // 文件名
    let filePath: string = distributedDir + fileName;          // 文件路径

    try {
      // 在分布式目录下创建文件
      let file = fs.openSync(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
      hilog.info(DOMAIN_NUMBER, TAG, 'Create file success.');
      // 向文件中写入内容(若资产为图片,可将图片转换为buffer后写入)
      fs.writeSync(file.fd, '[Sample] Insert file content here.');
      // 关闭文件
      fs.closeSync(file.fd);
    } catch (error) {
      let err: BusinessError = error as BusinessError;
      hilog.error(DOMAIN_NUMBER, TAG, `Failed to openSync / writeSync / closeSync. Code: ${err.code}, message: ${err.message}`);
    }

    // 2. 使用分布式文件目录下的文件创建资产对象
    let distributedUri: string = fileUri.getUriFromPath(filePath); // 获取分布式文件Uri

    // 获取文件参数
    let ctime: string = '';
    let mtime: string = '';
    let size: string = '';
    await file.stat(filePath).then((stat: file.Stat) => {
      ctime = stat.ctime.toString();  // 创建时间
      mtime = stat.mtime.toString();  // 修改时间
      size = stat.size.toString();    // 文件大小
    })

    // 创建资产对象
    let attachment: commonType.Asset = {
      name: fileName,
      uri: distributedUri,
      path: filePath,
      createTime: ctime,
      modifyTime: mtime,
      size: size,
    }

    // 3. 将资产对象作为分布式数据对象的根属性,创建分布式数据对象
    let parentSource: ParentObject = new ParentObject('jack mom', 'jack Dad');
    let source: SourceObject = new SourceObject("jack", 18, false, parentSource, attachment);
    this.d_object = distributedDataObject.create(this.context, source);

    // 生成组网id,激活分布式数据对象,save持久化保存
    // ...
}

**👉🏻 step 2:**对端需要先创建一个各属性为空的Asset资产对象作为分布式数据对象的根属性。在接收到on()接口status为restored的事件的回调时,表示包括资产在内的数据同步完成,可以像获取基本数据一样获取到源端的资产对象。实例代码如下:

import UIAbility from '@ohos.app.ability.UIAbility';
import type Want from '@ohos.app.ability.Want';
const TAG: string = '[MigrationAbility]';
const DOMAIN_NUMBER: number = 0xFF00;

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  handleDistributedData(want: Want) {
    // ...
    // 创建一个各属性为空的资产对象
    let attachment: commonType.Asset = {
      name: '',
      uri: '',
      path: '',
      createTime: '',
      modifyTime: '',
      size: '',
    }

    // 使用该空资产对象创建分布式数据对象,其余基础属性可以直接使用undefined
    let source: SourceObject = new SourceObject(undefined, undefined, undefined, undefined, attachment);
    this.d_object = distributedDataObject.create(this.context, source);

    this.d_object.on("status", (sessionId: string, networkId: string, status: 'online' | 'offline' | 'restored') => {
        if (status == 'restored') {
          // 收到监听的restored回调,表示分布式资产对象同步完成
          hilog.info(DOMAIN_NUMBER, TAG, "restored attachment:" + JSON.stringify(this.d_object['attachment']));
        }
    });
    // ...
  }
}

注意:

对端创建分布式数据对象时,SourceObject对象中的资产不能直接使用undefined初始化,需要创建一个各属性为空的Asset资产对象,否则会导致资产同步失败。

如果我们想要同步多个资产,可采用两种方式实现:

  1. 可将每个资产作为分布式数据对象的一个根属性实现,适用于要迁移的资产数量固定的场景。

  2. 可以将资产数组传化为Object传递,适用于需要迁移的资产个数会动态变化的场景(如用户选择了不定数量的图片)。当前不支持直接将资产数组作为根属性传递。

其中方式1的实现可以直接参照添加一个资产的方式添加更多资产。方式2的示例如下所示:

// 导入模块
import distributedDataObject from '@ohos.data.distributedDataObject';
import UIAbility from '@ohos.app.ability.UIAbility';
import commonType from '@ohos.data.commonType';

// 数据对象定义
class SourceObject {
  name: string | undefined
  assets: Object | undefined  // 分布式数据对象的中添加一个Object属性

  constructor(name: string | undefined, assets: Object | undefined) {
    this.name = name
    this.assets = assets;
  }
}

// 该函数用于将资产数组转为Record
GetAssetsWrapper(assets: commonType.Assets): Record<string, commonType.Asset> {
  let wrapper: Record<string, commonType.Asset> = {}
  let num: number = assets.length;
  for (let i: number = 0; i < num; i++) {
    wrapper[`asset${i}`] = assets[i];
  }
  return wrapper;
}

export default class MigrationAbility extends UIAbility {
  d_object?: distributedDataObject.DataObject;

  async onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult {
    // ...

    // 创建了多个资产对象
    let attachment1: commonType.Asset = {
      // ...
    }

    let attachment2: commonType.Asset = {
      // ...
    }

    // 将资产对象插入资产数组
    let assets: commonType.Assets = [];
    assets.push(attachment1);
    assets.push(attachment2);

    // 将资产数组转为Record Object,并用于创建分布式数据对象
    let assetsWrapper: Object = this.GetAssetsWrapper(assets);
    let source: SourceObject = new SourceObject("jack", assetsWrapper);
    this.d_object = distributedDataObject.create(this.context, source);

    // ...
}