鸿蒙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 AbilityData Ability
    ① Service Ability:用于提供后台运行任务的能力
    ② Data Ability:用于提供统一的数据访问抽象

千万不要吐槽,毕竟能看中文的官方技术性文档,已经是很了不起的事情了 😆

鸿蒙应用的配置文件是 config.json, 如果需要注册一个 Ability,需要指定一下, “type” 字段用来指定 Ability 类型,取值有 “page”、“data”、“service”, 格式如下:

// config.json
{
    "module": {
        ...
        "abilities": [
            {
                ...
                "type": "page"
                ...
            }
        ]
    }
}

1.2 学什么?

看了概述, 感觉要学习的其实就3个东西: Page AbilityService AbilityData 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 生命周期的一些区别:

  1. Android中使用 onStart() 表示进入前台, 第一次也会调, 鸿蒙用 onForeground() 表示,但是第一次进入时不会调用
  2. 无。

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 设备,这是万物互联的一个体现。其步骤大概三步走:

  1. 设备A上的Page请求迁移
  2. HarmonyOS 处理迁移任务,并回调设备A上Page的保存数据方法,用于保存迁移必须的数据
  3. 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 设备,流程如下:

  1. 设备 A 上的 Page 请求迁移
  2. 系统回调设备 A 上 Page 以及 AbilitySlice 栈里面所有 AbilitySlice 实例的 IAbilityContinuation.onStartContinuation 方法,已确认当前是否可以立即迁移
  3. 如果可以立即迁移,则系统回调设备 A 上 Page 及其 AbilitySlice 实例的 IAbilityContinuation.onSaveData
  4. 如果数据保存成功,则系统在 B上启动一个 Page,并恢复其 AbilitySlice 栈,然后回调 IAbilityContinuation.onRestore 方法,传递此前保存的数据,然后 B 设备的 Page 从 onStart() 生命周期开始进行
  5. 调用 A 设备 Page 以及其所有 AbilitySlice 的 IAbilityContinuation.onCompleteContinuation 方法
  6. 如果迁移发生异常, 系统回调 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概述

HarmonyOS(鸿蒙)—— Ability与页面

鸿蒙应用开发Ability和Android的activity不同之处

鸿蒙OS基础,Ability的生命周期

鸿蒙应用开发Ability介绍-了解和Android的activity不同之处

鸿蒙应用开发Ability介绍-了解和Android的activity不同之处