鸿蒙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();
}
效果与分析
页面还是上文中的页面,但是日志变化不小。
- 连接服务时会调用onStart()–>ononConnect(),若有返回的RemoteObject 实现类,之后会调用onAbilityConnectDone(),此时宣告Service连接成功,并可以接收连接成功后的数据返回IRemoteObject,通过resultCode可以知道是哪个Service返回的,方便处理。在没有断开服务的情况下,多次连接上述方法并不会执行,因为一旦连接成功,便维持,所以多次连接已连接的服务无意义。
- 断开服务时onDisconnect()–>onBackground()–>onStop(),多次断开已断开的Service也是没有意义的。
- onAbilityDisconnectDone()只有在Service异常死亡的时候才会被调用。
- CurrentRemoteObject()是将Service自身的实例返回给调用者。
- 若有多个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的实例返回即可。至此所有的远程服务工作结束。
远程服务流程
- 创建发送端的代理类实现IRemoteBroker,用于发送数据。
- 创建远程端的代理类继承RemoteObject并实现IRemoteBroker,用于接收、处理、返回数据。
- 创建连接远程服务需要的的IAbilityConnection,并在onAbilityConnectDone中获取返回IRemoteObject创建发送端代理类的实例,并在适合的位置用创建的实例发送消息。
- 连接起远程服务后会调用onConnect,在此方法中返回远程代理类的实例。
顺序不一定是上面的顺序,只要该有的都有了,调用远程服务就没有问题。
总结
- 相同的Service是单例的。
- Service不主动销毁,使用完毕后建议及时断开或销毁。
- 多个Ability可共用一个Service,其中一个Ability调用Service,Service会将结果发送给所有Ability。
- 当多个Ability共用一个Service时,所有Ability退出后,Service才能退出。
- Service执行在主线程中,若有耗时操作,请另开线程,否则ANR。
- 鸿蒙Service分布式可助任意连接设备变成Service设备。