AIDL保姆级使用教程

当前studio版本信息


image.png

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

服务端实现

新建AIDL文件

image.png

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

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

点击圈中部分
image.png

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

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

这里的第二行package com.vhd.mqttservice;很重要,服务端和客户端必须保证一模一样才能正常使用

定义回调接口

我们想有一些状态回调方法的话,同样需要新建AIDL文件
在上一步中生成的IDevManageAidlInterface.aidl文件的目录上右键,按照图示方式选择新建aidl文件


image.png

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


image.png

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

make生成java文件

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

image.png

image.png

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

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

我们将AIDL文件中添加注册与解注册的回调,这里要注意import com.vhd.mqttservice.IDevManageStatusCallbak;是需要我们手动导入的,完成修改后记得再次编译才能生效

service实现

AIDL定义的接口或方法都需要在Service里实现具体定义,我们在项目适当位置(推荐在项目根目录)新建java类并继承至Service

image.png

image.png

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

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

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

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

别忘了前面说的onBind返回的null不是我们想要的,那么我们将返回值修改为我们刚定义的mBinder


image.png

注册service

在AndroidManifest.xml中注册service


image.png

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支持


image.png

拷贝aidl目录

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


image.png

编译

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


image.png

可以看到客户端也生成了对应java类

绑定服务端service

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


image.png

这里有几个注意的点

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

构造ServiceConnection

我先直接贴出实现


image.png

这里分两步,第一步是声明一个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的返回值代表什么,在具体业务时可能有用


image.png

调用服务端方法

这里举例我们要调用服务端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是需要自己去实现的,如何做呢,这里可以提供一个思路


image.png

前面我们看到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)
    }
}

以上实现的关键点

  1. 协程死循环监测标志位是否去bindservice
  2. needRebind的设置有多个地方,除了根据bindService结果,还在onServiceConnected onServiceDisconnected回调时置标志位,兼容了连上后又异常断连的情况
  3. 我们在定义needRebind 时添加了标准@Volatile,这个是为了多线程环境下变量的可读,前面我们提到aidl的回调都是在binder线程池中,加上这里我们循环连接又是在协程里,多线程可读性必须要保证

aidl方法调用时序问题

我们在客户端调用服务端的方法会监听回调时,都应建立在已经正常连上服务端后,比如我要调用aidl定义的isConnectMqtt()方法,是应该等onServiceConnected回调发生后。
可以看到我们前面的代码,mAidlInterface?.registerConnectStatusCallbak的调用也是等onServiceConnected回调里面调用的

多进程访问问题

关于多进程,我第一次写也是宕机了我很久,这里需要特别注意多进程的问题,不止使用LDAP时。
前面我们在定义服务端时,有这样的配置

image.png

我们通过android:process=":remote"配置了独立进程,这样我在业务进程调用独立进程,或者独立进程调用我们业务进程时,变量访问都是异常的。
比如我们服务端,未配置LDAP时,所有业务都在进程A,并且我定义了全局静态变量a。加入LDAP功能后并且通过android:process=":remote"配置了独立进程,那么我们服务端LDAP实现的service就运行在A:remote这个进程,这时候我在A:remote这个进程去访问A中全局静态变量a时,会发现根本不是真实的值。
原因就是因为这是两个进程,会有进程隔离。
所以最简单的做法是不配置android:process=":remote",使他们进程一致。
那么,如果你必须要让LDAP运行在独立进程怎么办呢?在服务端参考客户端,实现LDAP客户端连接方法。感觉真的挺麻烦,所以最好的办法就是不要多进程。

至此,客户端基本完成了配置,后续就是根据自己的需要添加业务代码。

总结

之前一直都有了解和学习AIDL,这次是第一次动手实现两端代码,实践才能学习到更多(比如多进程导致变量访问失败问题)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容