再谈鸿蒙Service

948 阅读12分钟

在这里插入图片描述

鸿蒙Service相比Android的Service来讲,重要性和使用频率要高很多,因为其分布式的特点,Service被重新定义,做了很大的扩展,不仅仅只做一些后台任务,还可以进行远程控制、数据通信、资源分配等。鸿蒙应用开发中的Service是Ability的一种,并非和Android那样有明显的区分,使用方法也和Ability类似,也分本地和远程,单向和双向。
之前浅谈过鸿蒙Service,主要是对Service做了简单概述,对其生命周期进行了简单分析。本文主要对Service的具体使用做简单说明。

创建Service

创建Service比较简单,基本不用写代码,一路点下去就行,如下图所示: 在这里插入图片描述 由于Service区分为本地和远程的,所以这里创建两个Service,以备使用,分别为:LocalServiceAbility和RemoteServiceAbility:

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

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

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

    @Override
    public void onStop() {
        super.onStop();
        HiLog.info(LABEL_LOG, "LocalServiceAbility::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) {
    }
}

当点击创建Serivice的Finish按钮后,上述代码就会自动生成,开发者可根据自己的习惯配置一下日志输出格式。RemoteServiceAbility和LocalServiceAbility除了类名不一样外,其他都一样。由于Service是Ability的一种,所以Service都是继承Ability的,使用是比较方便,但是Service不同于Page,放在一起使用容易混乱,所以使用Service的时候建议封装个基类比较好容易区分,便于管理。当Service创建完后,DevEco Studio 自动在config.json中注册,无需手动注册。

本地服务

启动与关闭
启动服务
    Intent intent1 = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId("")
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.LocalServiceAbility")
                    .build();
            intent1.setOperation(operation);
            startAbility(intent1);
停止服务
     Intent intent1 = new Intent();
            Operation operation = new Intent.OperationBuilder()
                    .withDeviceId("")
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.LocalServiceAbility")
                    .build();
            intent1.setOperation(operation);
            stopAbility(intent1);
下一页
    Intent intent1 = new Intent();
    present(new SecondAbilitySlice(), intent1);

简单设置了三个按钮:开启服务、停止服务、下一页。仔细观察一下上述代码,可以发现服务的开启与停止和Ability的使用是一样的,如果有什么不清楚的可以看看HarmonyOS-page之间的跳转

日志输出

使用的LocalServiceAbility的代码和文章开头创建的一样,在生命周期中的每个方法进行日志输出,在onCommand()中将所有的参数进行了输出与弹出:

    @Override
    public void onCommand(Intent intent, boolean restart, int startId) {
        System.out.println("LocalServiceAbility::onCommand restart:" + restart + ",startId:" + startId);
        ToastDialog toastDialog = new ToastDialog(getContext());
        toastDialog.setText("::onCommand restart:" + restart + ",startId:" + startId);
        toastDialog.show();
    }
效果

在这里插入图片描述 不管是在手机上还是电视上,效果都一样,光看页面看不出什么,主要是分析日志。

  • 第一次开启服务时,LocalServiceAbility会依次执行onStart()->onCommand(),同时onCommand()中restart值为false,startId值为1。
  • 在不停止服务的时候再次开启服务会发现直接执行了onCommand(),且onCommand()中restart值为false,startId值为2。
  • 同样不关闭该服务,跳转至下一页然后再回来再次开启服务,会发现直接执行了onCommand(),且onCommand()中restart值为false,startId值为3。
  • 当切回到home页,再切会到该页面,点击开启服务,会发现直接执行了onCommand(),且onCommand()中restart值为false,startId值为4 。
  • 当点击停止服务时,此地点击开启服务此时Service会从头开始执行onStart()->onCommand(),且onCommand()中restart值为false,startId值为1。点击停止服务后跳转至下一页后返回开启服务,执行和点击停止服务后再开启服务后一样。

不管执行到哪一步,直接输出Service的实例,发现Service实例对应的地址是一样的。 上述方法的执行顺序,侧面应证了Service的一些特点:

  • 相同的Service是单例的,不会被多次创建。
  • Service自己的生命周期并不和Ability的生命周期绑定在一起,即不会随着页面的销毁而销毁。Service一旦被创建,不会自动停止,除非调用stopAbility或在Service内部相关操作执行完后调用terminateAbility后该服务会停止。
  • onStart只会在Service第一次开启的时候被调用,若该服务没有被停止,再次启用该服务,该方法不会被调用,即onStart在Service整个生命周期中只会执行一次。
  • onCommand在Service生命周期中允许被多次调用。其参数restart并非是指再次启用服务时,restart为true,查看日志会发现不管调用多少次结果均为false,所以restart代表的是服务异常关闭时再次启用会被置为true,比如崩溃。startId其实相当于计数器,在一次完整的生命周期中,每次调用该服务,startId均会+1,查看这个可以看看该服务被掉用了多少次。当服务被停止后startId会从头开始。
  • 当停止服务时,不会最先调用onStop(),而是先onBackground(),后onStop()。
连接与断开

上文中一个简单的服务从开启到停止一个完整的流程已经走完,但是onConnect()和onDisconnect()没有被召唤,这两个方法肯定不会是多余的,onConnect()和onDisconnect()是Service使用的另一种方式。之所以配置两种使用方式,是适用于不同的场景。 开启和停止服务属于最基本操作,该使用方式属于开关式,只管开始和结束,无法控制过程,即如果使用该方式开启了音乐播放,只能开启播放音乐,但是是否开启成功,播放哪一首,到什么时间了,是否被人为关掉了,该方式均无法把控。所以开启和停止服务属于单项信号式,而连接与断开是双向通信的方式,不仅可以开关服务,还可以获取服务状态。

创建Service实例
    private IAbilityConnection connection = new IAbilityConnection() {
        @Override
        public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int resultCode) {
        }

        @Override
        public void onAbilityDisconnectDone(ElementName elementName, int resultCode) {
          
        }
    };

在这里插入图片描述 onAbilityConnectDone()是用来处理连接Service成功的回调。elementName为连接设备的相关信息,启动Ability中有使用intent.setElementName(String deviceId, String bundleName, String abilityName)来启用,不清楚的可以看看HarmonyOS-page之间的跳转末尾的评论。IRemoteObject 相当于Service连接通道中的数据包,resultCode是返回结果码,一般0代表连接成功,否则连接失败。 在这里插入图片描述 onAbilityDisconnectDone()参数和onAbilityConnectDone方法中一样。注意上图中的crashes,unexpectedly,说明onAbilityDisconnectDone是用来处理Service异常死亡的回调,所以在正常的断开连接时是看不到此方法的调用。

连接
Intent intent = new Intent();
Operation operation = new Intent.OperationBuilder()
        .withDeviceId("deviceId")
        .withBundleName("com.harmonyos.service")
        .withAbilityName("com.harmonyos.service.LocalServiceAbility")
        .build();
intent.setOperation(operation);
connectAbility(intent, connection);

连接时需要调用connectAbility方法,而且需要将创建好的Service实例connection带进入即可。

断开
disconnectAbility(connection)

断开调用disconnectAbility,把Service实例connection传进入即可。

创建IRemoteObject实现类

创建返回数据就是Service侧也需要在onConnect()时返回IRemoteObject,从而定义与Service进行通信的接口。系统默认提供IRemoteObject的实现LocalRemoteObject ,也可以直接继承RemoteObject

//    private class CurrentRemoteObject extends ohos.aafwk.ability.LocalRemoteObject {
//
//        public CurrentRemoteObject() {
//        }
//    }

    private class CurrentRemoteObject extends RemoteObject {
        private CurrentRemoteObject() {
            super("CurrentRemoteObject");
        }

        @Override
        public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) {
            return true;
        }
    }
    

LocalRemoteObject 也是继承了RemoteObject ,所以两种方式其实是一样的。创建好了之后在LocalServiceAbility中将其返回

   @Override
    public IRemoteObject onConnect(Intent intent) {
        System.out.println("LocalServiceAbility::onConnect");
        return new CurrentRemoteObject();
    }
效果与分析

页面还是上文中的页面,但是日志变化不小。

  1. 连接服务时会调用onStart()-->ononConnect(),若有返回的RemoteObject 实现类,之后会调用onAbilityConnectDone(),此时宣告Service连接成功,并可以接收连接成功后的数据返回IRemoteObject,通过resultCode可以知道是哪个Service返回的,方便处理。在没有断开服务的情况下,多次连接上述方法并不会执行,因为一旦连接成功,便维持,所以多次连接已连接的服务无意义。
  2. 断开服务时onDisconnect()-->onBackground()-->onStop(),多次断开已断开的Service也是没有意义的。
  3. onAbilityDisconnectDone()只有在Service异常死亡的时候才会被调用。
  4. CurrentRemoteObject()是将Service自身的实例返回给调用者。
  5. 若有多个Ability连接该服务,第一个Ability连接服务时,ononConnect()会被调用,生成IRemoteObject 对象,并将此对象返回到所有连接到该服务的Ability。此时Service像是一个真实的服务器,将数据发给所有需要的用户,所以在文章开头讲鸿蒙Service有数据通信、资源分配功能。特别是远程连接服务的时候,用户完全可以将任意一台设备作为Service,其他设备作为Client,典型的C-S模式的任意切换。

远程服务

远程服务的开启与停止,连接与断开与本地服务的操作基本一致,但是deviceId要手动获取,并添加flag:

   ...
   Operation operation = new Intent.OperationBuilder()
                    .withDeviceId(deviceId)
                    .withBundleName("com.harmonyos.service")
                    .withAbilityName("com.harmonyos.service.RemoteServiceAbility")
                    .withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)
                    .build();
   ...

deviceId怎么获取,可使用如下方法获取:

    private String getRemoteDeviceId() {
        List<DeviceInfo> infoList = DeviceManager.getDeviceList(DeviceInfo.FLAG_GET_ALL_DEVICE);
        if ((infoList == null) || (infoList.size() == 0)) {
            return "";
        }
        int random = new SecureRandom().nextInt(infoList.size());
        return infoList.get(random).getDeviceId();
    }

withFlags(Intent.FLAG_ABILITYSLICE_MULTI_DEVICE)就是添加个标签,远程服务就是跨设备、多设备,所以添加个FLAG_ABILITYSLICE_MULTI_DEVICE理所应当的,也可以在intent.setFlags中添加,但是该方法已被废弃,过时废弃的方法能不用就不用。 远程服务和本地服务不同之处在于远程服务中你不知道谁是服务端,谁是客户端,所以需要在代码中两端的操作都要做好。

客户端工作

这里简单模拟的是客户端传递一个int类型的数据给远程服务端,然后服务端将数据*1024再返回。首先创建客户端代理类并实现IRemoteBroker,IRemoteBroker也就是远程代理,就像两个经纪人之间的交流,并非本主。

public class ClientRemoteProxy implements IRemoteBroker {
    private static final int RESULT_SUCCESS = 0;
    private static final int RESULT_TODO = 1;
    private final IRemoteObject remoteObject;
    
    public ClientRemoteProxy(IRemoteObject remoteObject) {
        this.remoteObject = remoteObject;
    }

    public int todoServiceJob(int command) {
        MessageParcel message = MessageParcel.obtain();
        message.writeInt(command);

        MessageParcel reply = MessageParcel.obtain();
        MessageOption option = new MessageOption(MessageOption.TF_SYNC);
        int result = 0;
        try {
            remoteObject.sendRequest(RESULT_TODO, message, reply, option);
            int resultCode = reply.readInt();
            if (resultCode != RESULT_SUCCESS) {
                throw new RemoteException();
            }
            result = reply.readInt();
        } catch (RemoteException e) {
            e.printStackTrace();
        }
        return result;
    }
    @Override
    public IRemoteObject asObject() {
        return remoteObject;
    }
}

最重要的是todoServiceJob方法,这里是消息发送最关键的地方,remoteObject.sendRequest()就是向远程发布消息,告诉远程设备我要干嘛。

  boolean sendRequest(int var1, MessageParcel var2, MessageParcel var3, MessageOption var4) throws RemoteException;

第一个参数相当于请求码,两端约定好,客户端发送这个码,远程设备接口后识别这个码就知道要干嘛了。MessageParcel 使用起来像队列,实际功能却和Map很像,通过MessageParcel.obtain()获取,然后writeInt和readInt写入和写出数据,数据类型包含基本类型和自己创建的对象。 在这里插入图片描述 MessageParcel读取或写入数据时,写一次指针往后移动一位,写一个数据就占一格,再写就往后再占一格,依次按顺序往下,使用的时候按顺序来就行。第二个参数MessageParcel var2就是客户端向远程设备传递的设备,第三个参数MessageParcel var3是远程设备向客户端回复的消息,发送的时候就已经把需要接收的消息的位置给留好了,而不是远程设备接收数据后将数据清空然后再将结果写入。两个篮子,一个装请求,一个装结果,互不干扰。第四个参数MessageOption var4是本次通信是同步的还是异步的,同步的如下:

     MessageOption option = new MessageOption(MessageOption.TF_SYNC);

异步的就是将MessageOption.TF_SYNC换成MessageOption.TF_ASYNC。返回的数据中一般第一位为状态码,resultCode = reply.readInt(),如果resultCode等于成功的状态码就取第二位,第二位才是真正的结果,然后将其传给需要的地方。数据存放的格式不固定,两端约定好就行,不一定要把状态码放在第一位,可以不传,可以放在前三位,均可。 ClientRemoteProxy 的使用如下:

public class MainAbilitySlice extends AbilitySlice {
    private ClientRemoteProxy clientRemoteProxy = null;
    private IAbilityConnection connection = new IAbilityConnection() {
        @Override
        public void onAbilityConnectDone(ElementName elementName, IRemoteObject iRemoteObject, int i) {
            clientRemoteProxy = new ClientRemoteProxy(iRemoteObject);
            System.out.println("onAbilityConnectDone");

        }
        @Override
        public void onAbilityDisconnectDone(ElementName elementName, int i) {
            System.out.println("onAbilityDisconnectDone");

        }
    };
    
        @Override
    public void onStart(Intent intent) {
        super.onStart(intent);
        super.setUIContent(ResourceTable.Layout_ability_main);
        findComponentById(ResourceTable.Id_btn_next_page).setClickedListener(component -> {
            if(clientRemoteProxy != null){
                int result = clientRemoteProxy.todoServiceJob(512);
                System.out.println("result:" + result);
            }
        });
	...
    }

当服务连接成功后调用onAbilityConnectDone方法并返回IRemoteObject ,此时ClientRemoteProxy拿到IRemoteObject创建实例,然后在相应的位置发送消息clientRemoteProxy.todoServiceJob(512),就是将512发送给远程设备,等待远程设备返回结果。

远程设备工作

远程设备就是将传递过来的数据处理后再传回去。

public class ServiceRemoteProxy extends RemoteObject implements IRemoteBroker {
    private static final int RESULT_SUCCESS = 0;
    private static final int RESULT_FAILED = -1;
    private static final int RESULT_TODO = 1;

    public ServiceRemoteProxy(String descriptor) {
        super(descriptor);
    }

    @Override
    public IRemoteObject asObject() {
        return this;
    }
    @Override
    public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option) throws RemoteException {
        if(code != RESULT_TODO){
            reply.writeInt(RESULT_FAILED);
            return  false;
        }

        int initData = data.readInt();
        int resultData = initData * 1024;
        reply.writeInt(RESULT_SUCCESS);
        reply.writeInt(resultData);
        
        return true;
    }
}

最重要的是onRemoteRequest,里面的参数解释和ClientRemoteProxy 中的sendRequest一样,若code不等于约定值,直接返回错误码拒绝服务。代码中将传进来的数据*1024后传回去,至此远程设备的服务工作已结束但是远程设备还没有被使用,使用如下:

public class RemoteServiceAbility extends Ability {
 	...
    private ServiceRemoteProxy serviceRemoteProxy = new ServiceRemoteProxy("");
    ...
    @Override
    public IRemoteObject onConnect(Intent intent) {
        return serviceRemoteProxy;
    }
   ...
}

在连接远程服务调用onConnect,直接将远程服务的代理类ServiceRemoteProxy的实例返回即可。至此所有的远程服务工作结束。

远程服务流程
  1. 创建发送端的代理类实现IRemoteBroker,用于发送数据。
  2. 创建远程端的代理类继承RemoteObject并实现IRemoteBroker,用于接收、处理、返回数据。
  3. 创建连接远程服务需要的的IAbilityConnection,并在onAbilityConnectDone中获取返回IRemoteObject创建发送端代理类的实例,并在适合的位置用创建的实例发送消息。
  4. 连接起远程服务后会调用onConnect,在此方法中返回远程代理类的实例。

顺序不一定是上面的顺序,只要该有的都有了,调用远程服务就没有问题。

总结

  • 相同的Service是单例的。
  • Service不主动销毁,使用完毕后建议及时断开或销毁。
  • 多个Ability可共用一个Service,其中一个Ability调用Service,Service会将结果发送给所有Ability。
  • 当多个Ability共用一个Service时,所有Ability退出后,Service才能退出。
  • Service执行在主线程中,若有耗时操作,请另开线程,否则ANR。
  • 鸿蒙Service分布式可助任意连接设备变成Service设备。