跨设备迁移,支持将 Page 在同一用户的不同设备间的迁移,就是将一个页面从 A 设备转移到 B 设备,这是万物互联的一个体现。其步骤大概三步走:
-
设备A上的Page请求迁移
-
HarmonyOS 处理迁移任务,并回调设备A上Page的保存数据方法,用于保存迁移必须的数据
-
HarmonyOS 在设备B上启动同一个Page,并回调其恢复数据方法
2.6.1 实现 IAbilityContinuation
接口
一个应用可能包含多个Page,仅需要在支持迁移的Page中通过以下方法实现IAbilityContinuation接口。同时,此Page所包含的所有AbilitySlice也需要实现此接口。
onStartContinuation
Page 请求迁移后,系统首先回调这个方法,开发者可以在此回调中决策当前是否可以执行迁移。比如弹一个弹窗让用户确认是否迁移
onSaveData
如果 onStartContinuation
返回true,那么就会调用这个方法,开发者在此方法中保存数据,这些数据后续将传到另一台设备上
onRestoreData
源侧设备上Page完成保存数据后,系统在目标侧设备上回调这个方法,开发者在此回调中接收用于恢复 Page 状态的数据。
这个方法调用实际在 onStart
之前,会触发目标设备上 Page 的重新启动生命周期
onCompleteContinuation
如果数据传递成功,源设备会回调这个方法,告知迁移结束,源设备可以在这里结束Page
onFailedContinuation
迁移过程中发生异常,会在源设备回调FA的此方法。
onRemoteTerminated
(可以不必实现)
如果开发者使用 continueAbilityReversibly()
而不是 continueAbility()
,则此后可以在源设备上使用 reverseContinueAbility
进行回迁。这种场景下,相当于同一个 Page 的两个实例运行在两台设备上。迁移完成后,如果目标设备上的 Page 生命周期销毁,那么源设备会调用此方法
2.6.2 请求迁移
实现了 IAbilityContinuation
的接口后,可以在生命周期内, 调用 continueAbility()
/ continueAbilityReversibly
请求迁移,后者可以回迁:
try {
continueAbility();
} catch (IllegalStateException e) {
…
}
从 A 设备迁移到 B 设备,流程如下:
-
设备 A 上的 Page 请求迁移
-
系统回调设备 A 上 Page 以及 AbilitySlice 栈里面所有 AbilitySlice 实例的
IAbilityContinuation.onStartContinuation
方法,已确认当前是否可以立即迁移 -
如果可以立即迁移,则系统回调设备 A 上 Page 及其 AbilitySlice 实例的
IAbilityContinuation.onSaveData
-
如果数据保存成功,则系统在 B上启动一个 Page,并恢复其 AbilitySlice 栈,然后回调
IAbilityContinuation.onRestore
方法,传递此前保存的数据,然后 B 设备的 Page 从onStart()
生命周期开始进行 -
调用 A 设备 Page 以及其所有 AbilitySlice 的
IAbilityContinuation.onCompleteContinuation
方法 -
如果迁移发生异常, 系统回调 A 的 Page及其所有 AbilitySlice 栈中所有 AbilitySlice 实例的
IAbilityContinuation.onFialedContinuation
方法,并不是所有异常都会回调此FA方法,仅局限于该接口枚举的异常。
这里有一个问题,A设备如何找到B设备,这就涉及到获取分布式设备类了,需要通过监听迁移按钮的点击事件,然后获取分布式设备列表,选择设备后进行传递,具体代码可以看:获取分布式设备
2.6.3 请求回迁
如果前面调用 continueAbilityReversibly
请求迁移完成后, 源设备可以调用 reverseContinueAbility
发起回迁:
try {
reverseContinueAbility();
} catch (IllegalStateException e) {
…
}
这里就不具体展示具体流程了,和迁移流程大同小异。
====================================================================================
基于 Service 的 Ability ,主要提供的能力是后台运行任务(比如音乐播放、文件下载),不能提供页面UI服务。
Service 是单例的,一个设备上,一个Service 只存在一个实例, 一个 Service 可以绑定多个 Ability, 只有在绑定的 Ability 全部退出后, 这个Service 才能退出。
理论上,它和 Android 的 Service 组件作用一样, 这样看来, Ability 更像是一个 context。
它有两种启动方法
-
启动Service, 其他 Ability 通过调用
startAbility()
时创建,然后保持运行,其他 Ability 通过调用stopAbility()
来停止 Service,停止后,它将会被销毁 -
绑定Service,其他 Ability 通过调用
connectAbility
来绑定 Service, 通过disconnectAbility()
来解绑。多个 Ability 可以连接一个 Service,当一个 Service 不在被任何 Ability 绑定时,其将会被销毁
Service Ability 生命周期的方法:
onStart
创建 Ability 的时候调用,整个生命周期只调用一次,传入的 Intent 应该是空的
onCommand
在Service创建完成之后调用,该方法在客户端每次启动该Service时都会调用,开发者可以在该方法中做一些调用统计、初始化类的操作
onConnect
在 Ability 绑定 Service Ability 的时候回调,会让 Service 创建并返回一个 IRemoteObject
对象,可以通过这个对象生成一个 IPC 通道,便于 Service 和 Ability 通信,所以可以看出来,这个 Service 和 通信的Ability 可以不在一个进程中。
其次,多个 Ability 可以绑定同一个 Service, 系统会缓存这个 IPC 通道,只有在第一个客户端绑定的时候,会调用 onConnect
方法,之后别的 Ability 再次绑定时,会将这个 IPC 通道发送过去,而无需再次调用 onConnect
方法
onDisConnected
在 Ability 和 Service 解除绑定的时候调用
onStop
Service 销毁的时候调用,可以在这里释放资源
public class ServiceAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
}
@Override
public void onCommand(Intent intent, boolean restart, int startId) {
super.onCommand(intent, restart, startId);
}
@Override
public IRemoteObject onConnect(Intent intent) {
return super.onConnect(intent);
}
@Override
public void onDisconnect(Intent intent) {
super.onDisconnect(intent);
}
@Override
public void onStop() {
super.onStop();
}
}
同时需要在 config.json 中注册:
{
“module”: {
“abilities”: [
{
“name”: “.ServiceAbility”,
“type”: “service”,
“visible”: true
…
}
]
…
}
…
}
通过 startAbility()
来启动一个 Service Ability,可以通过传入 Intent
来启动,可以支持本地或者远程的 Service
可以通过Intent传入三个信息:
DeviceId
设备ID,如果是本地设备,可以为空,如果是远程设备,可以通过 ohos.distributedschedule.interwork.DeviceManager 获取设备列表
BundleName
表示包名
AbilityName
表示待启动的Ability名称
启动本地设备 Service 的代码如下:
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId(“”)
.withBundleName(“com.xxx”)
.withAbilityName(“com.xxx.ServiceAbility”)
.withFlag(Intent.FLAG_ABILITYSLCE_MULTI_DEVICE) // 启动远端设备的Service时需要带上,表示分布式调度系统多设备启动
.build();
intent.setOperation(operation);
startAbility(intent);
如果 Service 需要与 Page 或者其他应用的 Service Ability 交互,就必须要创建用于连接的 Connection
, 这样别的 Ability 就可以通过 connectAbility()
方法和它进行连接。
在使用 connectAbility
时,需要传入目标 Service 的 Intent 和 IAblityConnection
的示例,它有两个方法,分别是连接成功的回调 和 异常死亡的回调,如下所示:
// 创建连接Service回调实例
private IAbilityConnection connection = new IAbilityConnection() {
// 连接到Service的回调
@Override
public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
// Client侧需要定义与Service侧相同的IRemoteObject实现类。开发者获取服务端传过来IRemoteObject对象,并从中解析出服务端传过来的信息。
}
// Service异常死亡的回调
@Override
public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
}
};
连接 Service的代码,需要带上 Connection:
// 连接Service
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
.withDeviceId(“deviceId”)
.withBundleName(“com.domainname.hiworld.himusic”)
.withAbilityName(“com.domainname.hiworld.himusic.ServiceAbility”)
.build();
intent.setOperation(operation);
connectAbility(intent, connection);
同时需要在 Service 需要在 onConnect()
时,返回 IRemoteObject,从而传递这个 IPC通道。鸿蒙提供了默认实现,可以通过继承 LocalRemoteObject
来创建这个通道,然后回传,如下所示:
private class MyRemoteObject extends LocalRemoteObject {
MyRemoteObject(){
}
}
// 把IRemoteObject返回给客户端
@Override
protected IRemoteObject onConnect(Intent intent) {
return new MyRemoteObject();
}
Service 一般是在后台运行,所以优先级比较低,容易被回收。
但是在一些场景下(比如文件下载),用户希望能够一直保持运行,这个时候就需要使用前台Service。 前台Service会始终保持正在运行的图标在状态栏显示,即和通知绑定
使用 前台Service,只需以下步骤:
-
Serivce 内部调用
keepBackgroundRunning()
绑定 Service 和 通知 -
在 Service 的配置文件中声明
ohos.permission.KEEP_BACKGROUND_RUNNING
权限 -
在配置文件中添加对应的
backgroundModes
参数 -
在 Serivce 的
onStop()
方法中调用cancelBackgroundRunning()
示例如下:
// 创建通知,其中121为notificationId
NotificationRequest request = new NotificationRequest(121);
NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
content.setTitle(“title”).setText(“text”);
NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
request.setContent(notificationContent);
// 绑定通知,1005为创建通知时传入的notificationId
keepBackgroundRunning(1005, request);
同时修改 config.json 里面的配置:
{
“name”: “.ServiceAbility”,
“type”: “service”,
“visible”: true,
“backgroundModes”: [“dataTransfer”, “location”]
}
backgroundModes 表示后台的服务类型,该标签仅适用于 Service Ability,取值如下:
这个是前台的音乐播放器Demo: 音乐播放器
=================================================================================
Data Ability 有助于应用管理自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。 Data既可以用于同设备下不同应用的数据共享,也支持跨设备不同应用数据共享
Data 提供的 api 都是 URI
来标识一个具体的数据,HarmonyOS的URI是基于URI通用标准,格式如下:
-
scheme: 固定为 “dataability”
-
authority: 设备id,如果为跨设备场景,则为目标设备的id,如果为本地设备场景,则不需要填写
-
path:资源路径信息,代表特定资源的位置信息
-
query:查询参数
-
fragment:可以用于指示要访问的子资源
例如:
跨设备场景:dataability://device_id/com.domainname.dataability.persondata/person/10
本地设备:dataability:///com.domainname.dataability.persondata/person/10
查询 persondata 路径下 person 参数的第10条数据
Data 提供自定义数据的增删改查等功能,并对外提供这些接口
4.1.1 确定数据存储方式
确定数据的存储方式, Data 支持一下两种数据形式:
-
文本数据:文本、图片、音乐等
-
结构化数据:如数据库、数据Bean等。
4.1.2 实现 DataAbility
Data Ability
用于接收其他应用发送的请求,所以它提供访问接口。
通过在工程目录下,点击 Empty Data Ability 并且输入 Data 的名称,既可创建一个默认实现的 DataAbility
Data 提供了两组接口,分别是:
-
文件存储
-
数据库存储
4.1.3 文件存储
通过 FileDescriptor openFile(Uri uri, String mode)
来操作文件, uri 为调用方传入的请求目标路径, mode为开发者对文件的操作选项,可选方式包括 “r”(只读), “w”(只写),“rw”(读写) 等
ohos.rpc.MessageParcel 提供了一个静态方法,用于获取 MessageParcel 实例。 开发者可以通过获取到的 MessageParcel 实例,使用 dupFileDescriptor()
复制带操作文件流的文件描述符,并将其返回,供远端应用访问文件。
代码如下所示:
@Override
public FileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
// 创建messageParcel
MessageParcel messageParcel = MessageParcel.obtain();
File file = new File(uri.getDecodedPathList().get(0)); //get(0)是获取URI完整字段中查询参数字段。
if (mode == null || !“rw”.equals(mode)) {
file.setReadOnly();
}
FileInputStream fileIs = new FileInputStream(file);
FileDescriptor fd = null;
try {
fd = fileIs.getFD();
} catch (IOException e) {
HiLog.info(LABEL_LOG, “failed to getFD”);
}
// 绑定文件描述符,使其具备文件操作流
return messageParcel.dupFileDescriptor(fd);
}
4.1.4 数据库存储
使用数据库,需要先连接到数据库。
系统会在应用启动的时候调用 onStart()
方法来创建 Data 实例,所以这个方法里面,开发者需要创建数据库连接,并获取连接对象,以便后续操作。
下面是一段连接数据库的示例代码:
private static final String DATABASE_NAME = “UserDataAbility.db”;
private static final String DATABASE_NAME_ALIAS = “UserDataAbility”;
private OrmContext ormContext = null;
@Override
public void onStart(Intent intent) {
super.onStart(intent);
DatabaseHelper manager = new DatabaseHelper(this);
ormContext = manager.getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, BookStore.class);
}
提供下面的 api 来增删改查:
这里需要注意的点是, 数据库是支持 ORM
的, ValueBucket 就像 Bundle 一样,可以传递参数,例如下面这个在数据库中插入一条数据:
public int insert(Uri uri, ValuesBucket value) {
// 参数校验
if (ormContext == null) {
HiLog.error(LABEL_LOG, “failed to insert, ormContext is null”);
return -1;
}
// 构造插入数据
User user = new User();
user.setUserId(value.getInteger(“userId”));
user.setFirstName(value.getString(“firstName”));
user.setLastName(value.getString(“lastName”));
user.setAge(value.getInteger(“age”));
user.setBalance(value.getDouble(“balance”));
// 插入数据库
boolean isSuccessful = ormContext.insert(user);
if (!isSuccessful) {
HiLog.error(LABEL_LOG, “failed to insert”);
return -1;
}
isSuccessful = ormContext.flush();
if (!isSuccessful) {
HiLog.error(LABEL_LOG, “failed to insert flush”);
return -1;
}
DataAbilityHelper.creator(this, uri).notifyChange(uri);
int id = Math.toIntExact(user.getRowId());
return id;
}
具体 orm 能力可以看这里: 对象关系映射数据库开发指导
4.1.5 注册 UserDataAbility
和 Service 类似,开发者必须在配置文件中注册 Data
需要关注几个属性:
- type
类型设置为 data
- uri
对外提供的访问路径,全局唯一
- permission
访问该 data ability 时需要申请的权限
如下所示:
{
“name”: “.UserDataAbility”,
“type”: “data”,
“visible”: true,
“uri”: “dataability://com.example.myapplication5.DataAbilityTest”,
“permissions”: [
“com.example.myapplication5.DataAbility.DATA”
]
}
开发者通过 DataAbilityHelper
来访问当前应用或者其他应用提供的共享数据。
4.2.1 声明访问权限
如果需要访问的 Data 声明了权限,那么访问此 Data 需要也在配置文件中声明权限
4.2.2 创建 DataAbilityHelper
通过传入一个 context 来创建 DataAbilityHelper
对象
DataAbilityHelper helper = DataAbilityHelper.creator(this);
4.2.3 使用 DataAbilityHelper
- 访问文件
通过 DataAbilityHelper 的 FileDescriptor openFile(Uri uri, String mode)
来操作文件,需要传入两个参数,其中uri用来确定目标资源路径,mode用来指定打开文件的方式,这个方法返回一个目标文件的描述符,可以把它封装成文件流,就可以对其进行自定义处理:
// 读取文件描述符
FileDescriptor fd = helper.openFile(uri, “r”);
// 使用文件描述符封装成的文件流,进行文件操作
FileInputStream fis = new FileInputStream(fd);
- 访问数据库
上面的图片又数据库操作的api,可以用来对数据库增删改查,下面是一段查询操作代码
DataAbilityHelper helper = DataAbilityHelper.creator(this);
// 构造查询条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between(“userId”, 101, 103);
// 进行查询
ResultSet resultSet = helper.query(uri, columns, predicates);
// 处理结果
resultSet.goToFirstRow();
do {
// 在此处理ResultSet中的记录;
} while(resultSet.goToNextRow());
===========================================================================
Intent 是对象之间传递信息的载体。
当 Intent 用于发起请求时,根据指定元素的不同,分为两种类型:
-
如果同时指定了 BundleName 与 AbilityName, 则根据 AbilityName 来直接启动应用
-
如果未同时指定 BundleName 和 AbilityName, 则根据 Operation 中的其他属性来启动应用
Intent intent = new Intent();
// 通过Intent中的OperationBuilder类构造operation对象,指定设备标识(空串表示当前设备)、应用包名、Ability名称
Operation operation = new Intent.OperationBuilder()
.withDeviceId(“”)
.withBundleName(“com.demoapp”)
.withAbilityName(“com.demoapp.FooAbility”)
.build();
// 把operation设置到intent中