Ability介绍

官网文档

Ability是应用所具备的抽象能力,是应用程序的重要组成部分。
一个应用可包含多个Ability,HarmonyOS支持以Ability为单位进行部署。

Ability的分类

  1. FA(Feature Ability)

    Page Ability:是FA唯一支持的模板,用于提供给用户交互的能力,可以理解为Android中的Activity

  2. PA(Particle Ability)

    Service Ability:用于提供后台服务运行的能力,可以理解为Android中的Service

    Data Ability:用于对外部提供统一的数据访问抽象,可以理解为Android中的ContentProvider

指定Ability运行的类型

在配置文件config.json(类似于AndroidManifest.xml)中注册Ability时,可以通过配置Ability元素中的“type”属性来指定Ability模板类型。

type的值类型: page、service、data,分别代表Page模板、Service模板、Data模板

例如PageAbility指定为 page:

{
    "module": {
        ...
        "abilities": [
            {
                ...
                "type": "page"
                ...
            }
        ]
        ...
    }
    ...
}

以下就围绕PageAbility、ServiceAbility、DataAbility展开,其中PageAbility中内容是最多的。


Page Ability

PageAbility的概念及基础

官网文档

一、PageAbility与AbilitySlice的关系

PageAbility作为单独的页面与用户交互,一个PageAbility由一个或多个AbilitySlice构成,AbilitySlice是指单个页面及其控制逻辑的总和。PageAbility就好比窗户,AbilitySlice就好比窗花,多个窗花可以贴在同一个窗户上从而可以看到不同的页面。

AbilitySlice:可以称之为子页面,可以存在并复用在其他PageAbility上,每个PageAbility中拥有一个独立的AbilitySlice实例栈,但它不是Android中Fragment的概念(Fraction类似于Android中的Fragment),它强调用于做同Ability下的相同业务(每个AbilitySlice提供的业务能力应具有高度相关性)。

二、AbilitySlice的路由配置

当Page进入前台展示时,需要一个默认的AbilitySlice,
在Ability的onCreate(Intent intent)方法中通过调用方法setMainRoute(String entry)设置默认AbilitySlice。

tips:Ability不需要AbilitySlice也是可以的,把AbilitySlice中的setUIContent(xxx)移至Ability的onStart方法中调用就好了。

其次也可以通过调用方法addActionRoute(String action, String entry),加入其他路由的AbilitySlice。

  1. setMainRoute(String entry):entry指某一AbilitySlice.class.getName()
  2. addActionRoute(String action, String entry):action指自定义跳转意图可使用的的任意字符串;entry同上

例:

public class MyAbility extends Ability {
    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        // set the main route
        setMainRoute(MainSlice.class.getName());

        // set the action route
        addActionRoute("action.pay", PaySlice.class.getName());
        addActionRoute("action.scan", ScanSlice.class.getName());
    }
}
三、页面导航

和Android类似,导航时使用一个名为Intent的类作为信息载体,指定启动目标的同时可以携带相关数据。

Intent的构成元素包括OperationParameters。其中Operation用于指定一些启动参数,Parameters用于传递一些额外参数。

官方列了一张表格来描述Intent的重要字段:

属性子属性描述
OperationAction表示动作,通常使用系统预置Action,应用也可以自定义Action。例如IntentConstants.ACTION_HOME表示返回桌面动作。
Entity表示类别,通常使用系统预置Entity,应用也可以自定义Entity。例如Intent.ENTITY_HOME表示在桌面显示图标。
Uri表示Uri描述。如果在Intent中指定了Uri,则Intent将匹配指定的Uri信息,包括scheme, schemeSpecificPart, authority和path信息。
Flags表示处理Intent的方式。例如Intent.FLAG_ABILITY_CONTINUATION标记在本地的一个Ability是否可以迁移到远端设备继续运行。
BundleName表示包描述。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
AbilityName表示待启动的Ability名称。如果在Intent中同时指定了BundleName和AbilityName,则Intent可以直接匹配到指定的Ability。
DeviceId表示运行指定Ability的设备ID。
Parameters-Parameters是一种支持自定义的数据结构,开发者可以通过Parameters传递某些请求所需的额外信息。

1. PageAbility间的导航

下面演示从了当前AbilitySlice导航跳转到另一个Ability

/**
 * 使用方法:startAbility(Intent intent)
 * BundleName和AbilityName是两个最重要的参数,之间有这样一个规则
 * 
 */
private void startToSecondAbility() {
        Intent intent = new Intent();
        // 方式一:直接指定”BundleName“ 和 ”AbilityName“
//        intent.setElementName("com.bzb.navigationapp", SecondAbility.class);

        // 方式二:使用”Operation“来构建启动参数
        Operation operation = new Intent.OperationBuilder()
		// 基本参数
                .withDeviceId("") // 不指定设备默认为当前设备
                .withBundleName("com.bzb.navigationapp") // 当前包名
                .withAbilityName(SecondAbility.class)

		  // 也可以指定一些其他的参数
//                .withAction(Intent.ACTION_QUERY_WEATHER) // 打开系统action
//                .withFlags(Intent.FLAG_ABILITY_CONTINUATION) // 是否允许迁移流转到其他设备上继续操作
                .build();
        intent.setOperation(operation);

        slice.startAbility(intent);
    }

下面演示了从当前AbilitySlice导航跳转到系统预置的Ability,并接收返回数据

A Ability:
/**
 * 使用方法:startAbilityForResult(Intent intent, int requestCode)
 */
public final static int REQ_CODE_QUERY_WEATHER = 6789;

private void queryWeather() {
    Intent intent = new Intent();
    Operation operation = new Intent.OperationBuilder()
            .withAction(Intent.ACTION_QUERY_WEATHER)
            .build();
    intent.setOperation(operation);
    startAbilityForResult(intent, REQ_CODE_QUERY_WEATHER);
}

@Override
protected void onStart(Intent intent) {
    // 使用Action时,需要在Ability中添加路由,上面已经提到过一次
    addActionRoute(Intent.ACTION_QUERY_WEATHER, DemoSlice.class.getName());
}

@Override
protected void onAbilityResult(int requestCode, int resultCode, Intent resultData) {
   if (requestCode == REQ_CODE_QUERY_WEATHER && resultData != null) {
       resultText.setText(resultData.getStringParam(RESULT_KEY));
   }
}


B Ability:
// ------------如果是跳转自己的Ability,在接收跳转的页面可以通过方法设置返回参数----------------
// setResult(int responseCode, Intent resultIntent)
@Override
protected void onActive() {
    Intent resultIntent = new Intent();
    setResult(0, resultIntent); // 0为当前Ability销毁后返回的resultCode。
}

注意:使用Action时,不仅要在Ability里注册路由,还需要在(config.json)中注册(但据我实测下来,两边均不注册都是可以正常跳转的)

{
    "module": {
        ...
        "abilities": [
            {
                ...
                "skills":[
                    {
                        "actions":[
                            "ability.intent.QUERY_WEATHER"
                        ]
                    }
                ]
                ...
            }
        ]
        ...
    }
    ...
}

2. AbilitySlice间的导航

相比Ability之间的导航,AbilitySlice之间的导航更加简单,不需要在PageAbility和config.json里注册路由。

需要注意的是,在使用present()方法时,第二参数intent不能传null,否则控制台就会报错
AbilitySliceScheduler: Input paras is NULL, jump failed,但并不会产生崩溃。

A:正常跳转使用present(AbilitySlice targetSlice, Intent intent)方法

private void navigationToTargetSlice() {
    present(new TargetSlice(), new Intent());
}

B:需要返回参数跳转使用presentForResult(AbilitySlice targetSlice, Intent intent, int requestCode)方法

public final static int REQ_CODE = 888; 

private void navigationToTargetSlice() {
    presentForResult(new TargetSlice(), new Intent(), REQ_CODE);
}

@Override
protected void onResult(int requestCode, Intent resultIntent) {
    if (requestCode == REQ_CODE) {
        // Process resultIntent here.
    }
}

官方有提到present()和presentForResult()两个方法使用时,对AbilitySlice实例栈的影响规则:

系统为每个Page维护了一个AbilitySlice实例的栈,每个进入前台的AbilitySlice实例均会入栈。当开发者在调用present()或presentForResult()时指定的AbilitySlice实例已经在栈中存在时,则栈中位于此实例之上的AbilitySlice均会出栈并终止其生命周期。前面的示例代码中,导航时指定的AbilitySlice实例均是新建的,即便重复执行此代码(此时作为导航目标的这些实例是同一个类),也不会导致任何AbilitySlice出栈。

PageAbility的生命周期

PageAbility生命周期图解

一、生命周期六个回调:
  1. onStart
  2. onActive
  3. onInactive
  4. onBackground
  5. onForeground
  6. onStop
二、生命周期五种状态:
  1. INITAL
  2. INACTIVE
  3. ACTIVE
  4. INACTIVE
  5. BACKGROUND

你没看错,上面1和4两个是重复的

三、生命周期回调方法详解
  • onStart(Intent intent)

当系统首次创建Page实例时,触发该回调。对于一个Page实例,该回调在其生命周期过程中仅触发一次,Page在该逻辑后将进入INACTIVE状态。开发者必须重写该方法,并在此配置默认展示的AbilitySlice或自行setUIContent,如下所示:

@Override
public void onStart(Intent intent) {
   super.onStart(intent);

   // 以下二者任选其一,通常开发中不会直接使用第二种setUIContent的方式
   super.setMainRoute(FooSlice.class.getName());
   super.setUIContent(ResourceTable.layout_main)
}
  • onActive()

Page会在进入INACTIVE状态后来到前台,然后系统调用此回调。Page在此之后进入ACTIVE状态,该状态是应用与用户交互的状态。Page将保持在此状态,除非某类事件发生导致Page失去焦点,比如用户点击返回键或导航到其他Page。当此类事件发生时,会触发Page回到INACTIVE状态,系统将调用onInactive()回调。此后,Page可能重新回到ACTIVE状态,系统将再次调用onActive()回调。因此,开发者通常需要成对实现onActive()和onInactive(),并在onActive()中获取在onInactive()中被释放的资源。

  • onInactive()

当Page失去焦点时,系统将调用此回调,此后Page进入INACTIVE状态。开发者可以在此回调中实现Page失去焦点时应表现的恰当行为。

  • onBackground()

如果Page不再对用户可见,系统将调用此回调通知开发者用户进行相应的资源释放,此后Page进入BACKGROUND状态。开发者应该在此回调中释放Page不可见时无用的资源,或在此回调中执行较为耗时的状态保存操作。

  • onForeground()

处于BACKGROUND状态的Page仍然驻留在内存中,当重新回到前台时(比如用户重新导航到此Page),系统将先调用onForeground()回调通知开发者,而后Page的生命周期状态回到INACTIVE状态。开发者应当在此回调中重新申请在onBackground()中释放的资源,最后Page的生命周期状态进一步回到ACTIVE状态,系统将通过onActive()回调通知开发者用户。

  • onStop()

系统将要销毁Page时,将会触发此回调函数,通知用户进行系统资源的释放。销毁Page的可能原因包括以下几个方面:

  1. 用户通过系统管理能力关闭指定Page,例如使用任务管理器关闭Page。

  2. 用户行为触发Page的terminateAbility()方法调用,例如使用应用的退出功能。

  3. 配置变更导致系统暂时销毁Page并重建。

  4. 系统出于资源管理目的,自动触发对处于BACKGROUND状态Page的销毁。

四、AbilitySlice的生命周期

AbilitySlice作为PageAbility的组成单元,它的生命周期是依托于所属Page生命周期的,且拥有和Page同样的生命周期回调。

PageAbility的生命周期会直接影响到在前台的AbilitySlice的生命周期,间接影响到在后台的(整个页面销毁的时候)。在同一Page下的AbilitySlice之间互相导航过程中,它们也有着自己独立于Page的生命周期变化,此时Page的状态会一直处于ACTIVE

在AbilitySlice中,开发者必须重写onStart回调方法,且在此方法中通过setUIContent()方法设置页面,如下所示:

@Override
protected void onStart(Intent intent) {
   super.onStart(intent);

   setUIContent(ResourceTable.Layout_main_layout);
}
  1. 在通过startAbility方法启动页面时,AbilitySlice由系统实例化;
  2. 在通过present方法导航时,AbilitySlice由应用自己实例化;

说白了就是一个传class让系统反射创建,一个是自己new实例。

五、PageAbility和AbilitySlice间的生命周期举例

举个例子,当一个DemoPageAbility拥有HeadAbilitySlice和FootAbilitySlice两个AbilitySlice时,当前HeadAbilitySlice正在前台已获得焦点,并即将导航到FootAbilitySlice,此期间的生命周期状态和回调方法顺序如下:

状态顺序

  1. HeadAbilitySlice状态:ACTIVE -> INACTIVE
  2. FootAbilitySlice状态: INITIAL -> INACTIVE -> ACTIVE (如果已启动过则直接到达ACTIVE)
  3. HeadAbilitySlice状态:INACTIVE -> BACKGROUND

方法回调执行顺序

  1. HeadAbilitySlice.onInactive()
  2. FootAbilitySlice.onStart()
  3. FootAbilitySlice.onActive()
  4. HeadAbilitySlice.onBackground()

此过程中,DemoPageAbility状态一直处于ACTIVE,当它的onStop被调用后,此Page下所有的Slice都将被销毁,无论是在后台还是前台。

PageAbility之间的跨设备流转

跨设备流转功能按体验可分为跨端迁移多端协同,能够打破设备界限实现多设备的联动,让使用者可以在多台不同性质的设备上任意切换着使用同一个应用程序。流转功能为产品提供了新的使用场景和体验视角,能够提高用户对产品的使用率和认可度。至于为什么会叫流转,我觉得应该是它能够像水流一样在不同容器间切换时做到无缝衔接。

跨端迁移:

在A端运行的FA(PageAbility)流转到B端上,完成流转后,B端继续使用,A端应用退出

具体使用场景如:开车时将手机导航流转到车机上、视频会议时从手机上流转到智慧屏上 等等…

多端协同:

多端上相同或不同的FA(PageAbility)/ PA(ServiceAbility或DataAbility)同时运行、或者交替运行,共同完成完整的业务流。

具体使用场景如:游戏时手机用于操控,智慧屏用于显示、网课时Pad用作画板,智慧屏用作直播 等等…

跨设备流转功能依靠于HarmonyOS本身设计的分布式技术实现,它的实际体验中远超过像现在的通过WiFi或者蓝牙投屏功能,不仅可以跨屏显示还可以使用原设备的性能进行屏幕操作,也就是跨屏协作的概念。不管设计上或者实现上有多么复杂,对于开发者来说我觉得还是清晰容易的。

HarmonyOS流转架构的优势:

  1. 统一流转管理UI,支持设备发现、选择以及任务管理。
  2. 支持远程服务调用等能力,可轻松设计业务。
  3. 支持多个应用同时进行流转。
  4. 支持不同形态设备,如手机、平板、TV、手表等。

官方提供了流转架构图流转架构图

一、流转到其他设备

调用continueAbility()continueAbilityReversibly()方法发起流转

一次成功的流转大致流程如下:

  1. 从源设备上的PageAbility发起流转请求,并与目标设备建立分布式连接,回调onStartContinuation()方法结果返回是否允许流转
  2. HarmonyOS处理流转任务,回调源设备上PageAbility的onSaveData()方法保存流转后的必要数据
  3. HarmonyOS在目标设备上启动同一个PageAbility,回调目标设备上的onRestoreData()方法恢复原有数据。此后目标设备上的Page从onStart()开始其生命周期回调
  4. 流转恢复数据结束后,回调源设备上的onCompleteContinuation()方法通知数据恢复的结果
二、流转功能的接口IAbilityContinuation和必要权限:

需要支持设备流转的PageAbility和对应包含的AbilitySlice都需要实现IAbilityContinuation接口

并注册相关权限

ohos.permission.GET_DISTRIBUTED_DEVICE_INFO:用于允许获取分布式组网内的设备列表和设备信息
ohos.permission.DISTRIBUTED_DATASYNC:用于允许不同设备间的数据交换
ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE:用于允许监听分布式组网内的设备状态变化
ohos.permission.READ_USER_STORAGE:读取存储卡中的内容
ohos.permission.WRITE_USER_STORAGE:修改或删除存储卡中的内容
ohos.permission.GET_BUNDLE_INFO:用于查询其他应用的信息
ohos.permission.servicebus.ACCESS_SERVICE:分布式数据传输的权限
com.huawei.hwddmp.servicebus.BIND_SERVICE:系统应用使用权限

该接口包含以下 五个 || 六个 回调方法:

package ohos.aafwk.ability;

import ohos.aafwk.content.IntentParams;

// 此接口定义是版本 SDK Api Version 5,6当中新增了一个onFailedContinuation回调,目前6为Beta版
public interface IAbilityContinuation {
    boolean onStartContinuation();

    boolean onSaveData(IntentParams var1);

    boolean onRestoreData(IntentParams var1);

    void onCompleteContinuation(int var1);

    default void onRemoteTerminated() {
        throw new RuntimeException("Stub!");
    }
}
  • boolean onStartContinuation()

Page请求迁移后,系统首先回调此方法。开发者可以在此回调中决策当前是否可以执行迁移,比如:弹框让用户确认是否开始迁移。

  • boolean onSaveData(IntentParams intentParams)

onStartContinuation()返回true时,系统会回调此方法。开发者在此回调中保存必须传递到另外设备上以便恢复Page状态的数据。

  • boolean onRestoreData(IntentParams intentParams)

源设备上的Page保存数据完成后,系统会在目标设备上回调此方法。开发者在此回调中接受用于恢复Page状态的数据。

注意,在目标设备上的Page会重新启动其生命周期,无论其启动模式如何配置。且系统回调此方法的时机在onStart()之前。

  • void onCompleteContinuation(int result)

目标设备上恢复数据一旦完成,系统就会在源设备上回调Page的此方法,以便通知应用整个流转的流程已结束。开发者可以在此检查流转结果是否成功,并可以在此处理流转结束的收尾工作。例如:应用可以在流转完成后主动结束源设备上应用的生命周期。

注意:此方法的调用不受流转结果成功或失败的影响,参数result为流转结果,猜测应该是 0、1

  • void onRemoteTerminated()

如果使用continueAbilityReversibly()而不是continueAbility(),则此后可以在源设备上使用reverseContinueAbility()进行回流。这种场景下,相当于同一个Page(的两个实例)同时在两个设备上运行,流转完成后,如果目标设备上Page因任何原因终止,则源设备上的Page通过此回调接收终止通知。

以下方法从 SDK API Version 6 开始提供,目前为Beta版本。

  • void onFailedContinuation()

流转过程中发生异常,系统会在源设备上回调Page的此方法,以便通知应用流转过程中发生的异常。并不是所有异常都会回调Page的此方法,仅局限于该接口枚举的异常。开发者可以在此检查异常信息,并在此处理流转异常发生后的动作。例如:应用可以提醒用户此时发生的异常信息。

三、流转回流

从源设备流转到目标设备后,目标设备可以进行回流到源设备上继续操作

请求回流需在设备A上调用 reverseContinueAbility()方法

try {
    reverseContinueAbility();
} catch (IllegalStateException e) {
    // 防止有其他流转正在进行
    ...
}

一次成功的回流流程大致如下:

这里的源设备和目标设备还是对应刚刚那两台,身份并无变化

  1. 源设备上的Page请求回流,系统回调目标设备上Page所有AbilitySlice实例的onStartContinuation()方法,以确认当前是否可以立即回流。
  2. 如果可以立即回流,则系统回调目标设备上Page所有AbilitySlice实例的onSaveData()方法,以便保存回流后恢复状态必须的数据。
  3. 如果保存数据成功,则系统在源设备上的Page恢复AbilitySlice栈,然后回调onRestoreData()方法,传递此前保存的数据。
  4. 数据恢复成功,系统终止目标设备上Page的生命周期。

跨设备流转无论是在用户体验上还是开发体验上都还是很棒的,系统自行管理着多台设备的生命周期,开发者也不需要过多关心设备之间的交互,前几日鸿蒙开发者日直播上演示了9台手机播放同一个视频,协作能力远超想象


Service Ability

官网文档

ServiceAbility的概念

ServiceAbility基于Service模板,不提供前台UI显示和操作,主要用于后台中运行任务(如:音乐播放、文件下载)。ServiceAbility可以由其他的应用或Ability启动,运行的生命周期也不受应用切换的影响。

ServiceAbility是单实例的,在多个Ability共用一个Service实例时,只有所有Ability都退出后,Service才能够退出。
和Android中的Service一样,它运行在主线程,耗时操作须放进子线程处理,否则容易产生ANR。

ServiceAbility的生命周期

和Android类似,Service的两种启动方式对应两种不同的生命周期:

  • startAbility

  • connectAbility

connectAbility()也可以连接通过startAbility()创建的Service。

两种启动方式具体有什么区别或者说具体有什么不同的使用场景呢,我在51cto的HarmonyOS技术社区也提出了这一疑问,可能是时间原因暂时还未收到比较理想的答案。我理解了一下官方文档中描述的一些内容,大概可以说是startAbility的方式适用于一次性任务、不需要关心创建结果的场景;connectAbility的方式适用于与UI交互频繁、与其他应用的Service交互、关注创建结果的场景。

官方提供了生命周期图生命周期图

生命周期回调详解:

  • onStart()

在创建Service的时候调用,用于Service的初始化。在Service的整个生命周期只会调用一次,调用时传入的Intent应为空。

  • onCommand()

在Service创建完成之后调用,该方法在客户端每次启动该Service时都会调用。开发者可以在该方法中做一些调用统计、初始化类的操作。

  • onConnect​()

在Ability和Service连接时调用,该方法返回IRemoteObject对象,开发者可以在该回调函数中生成对应Service的IPC通信通道,以便Ability与Service交互。Ability可以多次连接同一个Service,系统会缓存该Service的IPC通信对象,只有第一个客户端连接Service时,系统才会调用Service的onConnect方法来生成IRemoteObject对象,而后系统会将同一个RemoteObject对象传递至其他连接同一个Service的所有客户端,而无需再次调用onConnect方法。

  • onDisconnect​()

在Ability与绑定的Service断开连接时调用。

  • onStop()

在Service销毁时调用。Service应通过实现此方法来清理任何资源,如关闭线程、注册的侦听器等。

ServiceAbility的使用

一、创建Service

在HarmonyOS里,Service也是一种Ability,所以继承Ability类和重写生命周期方法即可

public class MyServiceAbility 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();
    }
}

同样Service需要在配置文件里注册

{
    "module": {
        "abilities": [         
            {    
                "name": ".MyServiceAbility",
                "type": "service",
                "visible": true
                ...
            }
        ]
        ...
    }
    ...
}

二、startAbility Service
  • 启动

这是第一种启动方式,由于继承自Ability所以启动ServiceAbility和启动PageAbility几乎没有任何区别。通过Intent能够启动本地和远端的ServiceAbility。

三个重要参数:

Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("")
// 	.withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE) // 如果传入deviceId,就需要设置此支持分布式调度系统多设备启动的标识
        .withBundleName("com.domainname.hiworld.himusic")
        .withAbilityName("com.domainname.hiworld.himusic.ServiceAbility")
        .build();
intent.setOperation(operation);
startAbility(intent);

通过startAbility方法启动service时,生命周期有两种情况:

  • 如果Service尚未运行,系统会先调用onStart()来初始化Service,再调用onCommand()来启动Service
  • 如果Service已经运行,系统会直接调用onCommand()方法来启动Service
  • 停止

Service一旦创建就会一直保持在后台运行,除非必须回收内存资源,否则系统不会停止或销毁Service。
停止Service同样支持停止本地设备Service和停止远程设备Service,使用方法与启动Service一样。一旦调用停止Service的方法,系统便会尽快销毁Service,以下是两种停止Service的方法:

  • terminateAbility():用于当前的service停止

  • stopAbility():可以在任何Ability停止远端的service

三、connectAbility Service

这是启动Service的第二种方式,在需要与PageAbility或者其他应用的ServiceAbility交互时,必须通过此方式启动,给到Service回调Client的接口。

在使用connectAbility()处理回调时,需要传入目标Service的Intent与IAbilityConnection的实例。IAbilityConnection提供了两个方法供开发者实现:onAbilityConnectDone()是用来处理连接Service成功的回调,onAbilityDisconnectDone()是用来处理Service异常死亡的回调。

以下是相关connectAbility Service的示例代码:

// 创建连接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) {
    }
};

// connectService
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);

// 创建自定义IRemoteObject实现类
private class MyRemoteObject extends LocalRemoteObject {
    MyRemoteObject(){ }
}

// 在Service的onConnect方法中,把IRemoteObject返回给客户端
@Override
protected IRemoteObject onConnect(Intent intent) {
    return new MyRemoteObject();
}

前台Service

Service默认是运行在后台的,优先级比较低,当系统资源不足的时候正在运行的Service也可能被系统回收。

在某些场景下,Service也需要有前台的感应存在,如音乐播放需要任务常驻并体现在通知栏和状态栏,于是就需要前台Service出场了。

前台Service的使用也很方便,只需要在onStart中使用keepBackgroundRunning()方法绑定Notification到Service,再加上对应权限即可,以下演示为步骤和代码:

  1. 在Service的onStart中构建Notification,并绑定到Service
@Override
public void onStart(Intent intent) {
   super.onStart(intent);
   // 创建通知
   NotificationRequest request = new NotificationRequest(1005);
   NotificationRequest.NotificationNormalContent content = new NotificationRequest.NotificationNormalContent();
   content.setTitle("我是通知的title").setText("我是通知的content");
   NotificationRequest.NotificationContent notificationContent = new NotificationRequest.NotificationContent(content);
   request.setContent(notificationContent);

   // 绑定通知,1005为创建通知时传入的notificationId
   keepBackgroundRunning(1005, request);
}

@Override
protected void onStop() {
    super.onStop();
    cancelBackgroundRunning​(); // 在stop中可停止Service
}
  1. 在config.json中配置Service,添加权限和backgroundModes
{
    "module": {
        "abilities": [         
            {    
    		"name": ".ServiceAbility",
    		"type": "service",
    		"visible": true,
		"permissions": ["ohos.permission.KEEP_BACKGROUND_RUNNING"],
    		"backgroundModes": ["dataTransfer", "location"]
	    }
        ]
        ...
    }
    ...
}


Data Ability

官网文档

DataAbility用于应用自身和其他应用的数据访问和共享,支持同设备不同应用之间的数据交互,也支持不同设备之间不同应用之间的数据交互。

数据来源可以是设备本地数据库也可以是设备本地文件,通过DataAbility对外提供自定义的增、删、改、查、文件浏览 等接口实现数据通信。

URI介绍

URI(Uniform Resource Identifier)作为双端的交互协议,HarmonyOS在设计上也基于URI的通用标准,协议格式如下图:

协议格式

URI示例:

  • 本地设备:dataability:///com.domainname.dataability.persondata/person/10
  • 跨设备场景:dataability://device_id/com.domainname.dataability.persondata/person/10

tips:本地设备的“device_id”字段为空,因此在“dataability:”后面有三个“/”。

格式参数详解

  1. scheme:协议方案名,固定为“dataability”,代表DataAbility所使用的协议类型。
  2. authority:设备ID。如果为跨设备场景,则为目标设备的ID;本地设备则不需要填写。
  3. path:资源的路径信息,代表特定资源的位置信息。
  4. query:查询参数。
  5. fragment:可以用于指示要访问的子资源。

创建Data

DataAbility也属于Ability的一种,因此同样通过继承Ability和实现相关方法来创建,以下展示了Data创建的步骤。

一、确定数据存储的方式

Data支持以下两种数据形式:

  • 文件数据:文本、图片、音乐 等等…
  • 结构化数据:数据库(HarmonyOS的本地数据库也是基于SQLite关系型的,但不限于此) 等等…
二、继承Ability并实现相关方法

Data提供了文件存储和数据库存储两组接口供用户使用。

开发者需要在Data中重写FileDescriptor#openFile​(Uri uri, String mode)方法来操作文件:

uri:客户端传入的请求目标路径;

mode:为开发者对文件的操作选项,可选方式包含“r”(读), “w”(写), “rw”(读写) 等

ohos.rpc.MessageParcel类提供了一个静态方法,用于获取MessageParcel实例。开发者可通过获取到的MessageParcel实例,使用dupFileDescriptor()函数复制待操作文件流的文件描述符,并将其返回,供远端应用访问文件。

官方示例:根据传入的uri打开对应的文件

private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");

@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);
}

1. 初始化数据库连接

系统会在应用启动时调用onStart()方法来创建Data实例。在此方法中,可以创建数据库建立连接,以便后续和数据库进行操作。为了避免影响应用启动速度,应尽可能将非必要的耗时任务推迟到使用时执行,而不是在此方法中执行所有初始化。

官方示例:初始化的时候连接数据库

private static final String DATABASE_NAME = "UserDataAbility.db";
private static final String DATABASE_NAME_ALIAS = "UserDataAbility";
private static final HiLogLabel LABEL_LOG = new HiLogLabel(HiLog.LOG_APP, 0xD00201, "Data_Log");
private OrmContext ormContext = null;

@Override
public void onStart(Intent intent) {
    super.onStart(intent);
    ormContext = new DatabaseHelper(this).getOrmContext(DATABASE_NAME_ALIAS, DATABASE_NAME, BookStore.class);
}

2. 实现数据库操作的方法

Ability定义了6个方法供用户处理对数据库表数据的增删改查。这6个方法在Ability中已默认实现,开发者可按需重写。

方法描述
ResultSet query​(Uri uri, String[] columns, DataAbilityPredicates predicates)查询数据库
int insert​(Uri uri, ValuesBucket value)向数据库中插入单条数据
int batchInsert​(Uri uri, ValuesBucket[] values)向数据库中插入多条数据
int delete​(Uri uri, DataAbilityPredicates predicates)删除一条或多条数据
int update​(Uri uri, ValuesBucket value, DataAbilityPredicates predicates)更新数据库
DataAbilityResult[] executeBatch​(ArrayList operations)批量操作数据库

方法详解

  • ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates)
    • uri:查询路径
    • columns:查询列名
    • predicates:查询条件
    • @return:查询结果
public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) {
    if (ormContext == null) {
        HiLog.error(LABEL_LOG, "failed to query, ormContext is null");
        return null;
    }

    // 查询数据库
    OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates, User.class);
    ResultSet resultSet = ormContext.query(ormPredicates, columns);
    if (resultSet == null) {
        HiLog.info(LABEL_LOG, "resultSet is null");
    }

    // 返回结果
    return resultSet;
}
  • int insert​(Uri uri, ValuesBucket value)
    • uri:查询路径
    • value:插入的数据封装对象
    • @return:插入结果,失败返回-1,成功返回行号
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);
    return Math.toIntExact(user.getRowId());
}
  • int insert​(Uri uri, ValuesBucket[] value)

看方法名就知道用于批量插入,接收一个ValuesBucket数组用于单次插入一组对象。它的作用是提高插入多条重复数据的效率。该方法系统已实现,开发者可以直接调用。

  • int delete​(Uri uri, DataAbilityPredicates predicates)

此方法用于执行删除操作。删除条件由类DataAbilityPredicates构建,服务端在接收到该参数之后可以从中解析出要删除的数据,然后到数据库中执行。根据传入的条件删除用户表数据的代码示例如下:

public int delete(Uri uri, DataAbilityPredicates predicates) {
    if (ormContext == null) {
        HiLog.error(LABEL_LOG, "failed to delete, ormContext is null");
        return -1;
    }

    OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
    int value = ormContext.delete(ormPredicates);
    DataAbilityHelper.creator(this, uri).notifyChange(uri);
    return value;
}
  • int update​(Uri uri, ValuesBucket value, DataAbilityPredicates predicates)

此方法用来执行更新操作。用户可以在ValuesBucket参数中指定要更新的数据,在DataAbilityPredicates中构建更新的条件等。更新用户表的数据的代码示例如下:

public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) {
    if (ormContext == null) {
       HiLog.error(LABEL_LOG, "failed to update, ormContext is null");
       return -1;
   }

   OrmPredicates ormPredicates = DataAbilityUtils.createOrmPredicates(predicates,User.class);
   int index = ormContext.update(ormPredicates, value);
   HiLog.info(LABEL_LOG, "UserDataAbility update value:" + index);
   DataAbilityHelper.creator(this, uri).notifyChange(uri);
   return index;
}
  • executeBatch()

此方法用来批量执行操作。DataAbilityOperation中提供了设置操作类型、数据和操作条件的方法,用户可自行设置自己要执行的数据库操作。该方法系统已实现,开发者可以直接调用。

三、在配置文件注册DataAbility

需要关注以下属性:

  1. type: 类型设置为data
  2. uri: 对外提供的访问路径,全局唯一
  3. permissions: 访问该DataAbility时需要申请的访问权限,非系统文件需要在配置文件中自定义,参考 权限开发指导 中关于“自定义权限”的相关说明。
{
    "name": ".UserDataAbility",
     "type": "data",
     "visible": true,
     "uri": "dataability://com.example.myapp.DataAbilityTest",
     "permissions": [
        "com.example.myapp.DataAbility.DATA"
     ]
}

访问Data

开发者可以通过DataAbilityHelper类来访问当前应用或其他应用提供的共享数据。DataAbilityHelper作为客户端,与提供方的Data进行通信。Data接收到请求后,执行相应的处理,并返回结果。DataAbilityHelper提供了一系列与Data Ability对应的方法。
下面介绍DataAbilityHelper具体的使用步骤。

一、声明使用权限

如果待访问的Data声明了访问需要权限,则访问此Data需要在配置文件中声明需要此权限。声明请参考 权限申请字段说明

"reqPermissions": [
    {
        "name": "com.example.myapplication5.DataAbility.DATA"
    },

    // 访问文件还需要添加访问存储读写权限
    {
        "name": "ohos.permission.READ_USER_STORAGE"
    },
    {
        "name": "ohos.permission.WRITE_USER_STORAGE"
    }
]

二、创建DataAbilityHelper

DataAbilityHelper为开发者提供了creator()方法来创建DataAbilityHelper实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个context对象来创建DataAbilityHelper对象。

创建示例:

DataAbilityHelper helper = DataAbilityHelper.creator(this);
三、访问DataAbility

无论是访问文件还是数据库,都通过DataAbilityHelper进行操作,在前面创建时已经提到过部分内容了,接下来重复的东西就不再列出来了。

  • 访问文件
    DataAbilityHelper为开发者提供了FileDescriptor#openFile​(Uri uri, String mode)方法来操作文件。该方法返回一个目标文件的FD(文件描述符),把文件描述符封装成流,开发者就可以对文件流进行自定义处理。
  • uri: 用来确定目标资源路径
  • mode:用来指定打开文件的方式,可选方式包含“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)。

访问文件示例:

// 读取文件描述符
FileDescriptor fd = helper.openFile(uri, "r");
FileInputStream fis = new FileInputStream(fd);

// 使用文件描述符封装成的文件流,进行文件操作
...
  • 访问数据库

数据库访问还是上面提到过的那六个方法,着重看一下条件构造器DataAbilityPredicatesexecuteBatch方法

  • query()
// 构造查询条件,使用 between 指定查询区间
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.between("userId", 101, 103);

// 进行查询
ResultSet resultSet = helper.query(uri, columns, predicates);

// 处理结果
resultSet.goToFirstRow();
do {
    // 在此处理ResultSet中的记录;
} while(resultSet.goToNextRow());
  • insert()
// 构造插入数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.insert(uri, valuesBucket);
  • update()
// 构造更新条件
DataAbilityPredicates predicates = new DataAbilityPredicates();
predicates.equalTo("userId", 102);

// 构造更新数据
ValuesBucket valuesBucket = new ValuesBucket();
valuesBucket.putString("name", "Tom");
valuesBucket.putInteger("age", 12);
helper.update(uri, valuesBucket, predicates);
  • executeBatch()

批量操作使用DataAbilityOperation来构建条件,最后放入list传入方法进行操作

DataAbilityHelper helper = DataAbilityHelper.creator(abilityObj, insertUri);

// 构造批量操作
ValuesBucket value1 = initSingleValue();
ValuesBucket value2 = initSingleValue2();

DataAbilityOperation opt1 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value1).build();
DataAbilityOperation opt2 = DataAbilityOperation.newInsertBuilder(insertUri).withValuesBucket(value2).build();

ArrayList<DataAbilityOperation> operations = new ArrayList<DataAbilityOperation>();
operations.add(opt1);
operations.add(opt2);
DataAbilityResult[] result = helper.executeBatch(insertUri, operations);

以上的内容还是非常多的,我把鸿蒙系统中设计的Ability的重要内容都提取了出来,对于我们Android开发者而言,更多需要理解的是一些和Android中概念几乎相通但命名不一样或者说是一些更加优秀的设计。理解了这些,在已有Android开发基础的情况下开发鸿蒙应用就非常容易了,甚至可以说比学Jetpack这套更容易。学习鸿蒙的过程中,我体验到了阅读国产文档的快乐,这是梦寐以求已久的,谁说的国产的东西不好使,我就觉得挺好的。鸿蒙把分布式的概念带进了客户端,解决了传统移动平台难以实现的多设备协作功能,也解决了小内存设备无法使用大应用的问题,当然鸿蒙的优势不仅仅只有这些。以上的知识也只是上层中的冰山一角,还有其他有趣的东西例如原子化的服务卡片平行视界智能穿戴设备智能家居和智能出行等我后续文章。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐