当前studio版本信息

AIDL是Android提供的一种跨进程通信方式,就是C/S方式,一个客户端一个服务端
服务端实现
新建AIDL文件

这里提示需要在gradle文件中进行配置,我们按照提示进行配置

配置完我们再次新建AIDL文件

点击圈中部分

这里名字可以自定义,不过建议风格保持一致,即以I开头(I表示Interface接口)AidlInterface结尾,方便后续区分和维护,然后点击右下角的finish按钮
自动生成的文件如下

这里自动生成了basicTypes方法,我们用不上可以直接删除,写上我们自己业务需要定义的方法

这里的第二行
package com.vhd.mqttservice;很重要,服务端和客户端必须保证一模一样才能正常使用
定义回调接口
我们想有一些状态回调方法的话,同样需要新建AIDL文件
在上一步中生成的IDevManageAidlInterface.aidl文件的目录上右键,按照图示方式选择新建aidl文件

依然是重命名好文件,然后finish

删掉自动生成的basicTypes方法,自定义回调方法

make生成java文件
这时候我们的aidl文件并不能正常使用,我们需要重新编译项目生成对应java文件


以上两种方式任意选择,编译完成后我们在编译目录下可以找到对应的java文件

这两个文件绝对不要手动修改,如果你想修改定义,就修改你的aidl文件并重新编译

我们将AIDL文件中添加注册与解注册的回调,这里要注意
import com.vhd.mqttservice.IDevManageStatusCallbak;是需要我们手动导入的,完成修改后记得再次编译才能生效
service实现
AIDL定义的接口或方法都需要在Service里实现具体定义,我们在项目适当位置(推荐在项目根目录)新建java类并继承至Service


sdudio会提示我们实现必须的方法,我们点击圈中部分

这里自动帮我们返回了一个null值,但是这不是我们想要的结果。我们需要将AIDL通过binder暴露出去
我们需要自定义binder

可以看到已经可以识别到AIDL生成的java接口类(因为编译时生成了java类)了

这里写法就是固定的了,声明Stub对象。我们在
new IDevManageAidlInterface.Stub()时,studio会自动带出需要我们实现的方法(就是我们在AIDL中声明的方法),这里每个方法的实现就得依据我们的具体业务需求了。比如这里的isConnectMqtt()方法返回值,就可以根据我们服务端实际情况返回,已连接就返回true,未连接就返回false。这样客户端在需要知道服务端的连接情况时,就可以通过isConnectMqtt()获取到实际状态。
别忘了前面说的onBind返回的null不是我们想要的,那么我们将返回值修改为我们刚定义的mBinder

注册service
在AndroidManifest.xml中注册service

android:exported="true"表示支持跨进程访问
android:process=":remote"表示是独立进程,这里的":"表示以主进程(进程名为包名)添加新名称作为新进程的名称,如 com.vhd.mqttservice 将会变成 com.vhd.mqttservice:remote,当然这里的remote也可以是任意其它字符串,只不过习惯命名成remote。(二编:这里多进程会有一些注意的点,后面会提到)
这里添加了
<intent-filter>
<action android:name="com.hd.mqtt.status"/>
</intent-filter>
是为了方便客户端可以隐式启动我们的AidlImpService,当然我们也可以不添加上面的这段代码,在客户端显式启动AidlImpService。至于intent的显式或隐式启动的定义,这里不多赘述。
至此,服务端完成AIDL基本配置
客户端实现
首先同样是要开启aidl支持

拷贝aidl目录
需要将服务端aidl文件所在目录整个拷贝至客户端main目录下,最终客户端main目录下的结构如下

编译
我们想在客户端程序中调用用服务端对应类或方法,同样需要编译aidl文件生成java接口类

可以看到客户端也生成了对应java类
绑定服务端service
为了方便管理和业务隔离,我们定义了一个单例DevManageComunicateUtil来管理AIDL连接,由于我客户端都是kotlin实现的,所以后面客户端代码均用kotlin代码。
我们在客户端启动服务端的service,前面讲服务端实现时,我们说了可以隐式启动

这里有几个注意的点
- 隐式启动service时,intent传入的action必须与服务端AndroidManefest中声明的action完全一致
- 这里设置了package,即服务端的具体包名,经实践,如果不设置,可能也启动不了服务端的service
- bindService方法我们可以看到它有两个实现,这里我们用第一个实现,它的第二个参数要求传入ServiceConnection对象,我们必须手动构造一个这样的对象
构造ServiceConnection
我先直接贴出实现

这里分两步,第一步是声明一个IDevManageAidlInterface对象mAidlInterface,第二步是声明ServiceConnection对象mServiceConnection。这两步的写法是固定的,至于为什么这么写这里不讨论。
需要注意的是onServiceConnected方法里实现
mAidlInterface = IDevManageAidlInterface.Stub.asInterface(service)
这是mAidlInterface对象实现的关键,后续通过mAidlInterface调用服务端的方法
那么实现就变成了
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import com.blankj.utilcode.util.Utils
import com.vhd.mqttservice.IDevManageAidlInterface
import com.vhd.utility.Logger
object DevManageComunicateUtil {
private val log = Logger.get(this)
private var mAidlInterface: IDevManageAidlInterface? = null
private val mServiceConnection = object : ServiceConnection{
override fun onServiceConnected(
name: ComponentName?,
service: IBinder?
) {
mAidlInterface = IDevManageAidlInterface.Stub.asInterface(service)
}
override fun onServiceDisconnected(name: ComponentName?) {
log.e("onServiceDisconnected: $name")
mAidlInterface = null
}
}
fun connectAidl(){
val mIntent = Intent("com.hd.mqtt.status").apply {
`package` = "com.vhd.mqttservice"
}
Utils.getApp().bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE)
}
}
这里bindService最后一个参数的作用还没做具体研究,先按下不表,后续补充。
可以看下bindService的返回值代表什么,在具体业务时可能有用

调用服务端方法
这里举例我们要调用服务端isConnectMqtt()方法,就可以通过mAidlInterface对象实现
fun isMqttConnected(): Boolean{
return mAidlInterface?.isConnectMqtt?:false
}
前面我们在服务端定义了IDevManageStatusCallbak接口,在客户端如何使用呢
private val mIDevManageStatusCallbak = object: IDevManageStatusCallbak.Stub(){
override fun onConnectStatusChanged(connected: Boolean) {
//触发了回调
}
}
需要注意的是我们这里定义的对象是IDevManageStatusCallbak.Stub,不是IDevManageStatusCallbak
那么我们通过mAidlInterface对象就可以注册该回调mAidlInterface?.registerConnectStatusCallbak(mIDevManageStatusCallbak)
bindService时序问题
在bindService时,如果服务端还没有启动,也就是说客户端比服务端先启动,这时候去bindSuccess是不会成功的,而且bindService也没有自动重新“bind”的机制,如果你想实现自动重新bind是需要自己去实现的,如何做呢,这里可以提供一个思路

前面我们看到bindService有返回值,我们根据返回值来进行定时重新bindService,这里我用协程实现重连
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import com.blankj.utilcode.util.Utils
import com.vhd.mqttservice.IDevManageAidlInterface
import com.vhd.mqttservice.IDevManageStatusCallbak
import com.vhd.utility.Logger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
object DevManageComunicateUtil {
private val log = Logger.get(this)
private var mAidlInterface: IDevManageAidlInterface? = null
private val BIND_SERVICE_INTERVAL = 3000L
@Volatile
private var needRebind = true
private val mIDevManageStatusCallbak = object: IDevManageStatusCallbak.Stub(){
// 注意:回调方法运行在Binder线程池,更新UI需切主线程
override fun onConnectStatusChanged(connected: Boolean) {
log.i("onConnectStatusChanged: $connected")
}
}
private val mServiceConnection = object : ServiceConnection{
override fun onServiceConnected(
name: ComponentName?,
service: IBinder?
) {
log.i("onServiceConnected: $name")
mAidlInterface = IDevManageAidlInterface.Stub.asInterface(service)
registerStatusCallback()
needRebind = false
}
override fun onServiceDisconnected(name: ComponentName?) {
log.e("onServiceDisconnected: $name")
mAidlInterface = null
needRebind = true
}
}
fun loopAttempConnectAidl(){
val mIntent = Intent("com.hd.mqtt.status").apply {
`package` = "com.vhd.mqttservice"
}
CoroutineScope(Dispatchers.IO).launch {
while (true){
if (needRebind){
val bindSuccess = Utils.getApp().bindService(mIntent, mServiceConnection, Context.BIND_AUTO_CREATE)
if (bindSuccess){
log.i("bindService success")
needRebind = false
}else{
log.e("bindService failed")
//如果需要 可以重新bindService
needRebind = true
}
}
delay(BIND_SERVICE_INTERVAL)
}
}
}
private fun registerStatusCallback(){
mAidlInterface?.registerConnectStatusCallbak(mIDevManageStatusCallbak)
}
private fun unregisterStatusCallback(){
mAidlInterface?.unregisterConnectStatusCallbak(mIDevManageStatusCallbak)
}
/**
* 释放AIDL连接
*/
fun releaseService(){
unregisterStatusCallback()
Utils.getApp().unbindService(mServiceConnection)
}
}
以上实现的关键点
- 协程死循环监测标志位是否去bindservice
- needRebind的设置有多个地方,除了根据bindService结果,还在onServiceConnected onServiceDisconnected回调时置标志位,兼容了连上后又异常断连的情况
- 我们在定义needRebind 时添加了标准
@Volatile,这个是为了多线程环境下变量的可读,前面我们提到aidl的回调都是在binder线程池中,加上这里我们循环连接又是在协程里,多线程可读性必须要保证
aidl方法调用时序问题
我们在客户端调用服务端的方法会监听回调时,都应建立在已经正常连上服务端后,比如我要调用aidl定义的isConnectMqtt()方法,是应该等onServiceConnected回调发生后。
可以看到我们前面的代码,mAidlInterface?.registerConnectStatusCallbak的调用也是等onServiceConnected回调里面调用的
多进程访问问题
关于多进程,我第一次写也是宕机了我很久,这里需要特别注意多进程的问题,不止使用LDAP时。
前面我们在定义服务端时,有这样的配置

我们通过
android:process=":remote"配置了独立进程,这样我在业务进程调用独立进程,或者独立进程调用我们业务进程时,变量访问都是异常的。比如我们服务端,未配置LDAP时,所有业务都在进程A,并且我定义了全局静态变量a。加入LDAP功能后并且通过
android:process=":remote"配置了独立进程,那么我们服务端LDAP实现的service就运行在A:remote这个进程,这时候我在A:remote这个进程去访问A中全局静态变量a时,会发现根本不是真实的值。原因就是因为这是两个进程,会有进程隔离。
所以最简单的做法是不配置
android:process=":remote",使他们进程一致。那么,如果你必须要让LDAP运行在独立进程怎么办呢?在服务端参考客户端,实现LDAP客户端连接方法。感觉真的挺麻烦,所以最好的办法就是不要多进程。
至此,客户端基本完成了配置,后续就是根据自己的需要添加业务代码。
总结
之前一直都有了解和学习AIDL,这次是第一次动手实现两端代码,实践才能学习到更多(比如多进程导致变量访问失败问题)