鸿蒙Ability学习
Posted RikkaTheWorld
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了鸿蒙Ability学习相关的知识,希望对你有一定的参考价值。
1. 概述
1.1 Ability是什么
下面是官方文档的概述:
Ability是应用所具备能力的抽象,也是应用程序的重要组成部分。一个应用可以具备多种能力(即可以包含多个Ability),HarmonyOS支持应用以Ability为单位进行部署。Ability可以分为FA(Feature Ability)和PA(Particle Ability)两种类型,每种类型为开发者提供了不同的模板,以便实现不同的业务功能。
之前在研究鸿蒙时,我发现里面很多东西都可以和android挂钩,一马当先的就是这个 Ability
了。从官方概述来说,它挂钩的应该是 组件 这个概念。在Android中有四大组件, 在鸿蒙中,就这一大组件了,它可以提供很多能力,供应用开发使用。
Ability 是一个抽象,其包括两个类别,FA和PA,来看看他们的区别:
Feature Ability(FA)
: 用于支持Page Ability
只支持 Page, 字面意思,就是 页面能力, 一个 Page 实例包含一组页面,每个页面用一个AbilitySlice
实例表示。Particle Ability(PA)
:用于支持Service Ability
和Data Ability
① Service Ability:用于提供后台运行任务的能力
② Data Ability:用于提供统一的数据访问抽象
千万不要吐槽,毕竟能看中文的官方技术性文档,已经是很了不起的事情了 😆
鸿蒙应用的配置文件是 config.json
, 如果需要注册一个 Ability,需要指定一下, “type” 字段用来指定 Ability 类型,取值有 “page”、“data”、“service”, 格式如下:
// config.json
{
"module": {
...
"abilities": [
{
...
"type": "page"
...
}
]
}
}
1.2 学什么?
看了概述, 感觉要学习的其实就3个东西: Page Ability
、 Service Ability
和 Data Ability
。
但是我看了下官方文档,发现事情没有这么简单:
What? Ability 是一个框架,除了 Ability 本身,它还包含了 事件通知、线程处理、剪贴板?
好家伙,我学一个 Ability,等于直接学了一个鸿蒙应用开发。
本篇应该不会讲解那么多东西,主要是 FA 和 PA, 至于下面这些,我看着学 🙃
2. Page Ability
Page Ability
是页面,它的实例就是一个 Page,一个Page可以包含一个或多个 AbilitySlice
,就是 Ability碎片 - -, 如下图所示:
鸿蒙支持不同 Page 之间的跳转,也支持指定跳转到某个Page中的具体 AbilitySlice。 当然,Page也可以不用包含 AbilitySlice
就能展示 UI。跟 Activity 和 Fragment 的关系是一样的。
2.1 AbilitySlice路由配置
我们知道一个 Page 可以包含多个 AbilitySlice, 但进入前台时默认只展示一个 AbilitySlice,需要使用下面api来设置默认展示的碎片:
setMainRoute()
如果需要更改默认展示的 AbilitySlice, 则可以通过下面方法为其配置一条路由规则:
// 代码中添加一个隐式路由
addActionRoute()
不过这个方法需要添加一些参数,这些参数需要在 config.json
中进行注册:
// config.json
"abilities": [
{
"skills":[
{
// 在这里为 ability 那些可以被拉起展示的 abilityslice
"actions":[
"action.pay",
"action.scan"
]
...
看下面代码, 其他页面可以通过 Intent 打开该 Page 时默认展示具体哪个 AbilitySlice:
public class MyAbility extends Ability {
@Override
public void onStart(Intent intent) {
super.onStart(intent);
// 设置主 AbilitySlice
setMainRoute(MainSlice.class.getName());
// 注册默认的页面, 其他页面可以通过 Intent 打开该 Page 时默认展示具体哪个 AbilitySlice
addActionRoute("action.pay", PaySlice.class.getName());
addActionRoute("action.scan", ScanSlice.class.getName());
}
}
2.2 Page Ability的生命周期
状态机如下所示:
onStart()
系统 首次创建 Page实例的时机。 该回调在生命周期中仅触发一次, Page在该逻辑后会进入INACTIVE
状态,该方法必须重写,且需要在该方法里设置 AbilitySlice,模板代码如下:
@Override
public void onStart(Intent intent) {
super.onStart(intent);
super.setMainRoute(FooSlice.class.getName());
}
onActive()
Page 在进入了INACTIVE
状态后来到前台,就会调用该方法。 在此进入ACTIVE
状态, 该状态是应用于用户交互的状态。
Page一直保持在该状态, 直至 Page 失去焦点,发生时,Page 会回到INACTIVE
状态,系统调用onInactive()
方法,此后如果回到ACTIVE
状态,就会再次调用onActive()
。onInactive()
当 Page 失去焦点时,就会调用此方法。onBackground()
当 Page 不再可见时,会调用此方法,此后 Page 进入BACKGROUND
状态,该方法可以用来释放无用资源onForeground()
Page 在处于BACKGROUND
的状态时若仍然驻留在内存中,当重新回到前台时, 会首先调用onForeground()
通知开发者,
然后进入INACTIVE
状态onStop()
系统将要销毁 Page 时,将会触发此回调函数,这个时候需要释放系统资源。销毁Page的原因:
①:用户使用系统能力关闭Page,比如任务管理器关闭
②:触发 Page 的terminateAbility()
方法
③:配置变更导致系统暂时销毁Page并重建 (竖屏转横屏?)
④:系统处于资源管理的目的, 回收处于BACKGROUND
状态的Page
和 Android Activity/Fragment 生命周期的一些区别:
- Android中使用
onStart()
表示进入前台, 第一次也会调, 鸿蒙用onForeground()
表示,但是第一次进入时不会调用 - 无。
2.3 AbilitySlice 生命周期
AbilitySlice
的生命周期和 Page 是一样的,具有同状态和同名的回调。 当 Page 生命周期发生变化时,它的 AbilitySlice 也会发生相同的生命周期的变化, 但是如果只是在同个 Page 内导航 AbilitySlice,Page状态不变,AbilitySlice 状态会变,相当于独立了。
此外,必须重写 AbilitySlice 的 onStart()
方法,并通过 setUIContent()
设置页面:
@Override
protected void onStart(Intent intent) {
super.onStart(intent);
setUIContent(ResourceTable.Layout_main_layout);
}
在同一个 Page 中,从 A AbilitySlice 导航到 B AbilitySlice 所经历的方法回调是:
A.onInactive() -> B.onStart() -> B.onActive() -> A.onBackground()
2.4 同一 Page 内导航
2.4.1 presenter()
当发起导航的 AbilitySlice 和导航目标的 AbilitySlice 处于同一个 Page 时, 可以通过 presenter()
实现导航,代码如下,导航到 Target AbilitySlice:
private void initUi() {
Text text = (Text) findComponentById(ResourceTable.Id_jump_button);
text.setClickedListener(listener -> present(new TargetSlice(), new Intent()));
}
2.4.2 presentForResult()
如果希望从导航目标的 AbilitySlice 返回时,带上结果,则需要使用 presenterForResult()
,带上 requestCode
,并重写 onResult()
回调:
private void initUi() {
Text text = (Text) findComponentById(ResourceTable.Id_jump_button);
text.setClickedListener(listener -> presentForResult(new TargetSlice(), new Intent(), 0));
}
@Override
protected void onResult(int requestCode, Intent resultIntent) {
// 这里返回页面结果
super.onResult(requestCode, resultIntent);
}
2.4.3 AbilitySlice 实例栈
每个 Page 都会维护一个 AbilitySlice 实例的栈,每个进入前台的 AbilitySlice 实例都会入栈。
当调用 presenter()
时,指定的 AbilitySlice 实例已经入栈了,则栈中位于此实例之上的 AbilitySlice 均会出栈,并终止其生命周期。
2.5 不同 Page 间导航
AbilitySlice 是作为 Page 的内部单元,以 Action 的形式对外暴露。
Page 间的导航使用 startAbility()
/ startAbilityForResult()
方法导航, 获得返回结果的回调为 onAbilityResult()
。在 Ability 中的 setResult()
可以设置返回结果。
2.6 跨设备迁移
跨设备迁移,支持将 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
如果数据传递成功,源设备会回调这个方法,告知迁移结束,源设备可以在这里结束PageonFailedContinuation
迁移过程中发生异常,会在源设备回调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) {
....
}
这里就不具体展示具体流程了,和迁移流程大同小异。
3. Service Ability
基于 Service 的 Ability ,主要提供的能力是后台运行任务(比如音乐播放、文件下载),不能提供页面UI服务。
Service 是单例的,一个设备上,一个Service 只存在一个实例, 一个 Service 可以绑定多个 Ability, 只有在绑定的 Ability 全部退出后, 这个Service 才能退出。
理论上,它和 Android 的 Service 组件作用一样, 这样看来, Ability 更像是一个 context。
3.1 Service Ability 的生命周期
它有两种启动方法
- 启动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
...
}
]
...
}
...
}
3.2 启动 Service
通过 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);
3.3 连接 Service
如果 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();
}
3.4 前台 Service
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: 音乐播放器
4. Data Ability
Data Ability 有助于应用管理自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。 Data既可以用于同设备下不同应用的数据共享,也支持跨设备不同应用数据共享
4.1 URI概述
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条数据
4.1 创建 Data
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(以上是关于鸿蒙Ability学习的主要内容,如果未能解决你的问题,请参考以下文章
鸿蒙应用开发Ability和Android的activity不同之处