布耗!对面是炸鱼的!!快让我的18岁舍友直接帮我拿下对局——如何用HarmonyOS鸿蒙操作系统实现自由流转
自由流转
- 引言
- 自由流转的定义与体验
- 自由流转应用开发的挑战
- 连接复杂
- 传统操作系统平台开发一个分布式应用工作量大
- 自由流转应用开发框架
- 分布式软总线技术架构
- 分布式数据管理
- 解决跨端硬件共享问题
- 分布式任务调度
- 系统抽象和交互逻辑
- 跨端迁移运作机制
- 具体开发步骤
- 为应用配置基础迁移功能
- 应用动态运行时申请用户授权
- 实现简单接口
- 分布式数据传输
- 使用组件迁移数据
- 页面栈迁移
- 少量状态数据迁移
- 内存数据迁移
- 分布式文件传输
- 跨端迁移开发总结
引言
自由流转,乍一看难以望文生义,我们加一个定语,多设备,多设备的自由流转就容易理解了。众所周知,HarmonyOS是面向万物互联的全场景分布式操作系统。所以在HarmonyOS的应用支持在手机、平板、智能穿戴、智慧屏等多种终端设备上运行,因此我们需要考虑应用在多设备的交互体验
自由流转的定义与体验
经过人机交互的实验发现,多设备的交互场景按照时间的串、并行分为以下两种情况
- 串行交互:指用户相继使用多个设备此类交互场景要求具备较高的连续性和一致性。
- 连续性是指当用户从一个设备转型另一个设备的时候,最新的操作状态应该是继续保留的、未被中断的。
- 一致性是指当用户在使用手机、手表、大屏等不同设备的时候,交互方式与基础视觉元素应当是一致的(例如多指手势、控件样式等)。这里的一致并不等同于相同,由于设备的屏幕尺寸、形态的不同,视觉元素可以经过适配的调整。
- 并行交互:指用户同时使用多个设备,此类交互场景要求具备较高的并发性、协作性和互补性。
- 并发性是指某一业务在多个设备同时呈现。
- 协作性是指多个设备彼此交互协调完成一项任务
- 互补性是指利用设备本身形态差异完成一项任务
对于并行交互和串行交互两种典型场景,HarmonyOS分布式运行环境分别提供了与之对应的基础能力,即跨段迁移和多端协同。两者统称为自由流转
自由流转应用开发的挑战
连接复杂
第一个挑战就是连接复杂,近场设备之间的互联互通挑战,相比计算总线是硬件总线能力是确定的并且稳定的。而近场通信是无限链接,其质量依赖无线通信能力,带宽受限易受干扰。
- 不同的终端蓝牙、WIFI、芯片不同,不同厂商的芯片、驱动、协议版本不同,环境差异对通信的影响不同,如蓝牙是直线传播好,空旷的客厅传播好,而拥挤的厨房和阳台传播不好。芯片、环境、协议、兼容性等不可控因素下,如何通过软件定义通信,保证近场设备的互联互通是核心的挑战。
- 其次数据难以互通,如何从设备A,访问设备B的数据,多设备协同操作时,如何知道数据在哪台设备的哪个应用上,还有不同设备类型意味着不同的传感能力、硬件能力、屏幕尺寸还有设备的硬件能力差异。如小设备没有摄像头、NFC等,设备间的差异导致难以协同
传统操作系统平台开发一个分布式应用工作量大
基于传统的操作系统开发跨设备交互的应用程序时,需要开发者自行解决设备发现、设备认证、设备连接、数据同步等技术难题。还要考虑不同的设备是否支持某些硬件能力,不但开发成本高,还存在安全隐私、兼容性、性能等诸多问题
自由流转应用开发框架
刚才介绍了要去开发一个能在多设备上交互的应用,难度很大。而HarmonyOS的设计理念是把复杂的处理交给系统,让消费者操作更便捷、更高效、更符合用户场景。同时让开发者开发更简单、更高效,提升开发者效率
上图是传统单机应用开发模型,依赖应用之间互相通信去完成发现、连接、传输与数据的互通,而这一部分工作往往是要开发者去考虑的。
上图是HarmonyOS应用中的开发模型,其为开发者提供了基础的分布式框架能力,这里列出了几个主要的部分。
- 分布式软总线:解决多设备发现、连接、组网痛点。
- 分布式数据提:供跨设备数据访问能力
- 分布式硬件:提供跨设备硬件共享访问能力
- 分布式任务调度:提供应用跨端迁移、多端协同能力
依靠这些关键部分,让各类智能设备构成一个超级终端设备,将复杂的发现、连接、传输、安全等都交给系统。对于上层应用,只需要简单的配置与接口调用就能进行多设备间的数据交互
分布式软总线技术架构
- 分布式软总线:通过协议货架和软硬协同,屏蔽各种设备的协议差别,提供高吞吐、低时延、高可靠、安全可信的通信通道,克服无线通信不可靠、不稳定的挑战,为开发者提供接近本地化访问效果的通信能力。
- 总线中枢模块:负责解析命令完成设备间的发现和连接,通过任务和数据两条总线实现设备间文件传输、消息传输等功能
- 分布式软总线从逻辑架构上分为四大组成部分,分别为发现、连接、组网、传输四大模块。在整个软总线业务逻辑中分工合作,通过构筑分布式通信框架,达成多设备互联互通的目标
总结
简单来讲,分布式软总线核心的技术点主要是自动发现和异构组网
- 自动发现:实现用户零等待的自发现体验,可信设备自动发现无需等待,自动安全连接。无需用户任何操作
- 异构网络组网:可以很好解决设备间的不同协议如何交互的问题,蓝牙和WiFi混合组网,自动构建一个逻辑全连接网络。解决设备间不同协议交互的问题
分布式数据管理
分布式数据管理基于分布式软总线的能力,实现应用程序数据和用户数据的分布式管理,用户数据不再与单一的物理设备绑定,业务逻辑和数据存储分离。跨设备的数据处理如同本地数据处理一样方便快捷。让开发者能够轻松实现全场景多设备下的数据存储、共享和访问
分布式数据管理的核心部分包括分布式数据库和分布式数据对象
根据跨设备同步数据生命周期的不同,可以分为两个:
- 持久数据生命周期比较长,需要保存到存储的数据库当中,根据数据关系、和特点。可以选择关系型数据库或键值型数据库。
- 临时数据生命周期比较短,通常保存在内存中,比如游戏应用产生的过程数据,建议使用分布式数据对象
解决跨端硬件共享问题
在传统操作系统中,设备间相互独立,单设备虽然硬件外设资源丰富,但是归属于各个设备。应用层实现设备间硬件资源调用成本高,应用间能力难以复用。
分布式硬件打破单一设备的硬件边界,能够将硬件设备化整为零,形成超级终端硬件资源池。供多个设备共享使用,真正达到软件定义硬件。设备间实现系统级融合并灵活按需适应不同场景的目的。对于开发者而言,只需要一套API实现本地和跨设备硬件查询调用。应用可以更加专注于业务创新,而无需关注底层细节
分布式任务调度
分布式任务调度基于分布式软总线,包管理,设备管理等技术特性,提供应用跨端迁移、多端协同能力。
分布式任务调度的核心能力包括:场景感知、迁移调度、协同调度、数据迁移
- 场景感知:综合感知、空间、身份、设备等信息,为服务随人走提供决策基础。
- 迁移调度:提供跨设备迁移通知、原生迁移框架,帮助开发者快速开发迁移应用。
- 协同调度:提供跨设备拉起、绑定的主动能力,系统自行协同能力,管理协同关系。帮助开发者快速构建应用协同业务。
- 数据迁移:提供货架化的数据迁移传输接口,支持应用按需使用接口传输数据
系统抽象和交互逻辑
结合以上介绍的分布式应用开发框架设计的关键核心技术进行简单的系统抽象。分布式软总线作为HarmonyOS系统的互联底座,提供快速发现、无感连接、稳定组网、极速传输这些关键技术,基于分布式软总线实现数据的分布式管理,让数据为多个设备共建共享。分布式硬件让硬件资源池化,完成多设备的硬件资源共享。各个设备被分布式系统统一起来成为一个超级终端。上层应用只需要简单的开发就可以完成多设备的迁移和协同
跨端迁移运作机制
当我们拥有两个设备A和B分别为源端(也就是用户当前使用的设备)和对端(当前任务迁移后使用的设备端)
- 在源端:通过UIAbility中的onContinue回调,开发者可以保存待接续的业务数据(例如在浏览器应用中完成跨端迁移,开发者需要使用onContinue回调保存页面UR等业务内容,而系统将自动保存页面状态,如当前浏览进度)。其次分布式框架体提供了跨设备应用界面页面栈以及业务数据的保存和恢复机制,它负责将数据从源端发送到对端。
- 在对端:同一UIAbility,可以通过onCreat或onNewWant接口来恢复业务数据,接续时,我们会用到的主要接口也就是onContinue和onCreat或onNewWant
具体开发步骤
为应用配置基础迁移功能
在Entry/src/main/moudle.json5配置文件中配置continuable标签
{"module": {// ..."abilities": [{// ..."continuable": true,//配置迁移能力"launchType": "singleton"//指定启动模式,冷启动场景使用onCreat实现,热启动场景使用onNewWant实现}],"requestPermissions": [{"name": "ohos.permission.DISTRIBUTED_DATASYNC",//增加权限配置、允许不同设备间的数据交换"reason": "$string:distributed_data_sync", "usedScene": {"abilities": ["EntryAbility"],"when": "inuse"}}]}
}
应用动态运行时申请用户授权
因为分布式数据同步权限的权限类型为user_grant,该类型权限不仅需要在应用中申请权限,还需要在应用动态运行时通过发送弹窗的方式请求用户授权,在用户主动授权后应用才会真正获得对应的权限
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit';export default class EntryAbility extends UIAbility {async checkPermissions(): Promise<void> {const permissions: Array<Permissions> = ["ohos.permission.DISTRIBUTED_DATASYNC"];const accessManager = abilityAccessCtrl.createAtManager();try {const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION;const bundleInfo = await bundleManager.getBundleInfoForSelf(bundleFlags);//获取授权状态const grantStatus = await accessManager.checkAccessToken(bundleInfo.appInfo.accessTokenId, permissions[0]);//未获得授权状态,需向用户申请状态if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_DENIED) {accessManager.requestPermissionsFromUser(this.context, permissions);}} catch (err) {Logger.error('EntryAbility', 'checkPermissions', `Catch err: ${err}`);return;}}
}
这个例子我们在onCreat中申请用户授权,首先检查授权状态,如果未申请授权,就以弹窗的形式向用户申请权限
实现简单接口
源端:实现源端回调
onContinue(wantParam: Record<string, Object | undefind>): AbilityConstant.OnContinueResult {...return AbilityContanst.OnContinueResult.AGREE
}
在源端onContinue函数中可以进行兼容性检验,根据检验结果返回迁移结果
对端:实现对端回调
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {this.checkPermission()if(launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {this.context.restoreWindowStage(new LocalStorage())//触发页面恢复}
}
分布式数据传输
应用迁移的开发关键是数据迁移,在进行应用迁移开发设计的时候,需要先分析业务数据模型采用合适的数据同步能力达到最佳的体验。
应用数据主要包括页面数据和业务数据:
- 页面数据:根据优先使用的分布式任务调度迁移框架进行迁移,如通过支持迁移的组件进行数据迁移,通过配置项设置进行页面栈迁移
- 业务数据:根据业务数据模型采用合适的数据同步能力如通过want参数传输应用数据状态,通过分布式数据对象迁移内存数据,通过分布式文件迁移文件数据。
使用组件迁移数据
部分组件支持分布式迁移:例如List、Grid、Scroll、WaterFlow…
@Entry
@Component
struct Index {private arr: number[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]build() {Column() {List({ space: 20 }) {ForEach(this.arr, (item: number) => {ListItem() {Text('' + item)...}}, (item: number) => (item.toString())) }.restoredId(1)//给需要迁移的组件配置restoredId}}
}
迁移的方式就是给可以自动迁移的组件设置restoredId,传入的参数为number类型的数字,不同的组件需要配置不同的restoredId,这样就能完成自动迁移
页面栈迁移
系统默认迁移页面栈,如果不希望迁移页面栈可以设置为false,无论是否需要迁移页面栈,在对端的onCreate函数中,只要是迁移的场景,就要主动触发页面恢复
源端:实现源端回调
onContinue(wantParam: Record<string, Object | undefind>): AbilityConstant.OnContinueResult {wantParam["ohos.extra.param.key.supportContinuePageStack"] = falsereturn AbilityContanst.OnContinueResult.AGREE
}
对端:实现页面恢复
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {if(launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {this.context.restoreWindowStage(new LocalStorage())//触发页面恢复}
}
少量状态数据迁移
一般是小于100kb的状态数据
源端:实现源端回调
onContinue(wantParam: { [key: string]: any }) {let sessionId: string = AppStorage.get('sessionId') as string...wantParam['sessionId'] = AppStorage.get<string>('sessionId')...
}
对端:恢复数据
onCreat(want: Want, launchParam: AbilityConstant.LaunchParam): void {...if(launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {AppStorage.setOrCreate<string>('sessionId', want.parameters?.sessionId)this.context.restoreWindowStage(new LocalStorage())}
}
多个设备间的对象设置为同一个sessionId才能自动同步数据。首先我们需要在源端onContinue接口中保存迁移数据,也就是wantParam['sessionId'] = AppStorage.get<string>('sessionId')
,在对端先判断是否迁移场景,如果是迁移,进行数据迁移的恢复,这样就完成了少量数据状态的迁移
内存数据迁移
内存数据迁移通常适用于临时数据,生命周期较短,通常保存在内存数据中的可以封装成一个对象,使用分布式数据对象的方式进行迁移。
在该场景中我们需要首先在onWindowStageCreate的时候,创建一个分布式数据对象
private localObject: disributeDataObject.DataObject | undefind = undefind;
...
onWindowStageCreate(windowStage: window.WindowStage) {...if(!this.lcalObject) {let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined);//创建分布式数据对象this.localObject = distributeDataObject.creat(this.context, mailInfo);}...
}
接下来我们需要在onContinue回调中设置sessionId,当可信组网中有多个设备时,多个设备间的对象如果设置为同一个sessionId就能自动同步,然后将数据保存在分布式对象中。保存成功后,当应用退出后重新进入应用时恢复。
注意
数据保存完之后必须要调用save触发对象同步
onContinue(wantParam: Record<string, Object | undefined>): AbilityConstant.OnContinueResult {try {let sessionId: string = AppStorage.get('sessionId') as string;if(!sessionId) {sessionId = distributedDataObject.genSessionId();AppStorage.setOrCreate('sessionId', sessionId);}if(this.localObject) {this.localObject.setSessionId(sessionId);this.localObject['recipient'] = AppStorage.get('recipient');this.localObject['sender'] = AppStorage.get('sender');this.localObject['subject'] = AppStorage.get('subcject');this.localObject['emailContent'] = AppStorage.get('emailContent');this.localObject.save(wantParam.targetDevice as string);wantParam.distributedSessionId = sessionId;}} catch(e) {...}}
在onCreate回调中,我们先拿到传递过来的sessionId,然后对分布式数据变化进行监听,然后与sessionId绑定,最后从分布式数据对象中恢复数据
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {if(launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {let sessionId: string = want.Parameters?.distributedSessionId as string;//获取sessionIdif(!this.localObject) {let mailInfo: MailInfo = new MailInfo(undefined, undefined, undefined, undefined);this.localObject = distributeDataObject.creat(this.context, mailInfo);this.loacalObject.on('change', this.changeCall);//监听分布式数据对象的数据变更}if(sessionId && this.loaclObject) {this.loaclObject.setSessionId(sessionId);//与sessionId绑定AppStorage.setOrCreate('recipient', this.localObject['recipient']);AppStorage.setOrCreate('sender', this.localObject['sender']);AppStorage.setOrCreate('subject', this.localObject['subject']);AppStorage.setOrCreate('eamilContent', this.localObject['eamilContent']);}this.context.restoreWindowStage(new LocalStorage());}
}
分布式文件传输
当进行应用开发时,传送的文件大小超过了100kb,这时需要使用分布式文件系统来进行迁移,分布式文件系统为应用提供了跨设备问价访问的能力,多个设备安装了同一个应用时通过基础文件接口,可跨设备读写其他设备分布式文件路径下的文件。
源端
如多设备流转场景,设备组网互联之后,源端的应用可访问对端同应用分布式路径下的文件。当期望应用文件被其他设备访问时,只需要将文件移动到分布式文件路径即可
let buf = new ArrayBuffer(CommonConstants.FILE_BUFFER_SIZE);
let readSize = 0;
let file = fileIo.openSync(documentSelectResultElement, fileIo.OpenMode.READ_ONLY);
let readLen = fileIo.readSync(file.fd, buf, { offset: readSize });let fileName = file.name;
...
let destination = fileIo.openSync(getContext().filesDir + '/' + fileName, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);let destinationDistribute = fileIo.openSync(getContext().distributedFilesDir + '/' + fileName, fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);while (readLen > 0) {//向分布式文件路径readSize += readLen;fileIo.writeSync(destination.fd, buf);fileIo.writeSync(destinationDistribute.fd, buf);console.info(destinationDistribute.path);readLen = fileIo.readSync(file.fd, buf, { offset: readSize });}fileIo.closeSync(file);fileIo.closeSync(destination);fileIo.closeSync(destinationDistribute);
对端
接着在对端文件路径下读取对应的文件数据,读取完毕后关闭文件流,这样很轻松就完成分布式文件传输数据了
let filePath: string = this.distributedPath + item.fileName;
let savePath: string = getContext().filesDir + '/' + item.fileName;
let saveFile = fs.openSync(savePAath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let file = fs.openSync(filePAath, fs.OpenMode.READ_WRITE);let buf: ArrayBuffer = new ArrayBuffer(CommonConstants.FILE_BUFFER_SIZE);let readSize = 0;let readLen = fileIo.readSync(file.fd, buf, { offset: readSize });while (readLen > 0) {readSize += readLen;fileIo.writeSync(saveFile.fd, buf);readLen = fileIo.readSync(file.fd, buf, { offset: readSize });}fileIo.closeSync(file);fileIo.closeSync(saveFile);
跨端迁移开发总结
- 分析业务数据模型,采用合适的数据同步能力,达到最佳实践,
- 三步完成迁移业务
- 配置应用可流转
- 申请数据同步权限
- onContinue中保存数据,onCreate/onNewWant中恢复数据
- 可选的数据迁移方式
- 迁移框架:通过支持迁移的组件和页面栈迁移参数配置。
- want:应用状态数据通过want参数传输(建议数据量小于100KB)
- 分布式数据对象:内存数据。
- 分布式文件:文件数据