晨曦_cyeupk · 2020年09月25日

鸿蒙 Ability 讲解(页面生命周期、后台服务、数据访问)

鸿蒙开发核心之Ability详解

一、Ability用途

  在知道用法之前,首先你是不是得知道这个Ability怎么读?对了,Ability (音译 :阿B了D),中文意思就是能力,不要给我扯什么音标啥的,不好使,你仔细想一下,你是因为英语学得好才来当程序员的吗?To young to simple!
  Ability 是应用所具备能力的抽象,也是应用程序的重要组成部分。一个应用可以具备多种能力(即可以包含多个 Ability),HarmonyOS 支持应用以 Ability 为单位进行部署。Ability 可以分为 FA(Feature Ability)和 PA(Particle Ability)两种类型,每种类型为开发者提供了不同的模板,以便实现不同的业务功能。
  从上面一段文字,去其糟粕,取其精华之后就是两点。FA(Feature Ability)PA(Particle Ability)

  FA(Feature Ability) (音译:非ture 阿B了D),中文意思是功能能力,它支持Page Ability 页面能力用于提供与用户交互的能力。一个Page 可以由一个或多个 AbilitySlice 构成,AbilitySlice 是指应用的单个页面及其控制逻辑的总和。
在这里插入图片描述
在这里插入图片描述

  一个 Page 可以包含多个 AbilitySlice,但是 Page 进入前台时界面默认只展示一个AbilitySlice。默认展示的 AbilitySlice 是通过 setMainRoute() 方法来指定的。如果需要更改默认展示的 AbilitySlice,可以通过 addActionRoute() 方法为此 AbilitySlice 配置一条路由规则。此时,当其他 Page 实例期望导航到此 AbilitySlice 时,可以在 Intent 中指定 Action。addActionRoute() 方法中使用的动作命名,需要在应用配置文件(config.json)中注册:
比如
在这里插入图片描述

  PA(Particle Ability) (音译:趴踢扣 阿B了D),这个里面也是支持两个能力, Service AbilityData Ability 我相信你知道它们的意思,就是服务能力和数据能力。Service用于提供后台运行任务的能力。Data 用于对外部提供统一的数据访问抽象。在配置文件(config.json)中注册 Ability 时,可以通过配置 Ability 元素中的“type”属性来指定 Ability 模板类型,示例如下。其中,“type”的取值可以为“page”、“service”或“data”,分别代表 Page 模板、Service 模板、Data 模板。结合下面这个图来看知道是怎么回事了,type的属性值取决于你创建Ability是选择的类型,当然你也可以后面再改。
在这里插入图片描述

二 、Page Ability讲解

现在我们知道这个Page Ability是主要负责页面交互的,那么就可以理解为Android 的Activity。那么都知道Activity有生命周期,同样的Page Ability也是的。下面来看看它的生命周期。

① Page Ability 生命周期

首先来看官方的一张图
在这里插入图片描述
重点看蓝色方框的。粉红色的是当前应用的状态。
声明周期分别是onStart()onActive()onInactive()onBackground()onForeground()onStop()

下面来看看详细的解释

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

在这里插入图片描述
和onCreate有点像。

  • onActive()Page Ability会在进入 INACTIVE 状态后来到前台,然后系统调用此回调。Page Ability 在此之后进入ACTIVE 状态,该状态是应用与用户交互的状态。Page Ability将保持在此状态,除非某类事件发生导致 Page Ability失去焦点,比如用户点击返回键或导航到其他 Page Ability。当此类事件发生时,会触发Page Ability回到 INACTIVE 状态,系统将调用 onInactive() 回调。此后,Page Ability可能重新回到ACTIVE 状态,系统将再次调用 onActive() 回调。因此,开发者通常需要成对实现 onActive()onInactive(),并在 onActive() 中获取在 onInactive() 中被释放的资源。类似于Android的onResume。
  • onInactive()Page Ability失去焦点时,系统将调用此回调,此后 Page 进入 INACTIVE 状态。开发者可以在此回调中实现 Page 失去焦点时应表现的恰当行为。类似于Android的onPause和onStop的集合体。
  • onBackground() 如果 Page Ability不再对用户可见,系统将调用此回调通知开发者用户进行相应的资源释放,此后Page Ability进入 BACKGROUND 状态。开发者应该在此回调中释放 Page Ability 不可见时无用的资源,或在此回调中执行较为耗时的状态保存操作。
  • onForeground() 处于 BACKGROUND 状态的 Page Ability仍然驻留在内存中,当重新回到前台时(比如用户重新导航到此 Page Ability),系统将先调用 onForeground()回调通知开发者,而后 Page 的生命周期状态回到 INACTIVE 状态。开发者应当在此回调中重新申请在 onBackground()中释放的资源,最后 Page 的生命周期状态进一步回到 ACTIVE 状态,系统将通过 onActive()回调通知开发者用户。
  • onStop() 系统将要销毁 Page Ability时,将会触发此回调函数,通知用户进行系统资源的释放。销毁 Page 的可能原因包括以下几个方面:

▪ 用户通过系统管理能力关闭指定 Page Ability,例如使用任务管理器关闭 Page Ability
▪ 用户行为触发 Page Ability的 terminateAbility()方法调用,例如使用应用的退出功能。
▪ 配置变更导致系统暂时销毁 Page Ability并重建。
▪ 系统出于资源管理目的,自动触发对处于 BACKGROUND 状态 Page Ability的销毁。

OK,Page Ability 的生命周期就讲完了,具体要熟悉的话还是从实际开发中获取才行。

② AbilitySlice 生命周期

先来看下面这张图
在这里插入图片描述
  说实话一开始创建项目的时候就只有这个MainAbilityHelloWorld以及slice包下的MainAbilitySlice,后来新建了一个SecondAbility,而SecondAbilitySlice是自动生成的,这说明一个问题,它们之间有不可告人的秘密。那么下面就戳穿这个秘密,摊牌了,它们是一对好基友。

解释:AbilitySlice 作为 Page Ability的组成单元,其生命周期是依托于其所属 Page Ability生命周期的。AbilitySlicePage Ability具有相同的生命周期状态和同名的回调,当 Page Ability生命周期发生变化时,它的 AbilitySlice 也会发生相同的生命周期变化。此外,AbilitySlice 还具有独立于 Page Ability的生命周期变化,这发生在同一 Page Ability中的 AbilitySlice 之间导航时,此时 Page Ability的生命周期状态不会改变。AbilitySlice 生命周期回调与 Page Ability的相应回调类似,因此不再赘述。由于 AbilitySlice 承载具体的页面,开发者必须重写 AbilitySliceonStart()回调,并在此方法中通过 setUIContent()方法设置页面,如下所示:
在这里插入图片描述
Page 与 AbilitySlice 生命周期关联

  当 AbilitySlice 处于前台且具有焦点时,其生命周期状态随着所属 Page Ability的生命周期状态的变化而变化。当一个 Page Ability
有多个 AbilitySlice 时,例如:MyAbility 下有 FooAbilitySliceBarAbilitySlice,当前 FooAbilitySlice 处于前台并获得焦点,并即将导航到 BarAbilitySlice,在此期间的生命周期状态变化顺序为:

  1. FooAbilitySlice 从 ACTIVE 状态变为 INACTIVE 状态。
  2. BarAbilitySlice 则从 INITIAL 状态首先变为 INACTIVE 状态,然后变为 ACTIVE 状态(假定此前 BarAbilitySlice 未曾

启动)。

  1. FooAbilitySlice 从 INACTIVE 状态变为 BACKGROUND 状态。对应两个 slice 的生命周期方法回调顺序为:

FooAbilitySlice.onInactive() --> BarAbilitySlice.onStart() --> BarAbilitySlice.onActive() -
-> FooAbilitySlice.onBackground()
在整个流程中,MyAbility 始终处于 ACTIVE 状态。但是,当 Page Ability被系统销毁时,其所有已
实例化的 AbilitySlice 将联动销毁,而不仅是处于前台的 AbilitySlice。

三、Service Ability讲解

  先来看一下Service Ability的官方解释基于 Service 模板的 Ability(以下简称“Service”)主要用于后台运行任务(如执行音乐播放、文件下载等),但不提供用户交互界面。Service 可由其他应用或 Ability 启动,即使用户切换到其他应用,Service 仍将在后台继续运行。
  Service 是单实例的。在一个设备上,相同的 Service 只会存在一个实例。如果多个 Ability 共用这个实例,只有当与 Service 绑定的所有 Ability 都退出后,Service 才能够退出。由于Service 是在主线程里执行的,因此,如果在 Service 里面的操作时间过长,开发者必须在Service 里创建新的线程来处理,防止造成主线程阻塞,应用程序无响应。其实和Android的Service有点像。
下面创建一个Service,右键你的包名 → New → Ability → Empty Service Ability。如下图所示
在这里插入图片描述
然后
在这里插入图片描述
上面的这个Visible你如果勾选上就是你的这个Service对其他应用程序可见,而Enable background mode表示后台模式,如果你打开这个开关,就表示你的Service要在后台运行,还可以自己选择你要在后台干嘛。
在这里插入图片描述
这个我翻译一下
在这里插入图片描述

这里你就必须要选一个,不选就不能创建这个Service Ability。下面我们就直接创建,不勾选,不打开。创建Service Ability不会生成AbilitySlice。
在这里插入图片描述
这个时候在config.json文件中会自动生成相关的属性。
在这里插入图片描述
可以看到相比于Page ,Service的属性要少一些,而且type的属性值是“service”。

① Service Ability 生命周期

下面看一下ServiceAbility的代码:

package com.llw.helloworld;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.rpc.IRemoteObject;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;

public class ServiceAbility extends Ability {
    private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "Demo");

    @Override
    public void onStart(Intent intent) {
        HiLog.error(LABEL_LOG, "ServiceAbility::onStart");
        super.onStart(intent);
    }

    @Override
    public void onBackground() {
        super.onBackground();
        HiLog.info(LABEL_LOG, "ServiceAbility::onBackground");
    }

    @Override
    public void onStop() {
        super.onStop();
        HiLog.info(LABEL_LOG, "ServiceAbility::onStop");
    }

    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
    }

    @Override
    public IRemoteObject onConnect(Intent intent) {
        return null;
    }

    @Override
    public void onDisconnect(Intent intent) {
    }
}

生命周期:onStart()onCommand()onConnect()onDisconnect()onStop()
单个讲解

  • onStart() 该方法在创建 Service 的时候调用,用于 Service 的初始化,在 Service 的整个生命周期只会调用一次。
  • 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 应通过实现此方法来清理任何资源,如关闭线程、注册的侦听器等。

② 启动Service Ability

  Ability 为开发者提供了startAbility() 方法来启动另外一个 Ability。因为 Service 也是 Ability的一种,开发者同样可以通过将 Intent 传递给该方法来启动 Service。不仅支持启动本地Service,还支持启动远程 Service。
  开发者可以通过构造包含 DeviceId、BundleName 与 AbilityName 的 Operation 对象来设置目标 Service 信息。这三个参数的含义如下:

  • DeviceId:表示设备 ID。如果是本地设备,则可以直接留空;如果是远程设备,可以通过ohos.distributedschedule.interwork.DeviceManager 提供的 getDeviceList 获取设备列表。
  • BundleName:表示包名称。
  • AbilityName:表示待启动的 Ability 名称。

下面用代码来实践一下,比如我现在要在MainAbilitySlice的onStart方法中启动ServiceAbility。就可以这么写

    /**
     * 启动本地服务
     */
    private void startupLocalService() {
        Intent intent = new Intent();
        //构建操作方式
        Operation operation = new Intent.OperationBuilder()
                // 设备id
                .withDeviceId("")
                // 应用的包名
                .withBundleName("com.llw.helloworld")
                // 跳转目标的路径名  通常是包名+类名
                .withAbilityName("com.llw.helloworld.ServiceAbility")
                .build();
        //设置操作
        intent.setOperation(operation);
        startAbility(intent);
    }

然后在onStart中调用即可。
在这里插入图片描述
那么怎么证明ServiceAbility是启动了呢?很简单,我们只要在ServiceAbility的onStart方法中打印一个日志就可以了。进入到ServiceAbility,你会发现创建的时候就给你写好了日志。
在这里插入图片描述
那么现在启动远程模拟器,然后运行HelloWorld。进入到主页
在这里插入图片描述
那么这个时候Service已经启动了,通过日志来看看,点击编译器下面的HiLog栏目,然后输入Demo,就能找到这个日志了。
在这里插入图片描述
那么现在我们就启动了这个本地的Service,那么如何启动远程的Service呢?

    private void startupRemotelyService() {
        Intent intent = new Intent();
        Operation operation = new Intent.OperationBuilder()
                .withDeviceId("deviceId")
                .withBundleName("com.huawei.hiworld.himusic")
                .withAbilityName("com.huawei.hiworld.himusic.entry.ServiceAbility")
                // 设置支持分布式调度系统多设备启动的标识
                .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                .build();
        intent.setOperation(operation);
        startAbility(intent);
    }

  远程启动Service可以这么写,但是有一点你要确认,那就是你启动的这个服务是否允许其他应用程序发现?否则你就算知道这个服务的包名和类名也是白搭。还记得刚才在创建Service Ability的时候的Visible吗?勾选就是允许,默认是没有勾选的。那么我又想去勾选了咋办?难道我现在重新创建一个再勾选上?感觉这样是可以的,但是太蠢了。不够优雅。既然你也不知道怎么搞,我也不知道怎么搞,那么就实验一下,比如我再创建一个ServiceAbility。这里设置名为ServiceAbility2,然后勾选一下Visible,然后我们到config.json配置文件中去看之前的没有勾选的Service有啥不同。
在这里插入图片描述
  现在你是不是就有种恍然大明白的感觉了。只要通过加一个visible的属性,设置为true,就可以了,如果没有这个属性,就是默认为false。OK,那么这就解决了这个启动Service的问题。

通过 startAbility() 方法来启动 Service。

  • 如果 Service 尚未运行,则系统会先调用 onStart()来初始化 Service,再回调 Service 的 onCommand()方法来启动Service。刚才我们并没有看到有打印onCommand,是因为它里面没有方法。那么现在我在这个onCommand方法里面也加一个日志,然后重新运行一下
    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
        HiLog.error(LABEL_LOG, "ServiceAbility::onCommand");
    }

在这里插入图片描述

  • 如果 Service 正在运行,则系统会直接回调 Service 的 onCommand()方法来启动 Service。这个场景需要先返回到设备主页面,然后再打开这个应用,首先返回主页面,点击右边的圆形按钮

在这里插入图片描述
设备主页,这时候Service在后台运行,然后再点一下圆形按钮,进入到应用页面。
在这里插入图片描述
这里是应用页面,目前只有一个新增的应用,其他两个是系统应用,这里是一个列表,你可以通过鼠标按住左键上下进行拖动。然后点击这个HelloWorld。
在这里插入图片描述
回到应用的主页面。这个时候你看日志
在这里插入图片描述
系统直接回调 Service 的 onCommand()方法来启动 Service。这样实际操作一下是不是印象更深刻呢?为了使这个操作更加易懂,我决定安装一个电脑录屏软件,然后再把录得视频转GIF,再贴到文章里,这样看起来就更加的易懂了。刚才说了启动,那么下面说停止。

③ 停止Service Ability

  • 停止 Service

  Service 一旦创建就会一直保持在后台运行,除非必须回收内存资源,否则系统不会停止或销毁 Service。开发者可以在 Service 中通过 terminateAbility()停止本 Service 或在其他 Ability调用 stopAbility()来停止 Service。
  停止 Service 同样支持停止本地设备 Service 和停止远程设备 Service,使用方法与启动Service 一样。一旦调用停止 Service 的方法,系统便会尽快销毁 Service。

有两种停止Service的方法,在Page Ability中停止,和在本Service中停止,先试一下第一种。
下面我们在MainAbilitySlice中增加一个停止服务的方法。

    /**
     * 停止本地服务  在Page Ability中停止Service
     */
    private void stopLocalService() {
        Intent intent = new Intent();
        //构建操作方式
        Operation operation = new Intent.OperationBuilder()
                // 设备id
                .withDeviceId("")
                // 应用的包名
                .withBundleName("com.llw.helloworld")
                // 跳转目标的路径名  通常是包名+类名
                .withAbilityName("com.llw.helloworld.ServiceAbility")
                .build();
        //设置操作
        intent.setOperation(operation);
        //停止服务
        stopAbility(intent);
    }

然后再点击按钮的时候调用。
在这里插入图片描述
然后先运行一下进入到主页面,然后点击Next按钮,看下面的日志。
在这里插入图片描述

可以看到当我们从其他的Page Ability中停止Service时,会先回调onBackground。因为这个时候服务是在前台运行的,系统会把服务放到后台,然后再通过stop来停止这个服务。
下面再看看在本Service中停止这个服务。可以通过一个延时服务来操作,下面来看看代码怎么写的。

    /**
     * 创建一个线程池
     */
    final static ScheduledExecutorService service = Executors.newScheduledThreadPool(4);

    private void stopService() {
        // 延时任务
         service.schedule(threadFactory.newThread(new Runnable() {
            @Override
            public void run() {
                //停止服务当前服务
                terminateAbility();
            }
            //延时三秒执行
        }), 3, TimeUnit.SECONDS);
    }

    /**
     * 线程工厂
     */
    private ThreadFactory threadFactory = new ThreadFactory() {
        @Override
        public Thread newThread(final Runnable r) {
            return new Thread() {
                @Override
                public void run() {
                    r.run();
                }
            };
        }
    };

  为什么要这么写呢?因为DS里面推荐使用ScheduledExecutorService ,不然我就直接用Timer或者Thread就可以了。创建了一个线程池,然后创建一个线程工厂,在进行延时操作的时候,传入了三个参数,一个是线程工厂,里面有一个Runnable(),第二个参数代表数量,第三个参数是单位,上面的代码就是3秒。
 下面直接运行到模拟器,然后等待三秒就会自动调用terminateAbility();停止Service。你会发现和通过其他的Page Ability停止服务的执行流程是一样的。在这里插入图片描述

③ 连接Service Ability

  如果 Service 需要与 Page Ability 或其他应用的 Service Ability 进行交互,则应创建用于连接的 Connection。Service 支持其他 Ability 通过 connectAbility()方法与其进行连接。
  在使用 connectAbility()处理回调时,需要传入目标 Service 的 Intent 与 IAbilityConnection的实例。IAbilityConnection 提供了两个方法供开发者实现:onAbilityConnectDone() 用来处理连接的回调,onAbilityDisconnectDone() 用来处理断开连接的回调。

  在MainAbilitySlice中添加如下代码:

    /**
     * 连接服务
     */
    private void connectService(){
        // 连接 Service
        Intent intent = new Intent();
        //构建操作方式
        Operation operation = new Intent.OperationBuilder()
                // 设备id
                .withDeviceId("")
                // 应用的包名
                .withBundleName("com.llw.helloworld")
                // 跳转目标的路径名  通常是包名+类名
                .withAbilityName("com.llw.helloworld.ServiceAbility")
                .build();
        //设置操作
        intent.setOperation(operation);
        //连接到服务
        connectAbility(intent,connection);

    }

    // 创建连接回调实例
    private IAbilityConnection connection = new IAbilityConnection() {
        // 连接到 Service 的回调
        @Override
        public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int i) {
            // 在这里开发者可以拿到服务端传过来 IRemoteObject 对象,从中解析出服务端传过来的信息
        }

        // 断开与连接的回调
        @Override
        public void onAbilityDisconnectDone(ElementName elementName, int i) {
           
        }
    };

然后在点击的时候调用
在这里插入图片描述
别Service的onConnect方法中加入日志打印
在这里插入图片描述
下面运行一下:
在这里插入图片描述
连接成功。

④ 断开Service Ability

断开服务其实就比较的简单了,调用disconnectAbility()方法即可,而且不用传intent,但是要传IAbilityConnection进入,所以可以可以这样来测试,在连接到Service之后马上断开连接。

//断开服务
disconnectAbility(connection);

在这里插入图片描述
然后运行起来,进入应用页面,然后点击Next。
在这里插入图片描述
OK,到这一步,相信你已经会基本操作了。而Service的生命周期根据调用方法的不同,其生命周期有以下两种路径:

  • 启动 Service 该 Service 在其他 Ability 调用 startAbility()时创建,然后保持运行。其他 Ability 通过调用stopAbility()来停止 Service,Service 停止后,系统会将其销毁。
  • 连接 Service 该 Service 在其他 Ability 调用 connectAbility()时创建,客户端可通过调用disconnectAbility()断开连接。多个客户端可以绑定到相同 Service,而且当所有绑定全部取消后,系统即会销毁该 Service。

看一下官网的图片
在这里插入图片描述

⑤ 前台Service

  刚才我们说的都是后台的Service,那么怎么到前台来呢?最通用的前台服务就是音乐播放了,用手机的时候它会在通知栏创建,然后播放音乐,那么在鸿蒙中需要怎么使用前台服务呢?使用前台 Service 并不复杂,开发者只需在 Service 创建的方法里,调用keepBackgroundRunning()将 Service 与通知绑定。调用 keepBackgroundRunning()方法前需要在配置文件中声明 ohos.permission.KEEP_BACKGROUND_RUNNING 权限,该权限是 normal 级别,同时还需要在配置文件中添加对应的 backgroundModes 参数。在onStop()方法中调用 cancelBackgroundRunning()方法可停止前台 Service。

  说这么多没啥用,下面来实际操作一下:
在connectService方法中注释断开服务
在这里插入图片描述
然后进入到ServiceAbility中,新一个启动前台服务的方法。

    /**
     * 启动前台服务
     */
    private void startupForegroundService(){
        //创建通知请求 设置通知id为9527
        NotificationRequest request = new NotificationRequest(1005);
        //创建普通通知
        NotificationRequest.NotificationNormalContent content =
                new NotificationRequest.NotificationNormalContent();
        //设置通知的标题和内容
        content.setTitle("Title").setText("Text");
        //创建通知内容
        NotificationRequest.NotificationContent notificationContent = new
                NotificationRequest.NotificationContent(content);
        //设置通知
        request.setContent(notificationContent);
        keepBackgroundRunning(1005,request);
        HiLog.error(LABEL_LOG, "ServiceAbility::startupForegroundService");
    }

然后在onStart中调用。
在这里插入图片描述
别忘了在config.json中给相关的代码配置:
在这里插入图片描述

然后直接运行到主页面,之后会先启动Service,然后将Service变成前台服务。运行之后如下:
在这里插入图片描述
说实话目前也就只是日志打印出来了,但是我也不知道当前这个服务是不是在前台。
然后在onCommand中取消前台服务:

    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
        HiLog.error(LABEL_LOG, "ServiceAbility::onCommand");

        cancelBackgroundRunning();
        HiLog.error(LABEL_LOG, "ServiceAbility::cancelBackgroundRunning");

    }

再运行一次。
在这里插入图片描述

四、Data Ability讲解

  使用 Data 模板的 Ability(以下简称“Data”)有助于应用管理其自身和其他应用存储数据的访问,并提供与其他应用共享数据的方法。Data 既可用于同设备不同应用的数据共享,也支持跨设备不同应用的数据共享。

  数据的存放形式多样,可以是数据库,也可以是磁盘上的文件。Data 对外提供对数据的增、删、改、查,以及打开文件等接口,这些接口的具体实现由开发者提供。说起来和Android的ContentProvider有些像。

① URI 介绍

  Data 的提供方和使用方都通过 URI(Uniform Resource Identifier)来标识一个具体的数据,例如数据库中的某个表或磁盘上的某个文件。HarmonyOS 的 URI 仍基于 URI 通用标准,格式如下:

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

URI 示例:

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

② 访问 Data和声明使用权限

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

如果待访问的 Data 声明了访问需要权限,则访问此 Data 需要在配置文件中声明需要此权限。比如
在这里插入图片描述
reqPermissions 表示应用运行时向系统申请的权限。
说了这么多还是来创建一个Data Ability吧,鼠标右键包名 → New → Ability → Empty Data Ability
在这里插入图片描述
这个的Visible和Service的Visible是同样的意思,勾选上就是运行其他应用程序访问数据。
在这里插入图片描述

然后打开config.json,看创建DataAbility时,自动生成了那些代码。
在这里插入图片描述
可以看到type为“data”,另外还自带一个提供给外部数据的权限,已经访问这个DataAbility的uri。

然后看一下DataAbility的代码:

package com.llw.helloworld;

import ohos.aafwk.ability.Ability;
import ohos.aafwk.content.Intent;
import ohos.data.resultset.ResultSet;
import ohos.data.rdb.ValuesBucket;
import ohos.data.dataability.DataAbilityPredicates;
import ohos.hiviewdfx.HiLog;
import ohos.hiviewdfx.HiLogLabel;
import ohos.utils.net.Uri;
import ohos.utils.PacMap;

import java.io.FileDescriptor;

public class DataAbility extends Ability {
    private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "Demo");

    @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        HiLog.info(LABEL_LOG, "ProviderAbility onStart");
    }

    @Override
    public ResultSet query(Uri uri, String[] columns, DataAbilityPredicates predicates) {
        return null;
    }

    @Override
    public int insert(Uri uri, ValuesBucket value) {
        HiLog.info(LABEL_LOG, "ProviderAbility insert");
        return 999;
    }

    @Override
    public int delete(Uri uri, DataAbilityPredicates predicates) {
        return 0;
    }

    @Override
    public int update(Uri uri, ValuesBucket value, DataAbilityPredicates predicates) {
        return 0;
    }

    @Override
    public FileDescriptor openFile(Uri uri, String mode) {
        return null;
    }

    @Override
    public String[] getFileTypes(Uri uri, String mimeTypeFilter) {
        return new String[0];
    }

    @Override
    public PacMap call(String method, String arg, PacMap extras) {
        return null;
    }

    @Override
    public String getType(Uri uri) {
        return null;
    }
}

在创建的时候就生成了一些代码,基本的增删改查、打开文件、获取URI类型、获取文件类型、还有一个回调。再加上一个onStart方法,总共是9个,乍一看比较多。下面先来介绍 DataAbilityHelper 具体的使用步骤。
创建 DataAbilityHelper

  DataAbilityHelper 为开发者提供了 creator()方法来创建 DataAbilityHelper 实例。该方法为静态方法,有多个重载。最常见的方法是通过传入一个 context 对象来创建DataAbilityHelper 对象。
在这里插入图片描述
DataAbilityHelper 为开发者提供了一系列的接口来访问不同类型的数据(文件、数据库等)。

  • 访问文件

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

        // 读取文件描述符
        try {
            //通过文件描述符 读取指定uri的文件 ,“r”(读), “w”(写), “rw”(读写),“wt”(覆盖写),“wa”(追加写),“rwt”(覆盖写且可读)
            FileDescriptor fileDescriptor = helper.openFile(Uri.parse("dataability://com.llw.helloworld.DataAbility"),"r");
            //获取文件输入流
            FileInputStream fileInputStream = new FileInputStream(fileDescriptor);
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
  • 访问数据库

DataAbilityHelper 为开发者提供了增、删、改、查以及批量处理等方法来操作数据库。
下面代码来说明一下:

* **query** 查询方法,其中 uri 为目标资源路径,columns 为想要查询的字段。开发者的查询条件可以通过 DataAbilityPredicates 来构建。查询用户表中 id 在 1-10 之间的用户的年龄,并把结果打印出来,代码示例如下:
    /**
     * 查询
     */
    private void queryData(DataAbilityHelper helper) {
        //构建uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        //构建查询字段
        String[] column = {"age"};
        // 构造查询条件
        DataAbilityPredicates predicates = new DataAbilityPredicates();
        //查询用户id在1~10之间的数据
        predicates.between("userId",1,10);

        //进行查询
        try {
            //用一个结果集来接收查询返回的数据
            ResultSet resultSet = helper.query(uri,column,predicates);
            //从第一行开始
            resultSet.goToFirstRow();
            //处理每一行的数据
            do {
                // 在此处理 ResultSet 中的记录
                HiLog.info(LABEL_LOG, resultSet.toString());
            }while (resultSet.goToNextRow());

        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
    }
  • insert 插入方法,其中 uri 为目标资源路径,ValuesBucket 为要新增的对象。插入一条用户信息的代码示例如下:
    /**
     * 插入 单条数据
     */
    private void insertData(DataAbilityHelper helper) {
        //构建uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        // 构造插入数据
        ValuesBucket valuesBucket = new ValuesBucket();
        valuesBucket.putString("name","KaCo");
        valuesBucket.putInteger("age",24);
        try {
            helper.insert(uri,valuesBucket);
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
    }
  • batchInsert 批量插入方法,和 insert()类似。批量插入用户信息的代码示例如下:
    /**
     * 插入 多条数据
     * @param helper 数据帮助类
     */
    private void batchInsertData(DataAbilityHelper helper) {
        //构建uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        // 构造插入数据
        ValuesBucket[] valuesBuckets = new ValuesBucket[3];
        //构建第一条数据
        valuesBuckets[0] = new ValuesBucket();
        valuesBuckets[0].putString("name","Jim");
        valuesBuckets[0].putInteger("age",18);
        //构建第二条数据
        valuesBuckets[1] = new ValuesBucket();
        valuesBuckets[1].putString("name","Tom");
        valuesBuckets[1].putInteger("age",20);
        //构建第三条数据
        valuesBuckets[2] = new ValuesBucket();
        valuesBuckets[2].putString("name","Kerry");
        valuesBuckets[2].putInteger("age",24);
        try {
            //批量插入数据
            helper.batchInsert(uri,valuesBuckets);
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
    }
  • delete 删除方法,其中删除条件可以通过 DataAbilityPredicates 来构建。删除用户表中 id 在 1-10 之间的用户,代码示例如下:
    /**
     * 删除数据
     * @param helper 数据帮助类
     */
    private void deleteData(DataAbilityHelper helper) {
        //构建uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        // 构造删除条件
        DataAbilityPredicates predicates = new DataAbilityPredicates();
        //用户id在1~10的数据
        predicates.between("userId",1,10);
        try {
            //删除
            helper.delete(uri,predicates);
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
    }
  • update 更新方法,更新数据由 ValuesBucket 传入,更新条件由 DataAbilityPredicates 来构建。更新 id 为 2 的用户,代码示例如下:
    /**
     * 更新数据
     * @param helper 数据帮助类
     */
    private void updateData(DataAbilityHelper helper) {
        //构造uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        //构造更新数据
        ValuesBucket valuesBucket = new ValuesBucket();
        valuesBucket.putString("name","Aoe");
        valuesBucket.putInteger("age",66);
        //构造更新条件
        DataAbilityPredicates predicates = new DataAbilityPredicates();
        //userId为2的用户
        predicates.equalTo("userId",2);
        try {
            //更新数据
            helper.update(uri,valuesBucket,predicates);
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        }
    }
  • executeBatch 此方法用来执行批量操作。DataAbilityOperation 中提供了设置操作类型、数据和操作条件的方法,开发者可自行设置自己要执行的数据库操作。插入多条数据的代码示例如下:
    /**
     * 批量操作数据
     * @param helper 数据帮助类
     */
    private void executeBatchData(DataAbilityHelper helper) {
        //构造uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.DataAbility");
        //构造批量操作
        //第一个
        ValuesBucket valuesBucket1 = new ValuesBucket();
        valuesBucket1.putString("name","Karen");
        valuesBucket1.putInteger("age",24);
        //构建批量插入
        DataAbilityOperation operation1 = DataAbilityOperation.newInsertBuilder(uri).withValuesBucket(valuesBucket1).build();
        //第二个
        ValuesBucket valuesBucket2 = new ValuesBucket();
        valuesBucket2.putString("name","Leo");
        valuesBucket2.putInteger("age",48);
        DataAbilityOperation operation2 = DataAbilityOperation.newInsertBuilder(uri).withValuesBucket(valuesBucket2).build();
        
        ArrayList<DataAbilityOperation> operations = new ArrayList<>();
        operations.add(operation1);
        operations.add(operation2);
        try {
            //获取批量操作数据的结果
            DataAbilityResult[] results = helper.executeBatch(uri,operations);
            HiLog.debug(LABEL_LOG,results.length+"");
        } catch (DataAbilityRemoteException e) {
            e.printStackTrace();
        } catch (OperationExecuteException e) {
            e.printStackTrace();
        }
    }

③ 创建Data

确定数据存储方式
确定数据的存储方式,Data 支持以下两种数据形式:

  • 文件数据:如文本、图片、音乐等。
  • 结构化数据:如数据库等。

下面创建一个UserDataAbility,注意勾选上Visible
在这里插入图片描述
实现 UserDataAbility
  UserDataAbility 接收其他应用发送的请求,提供外部程序访问的入口,从而实现应用间的数据访问。Data 提供了文件存储和数据库存储两组接口供用户使用。
文件存储
  开发者需要在 Data 中重写 FileDescriptor openFile(Uri uri, String mode)方法来操作文件:uri 为客户端传入的请求目标路径;mode 为开发者对文件的操作选项,可选方式包含“r”(读), “w”(写), “rw”(读写)等。
  MessageParcel 类提供了一个静态方法,用于获取 MessageParcel 实例。通过dupFileDescriptor()函数复制待操作文件流的文件描述符,并将其返回,供远端应用使用。示例,根据传入uri打开对应的文件,在UserDataAbility中写入如下方法

    /**
     * uri 打开对应的文件
     */
    private void openUriFile() {
        //构建uri
        Uri uri = Uri.parse("dataability://com.llw.helloworld.UserDataAbility");
        //获取文件  通过uri获取解码路径列表的第2条数据
        File file = new File(uri.getDecodedPathList().get(1));
        //只读
        file.setReadOnly();
        try {
            //文件输入流
            FileInputStream fileInputStream = new FileInputStream(file);
            //得到文件描述符
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            //绑定文件描述符
            MessageParcel.dupFileDescriptor(fileDescriptor);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

  • 初始化的时候连接数据库。

首先要创建一个数据实体bean

package com.llw.helloworld;

import ohos.data.orm.OrmObject;

public class BookStore extends OrmObject {
    private int id;
    private String bookName;
    private double price;
    private int page;
    private String author;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }

    public int getPage() {
        return page;
    }

    public void setPage(int page) {
        this.page = page;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }
}

然后在UserDataAbility中如下:
在这里插入图片描述

上面的代码是官方文档里面的,可以看到这里是有一个地方报错的,因为少了一个参数,然后看一下getOrmContext方法少什么参数。
在这里插入图片描述
然后来看一下OrmMigration的源码
在这里插入图片描述
这是一个抽象类,可以通过继承的方式去实现它里面的方法。
下面我创建一个TestOrmContext1继承OrmMigration,里面的代码如下:

package com.llw.helloworld;

import ohos.data.orm.OrmMigration;
import ohos.data.rdb.RdbStore;

public class TestOrmContext1 extends OrmMigration {
    /**
     * 此处用于配置数据库版本迁移的开始版本和结束版本,
     * super(startVersion, endVersion)即数据库版本号从 1 升到 2。
     */
    public TestOrmContext1() {
        super(1, 2);
    }

    /**
     * 迁移时
     *
     * @param rdbStore
     */
    @Override
    public void onMigrate(RdbStore rdbStore) {
        rdbStore.executeSql("ALTER TABLE `BookStore` ADD COLUMN `addColumn1` INTEGER");
    }
}

其实这个方法的意思就是在连接数据库的时候查询数据库的版本,决定是否要升级。
因为加了也报错,那么我为什么不加上去呢?你以为加上去就不报错了吗?我是不得其解,也许是我才疏学浅吧。
在这里插入图片描述

  • 编写数据库操作方法

Ability 定义了 6 个方法供用户处理对数据库表数据的增删改查。
先创建一个用户的实体

package com.llw.helloworld;

public class User extends OrmObject {
    private int id;
    private String name;
    private int age;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}
  • query 该方法接收三个参数,分别是查询的目标路径,查询的列名,以及查询条件,查询条件由类DataAbilityPredicates 构建。根据传入的列名和查询条件查询用户表的代码示例如下:

在这里插入图片描述
可以看到创建Data Ability的时候就会自动生成这个方法,下面的代码就在这个方法里面写:

    /**
     * 查询数据库
     *
     * @param uri        目标uri
     * @param columns    查询的字段
     * @param predicates 查询的条件
     * @return 结果集
     */
    @Override
    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;
    }
  • insert 该方法接收两个参数,分别是插入的目标路径和插入的数据值。其中,插入的数据由ValuesBucket 封装,服务端可以从该参数中解析出对应的属性,然后插入到数据库中。此方法返回一个 int 类型的值用于标识结果。接收到传过来的用户信息并把它保存到数据库中的代码示例如下:
    /**
     * 插入单条数据
     * @param uri 目标uri
     * @param value 插入的数据
     * @return 插入后的id
     */
    @Override
    public int insert(Uri uri, ValuesBucket value) {
        if (ormContext == null) {
            HiLog.info(LABEL_LOG, "failed to insert, ormContext is null");
            return -1;
        }
        //获取uri解码路径
        String path = uri.getDecodedPath();
        PathMatcher pathMatcher = new PathMatcher();
        if (pathMatcher.getPathId(path) == PathMatcher.NO_MATCH) {
            HiLog.info(LABEL_LOG, "UserDataAbility insert path is not matched");
            return -1;
        }
        // 构造插入数据
        User user = new User();
        user.setId(value.getInteger("id"));
        user.setName(value.getString("name"));
        user.setAge(value.getInteger("age"));
        //插入数据库
        boolean isSuccessed = true;
        isSuccessed = ormContext.insert(user);

        if (!isSuccessed) {
            HiLog.error(LABEL_LOG, "failed to insert");
            return -1;
        }
        isSuccessed = ormContext.flush();
        if (!isSuccessed) {
            HiLog.error(LABEL_LOG, "failed to insert flush");
            return -1;
        }
        DataAbilityHelper.creator(this, uri).notifyChange(uri);
        int id = Math.toIntExact(user.getRowId());
        return id;
    }
  • batchInsert 该方法为批量插入方法,接收一个 ValuesBucket 数组用于单次插入一组对象。它的作用是提高插入多条重复数据的效率。该方法系统已实现,开发者可以直接调用。
  • delete 该方法用来执行删除操作。删除条件由类 DataAbilityPredicates 构建,服务端在接收到该参数之后可以从中解析出要删除的数据,然后到数据库中执行。根据传入的条件删除用户表数据的代码示例如下:
    /**
     * 删除
     * @param uri 目标uri
     * @param predicates 删除条件
     * @return 删除的结果
     */
    @Override
    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;
    }
  • update 此方法用来执行更新操作。用户可以在 ValuesBucket 参数中指定要更新的数据,在DataAbilityPredicates 中构建更新的条件等。更新用户表的数据的代码示例如下:
    /**
     * 更新数据
     * @param uri 目标uri
     * @param value 更新的数据
     * @param predicates 更新条件
     * @return 更新的结果
     */
    @Override
    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 中提供了设置操作类型、数据和操作条件的方法,用户可自行设置自己要执行的数据库操作。该方法系统已实现,开发者可以直接调用。

五、结语

  说实话写这一篇文章花费了一番功夫,不断的浏览官网上的文档然后结合实际来写,写的不是很好,请勿见怪,另外就是觉得官网的教程只是一部分,更多的需要开发者自行去探索和发现,正所谓师傅领进门,修行在个人,鸿蒙需要成长,我们开发者同样也要成长,也许不会前进的路上会很坎坷,但经历过后就会发现另一番风景,我是初学者,保持初学的态度和动力,感谢您的阅读,山高水长,后会有期!

推荐阅读
关注数
2957
内容数
446
华为鸿蒙相关技术,活动及资讯,欢迎关注及加入创作
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息