Android之AIDL进程间通讯

Android之AIDL进程间通讯

前言

  AIDL是一个缩写,全称是Android Interface Definition Language,也就是Android接口定义语言。需要注意的是:它是一门语言。 实际开发当中,有时候需要实现进程间的通信,比如:守护进程和主进程的交互、一些需要放在独立进程的Service、SDK开发等等。 当我们遇到此类问题的时候,AIDL实现进程间通信显得尤为重要。

介绍及aidl语法

  我们知道谷歌设计这么一种语法是为了解决一个问题,那就是进程间的通信。那为什么要进程间通信?神马是进程间的通信?

介绍

  在我们的系统里,每一个进程都是一块独立的内存区域,每个进程间是不会相互干扰的,每个进程都有自己的行为、属性、数据等。一个个进程相当于一个个独立的星球,有着自己的运行轨迹。而 AIDL技术 就是这一个个星球之间的桥梁,进程之间数据的交互可以通过AIDL实现。
  当然了,在安卓的世界里,跨进程通信的方式不仅仅 AIDL 能实现,你也可以通过BroadcastReceiver , Messenger 等等,但是你想啊,如果通信频繁,那就需要频繁的发广播,这将会是非常消耗资源的。Messenger 进行跨进程通信时请求队列是同步进行的,无法并发执行,在有些要求多进程并发处理业务的情况下不适用。

我们可以引用官方的一段话说明:

只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,您才有必要使用 AIDL。如果您无需跨不同应用执行并发 IPC,则应通过实现 Binder 来创建接口;或者,如果您想执行 IPC,但不需要处理多线程,请使用 Messenger 来实现接口。无论如何,在实现 AIDL 之前,请您务必理解绑定服务。

aidl

它的语法基本和 Java 是一样的,所以我们学起来将会是非常快的上手,甚至不用刻意去学。

文件

在学习语法之前,我们先看看 aidl 长什么样子。
截屏2020-10-19 上午9.45.18

这里我新建了一个项目,然后在项目模块上右键new -> AIDL -> AIDL File 即可创建一个aidl文件。

语法

  aidl的语法也是十分简单,因为它只是简单声明一个接口而已,不需要具体的实现(具体的实现编写在实现文件中,一个.java或者.kt文件),它支持下列数据类型。

数据类型

  • Java 中的所有原语类型(如 int、long、char、boolean 等)

  • String

  • CharSequence

  • List
    List 中的所有元素必须是以上列表中支持的数据类型,或者是你自己所声明的,并且是由 AIDL 生成的其他接口或 Parcelable 类型。另一方实际接收的具体类是 ArrayList

  • Map
    Map 中的所有元素必须是以上列表中支持的数据类型,或者是你自己所声明的,并且是由 AIDL 生成的其他接口或 Parcelable 类型。AIDL不支持泛型Map,比如Map<String,String>是不被支持的,只需要写Map即可,另一方实际接收的具体类始终是 HashMap

上面所列出来的类型统统不需要导入包,只管使用用即可,但是有一点除外,就是你自己定义的实体类,必须要导入包名。下面会讲到如何导入,现在我们已经了解了数据类型了。

定义方法

定义方法的时候遵循以下规则:

  • 方法可带零个或多个参数,返回值或空值。
  • 所有非基础数据类型参数均需要指明数据流动的方向标记。这类标记可以是 inoutinout,基本数据类型默认且只能是in(具体意思下面会讲到,先了解大概)。
  • 可以在 AIDL 接口中定义 String 常量和 int 常量。例如:const int VERSION = 1; 这是合法的。
  • 可以用 @nullable 注释可空参数或返回类型。

到此,我们就已经学会了如何定义方法,下面看一个示例:

package com.hicyh.aidldemo;

interface IMyAidlInterface {

    //定义一个带返回值带方法
    String getName();
    
    //定义一个带参数带方法
    void setName(in String name);
}

Aidl真实用法示例

  我这里的示例全部建立在两个App之间的交互上,倘若有需要在一个App里实现跨进程通信,编写方式也一样。
在开始之前,我们需要建立两个项目:
第一个为:AidlServiceDemo,这是服务端;
第二个为:AidlClientDemo,这是客户端。

Aidl进程间通信是以客户端服务端的形式交互的。

完整示例

我们先吧两个项目建立出来,然后打开AidlServiceDemo,先进行服务端的编写。

编写AidlServiceDemo服务端

打开我们新建的AidlServiceDemo项目

第一步:

在项目模块上右键new -> AIDL -> AIDL File 即可创建一个aidl文件。

截屏2020-10-19 上午9.45.18

新建两个方法:

// IMyAidlInterface.aidl
package com.hicyh.aidlservicedemo;

// Declare any non-default types here with import statements

interface IMyAidlInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
            
    //新增如下两个方法
    void setName(in String name);
    
    String getName();
}

可以看到,我们上面的这一步,新建了一个aidl文件,并且新增了两个方法,那么AS就会帮我们新建一个真正的接口,提供给你去实现你方法的业务逻辑:

截屏2020-10-26 下午5.07.39

如果你看不到这个接口,那么你重新build一下项目即可看到

第二步:

在Java路径下,新增两个空白文件待用:AidlInterfaceImpl以及AidlService

截屏2020-10-26 下午3.09.59

AidlInterfaceImpl是对Aidl接口文件(如上图中的IMyAidlInterface.aidl文件)的具体实现。

AidlService是一个Android服务,客户端会绑定到这个服务,实现客户端到服务端的连接。

第三步

  我们第一步的时候已经创建了一个IMyAidlInterface.aidl文件了,而且这个文件里我们写了两个方法,没有方法体,AS帮我们生成了一个同名接口,那就这一步我们就来实现这个接口,即可实现我们的业务。

编辑AidlInterfaceImpl.kt文件:

package com.hicyh.aidlservicedemo

/**
 * Aidl接口的实现类
 * 继承自IMyAidlInterface.Stub()
 */
class AidlInterfaceImpl : IMyAidlInterface.Stub() {
    
    override fun basicTypes(
        anInt: Int,
        aLong: Long,
        aBoolean: Boolean,
        aFloat: Float,
        aDouble: Double,
        aString: String?
    ) {
        TODO("Not yet implemented")
    }

    override fun setName(name: String?) {
        TODO("Not yet implemented")
    }

    override fun getName(): String {
        TODO("Not yet implemented")
    }
}

AidlInterfaceImpl中继承IMyAidlInterface.Stub()IMyAidlInterface.Stub()是第一步新建aidl文件的时候,AS帮我们自动生成的,我们继承它,并且实现里面的方法,其实里面的方法就是我们在IMyAidlInterface.aidl中定义的方法。

我们可以随意在这几个实现方法中返回点东西,接着就编写AidlService,实现绑定客户端。

第四步

编写AidlService服务,如下代码:

package com.hicyh.aidlservicedemo

import android.app.Service
import android.content.Intent
import android.os.IBinder

/**
 * Aidl服务
 */
class AidlService : Service() {

    //实例化aidl的实现类
    private val mAidlImpl = AidlInterfaceImpl()

    override fun onBind(p0: Intent?): IBinder? {

        //返回我们的实现类即可
        return mAidlImpl
    }

}

其实就是把我们的实现文件,在ServiceonBind()方法中把实现类的实例返回即可,如此一来,服务端就已经编写完成了!

不要忘记了在清单文件里把这个服务注册上去,同时开放访问权限,如下:

        <service
            android:name=".AidlService"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="1000">
                <!--定义Action,客户端绑定的时候,如果Action不正确,将不会绑定成功,起到安全的作用-->
                <action android:name="com.hicyh.aidlservicedemo.AidlService" />
            </intent-filter>
        </service>

编写AidlClientDemo客户端

  上面我们写好了服务端,现在我们打开之前新建的AidlClientDemo客户端项目,其实演示的时候我们完全能把服务端和客户端写在同一个项目,我想来想去,还是分开项目写,这样能非常清晰的知道Aidl通信是跨进程的跨越App的。

第一步

  首先第一步是最重要的,我们需要把服务端里写的.aidl文件都复制到客户端,不管在何时何地,服务端的aidl文件都必须和客户端的aidl文件一致,非但文件必须要一致,就连aidl的包名都要一致,看图:

截屏2020-10-27 上午10.06.20

所以我建议的做法是,编写好Service的aidl文件后,直接复制aidl文件夹到客户端,避免出错。

第二步

我们新建一个客户端管理类——ClientManage,用来复制连接服务端以及调用服务端的方法的:

package com.hicyh.aidlclientdemo

import android.app.Application
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.IBinder
import android.util.Log
import com.hicyh.aidlservicedemo.IMyAidlInterface


object ClientManage {
    private val TAG = this.javaClass::getSimpleName.toString()

    //我们的服务接口
    private var mService: IMyAidlInterface? = null

    //服务连接的回调
    private val serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, service: IBinder) {
            //服务连接成功回调
            Log.d(TAG, "服务绑定成功!")
            //把我们的服务接口拿出来,就可以直接调我们写的服务端 AidlInterfaceImpl 的方法了
            mService = IMyAidlInterface.Stub.asInterface(service)
        }

        override fun onServiceDisconnected(name: ComponentName) {
            Log.d(TAG, "服务被断开了!")
        }
    }

    //开个方法,提供给外部绑定使用
    fun bindService(application: Application) {
        val myIntent = Intent()
        //这是服务端注册Service的时候,声明的Action,必须跟服务端的一样
        myIntent.action = "com.hicyh.aidlservicedemo.AidlService"
        //这是服务端的包名
        myIntent.`package` = "com.hicyh.aidlservicedemo"
        //这是绑定服务端的时候附带的数据,可以用此做个验证,如果传过去的参数服务端验证失败
        // 可以在服务端那边拒绝连接,起到安全的作用
        myIntent.putExtra("Auth", "xxxx")
        //开始绑定服务
        application.bindService(myIntent, serviceConnection, Context.BIND_AUTO_CREATE)
    }

    //公开getName方法,其实这里可能会抛出异常,我们暂时不处理
    fun getName(): String {
        return mService?.name ?: ""
    }

    //公开setName,其实这里可能会抛出异常,我们暂时不处理
    fun setName(name: String) {
        mService?.name = name
    }
}

如此一来,我们只需要在Application里绑定一次服务,就可以在任何地方使用ClientManage

第三步

在使用的时候,我们可以先绑定服务,然后直接调用其里面的方法即可!


package com.hicyh.aidlclientdemo

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        //这个绑定的过程预计耗时1到2秒
        ClientManage.bindService(this.application)
        //所以我们延迟执行下面的调用,一般做法是我们得到绑定成功的回调之后,才去调用方法
        txtView.setOnClickListener {
            ClientManage.setName("张三")
            Log.e("Main", "getName=${ClientManage.getName()}")
        }
    }
}

第四步

一个完整的示例已经编写完了,是时候运行看效果了,首先我们先把服务端项目运行起来,然后再把客户端运行,查看打印的结果就知道了

截屏2020-10-27 上午11.21.25

可以看到打印结果,我们已经成功连接了两个项目,并进行通信,客户端给服务端发消息,并且得到返回值。

实现思路

  我们现在完成了一个完整的从服务端到客户端的编码,可以发现其实aidl进程间通信就是通过放开一个Service,给别人去绑定,然后通过onBind()方法返回一个实现对象,客户端去绑定这个Service,得到这个实现的对象的接口文件,通过调用这个接口,从而实现了调用到服务端的具体逻辑。

in、out、inout定向标志

在.aidl文件的编写的时候,我们定义的方法里,所有的参数除了数据类型要声明出来之外,数据类型前面还加了一个标志:

// IMyAidlInterface.aidl
package com.hicyh.aidlservicedemo;

interface IMyAidlInterface {
    ...
    //声明方法的时候,对象类型的参数前面,会多有一个标签 in
    void setName(in String name);
    ...
}

这个标签是对象类型才需要显示的指定,当然,你不指定,就默认为in,基本数据类型默认、且只能为in标志。

这个标志代表着数据的流向,从哪里流向哪里的意思。

  • in 代表着对象数据只能从客户端流向服务端
  • out 代表着对象数据只能从服务端流向客户端
  • inout 不言而喻,这个代表着对象数据是双向流动的

可能这么说还不是很清楚,我们可以看以下代码打个比方:


class MainActivity : AppCompatActivity() {
    //假设有一个实体类
    data class Test(var name: String, var age: Int)

    //实例化这个实体类
    var tt = Test("张三", 26)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        //调用方法
        setTest(tt)

        //在来看看 tt 的age 已经 = 30 了
        Log.e("Main", tt.toString())
    }

    //在方法里面把Test这个对象的属性改变了
    private fun setTest(pt: Test) {
        pt.age = 30
    }
}

  从上面的代码可以看得出,在方法里改了外面对象的属性,则外面的对象也会跟着改,这是因为引用类型的缘故,方法里的对象持有的是实例的地址值,外面的tt也是持有此实例的地址值,他俩共同持有一个对象实例,所以任何一方修改,都会影响到另一方,也可以用来理解数据流向的问题。

回到我们的aidl文件中:

  • 如果参数标志是 in, 说明只能客户端的操作会影响到服务端,服务端不管怎么修改这个参数,客户端都不会同步这个修改。
  • 如果参数标志是out,说明数据只能从服务端影响到客户端,客户端不管怎么修改,这个参数最终只能从服务端那边过来。
  • 如果参数标志是inout, 说明这个参数是双向的,客户端和服务端只要任何一方修改了这个参数,那另外一方也会跟着被修改。比如还是上面的Test(),如果有一个方法传了Test()对象,并且用了inout标签,那客户端在本地把age属性改了,那么就会瞬间同步到服务端,服务端的这个Test()也是被修改之后的对象了,就相当于它们两个共同操作同一个对象一样。但是实际上它们是操作不同对象的,毕竟它们在不同的进程中,只是系统帮我们代理了这种关系而已。

List以及Map

List和Map是aidl直接支持的,不需要导入任何包,先讲如何传递List以及Map吧:

第一步

先把服务端和客户端的.aidl文件修改为如下:

// IMyAidlInterface.aidl
package com.hicyh.aidlservicedemo;

interface IMyAidlInterface {

    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

    //新增如下两个方法
    void setName(in String name);

    //返回字符串
    String getName();

    //使用List
    void setStringList(in List<String> strList);

    //返回Map,注意了,这里不要写成Map<String,String>,aidl不支持泛形
    Map getMap();
}

我们在原有的基础上新增了两个方法,setStringListgetMap,这两个方法分别演示了如何在aidl中使用List和Map。

⚠️注意了:aidl 不支持泛型 Map(如 Map<String,Integer> 形式的 Map)。 仅仅用Map就好,编译器会在自动生成的类库中使用HashMap,我们在实现文件中就可以使用泛形了,但在aidl文件里是不行的。

第二步

AidlServiceDemo服务端项目AidlInterfaceImpl.kt实现文件中,实现我们刚新增的两个方法:

package com.hicyh.aidlservicedemo

import android.util.Log

/**
 * Aidl接口的实现类
 */
class AidlInterfaceImpl : IMyAidlInterface.Stub() {

    ......

    override fun setStringList(strList: MutableList<String>?) {
        strList?.forEach {
            Log.e(TAG,"服务端收到 string=$it")
        }
    }


    override fun getMap(): MutableMap<String, String> {
        val map = mutableMapOf<String,String>()
        map["from"] = "服务端返回"
        map["name"] = "张三"
        map["age"] = "24"
        map["phone"] = "14539987544"
        return map
    }
}

我代码中是在原有的基础上修改的,省略了原先的代码。

第三步

修改客户端的ClientManage文件,同样是在原有的代码上新增两个方法即可:


...
    //使用list
    fun setStringList(list: MutableList<String>) {
        mService?.setStringList(list)
    }

    //使用map
    fun getMap() {
        val map = mService?.map
        Log.e("-->",map.toString())
    }
...

然后在你想要的地方调用即可:

截屏2020-10-27 下午5.22.59

aidl中传递自定义对象

  需要在aidl中传递的对象必须是由 AIDL 生成的其他接口或 Parcelable 类型,这里我们新建一个Book类做示例。这个类是服务端和客户端都要同时使用的,所以这个类在两个端都必须要有。

编写服务端的Book类

还是在原有的基础上修改项目,在aidl文件夹下新增Book.aidl文件:
截屏2020-10-27 下午5.46.37

可以看到,我们声明实体类很简单的两行代码,里面具体的属性、实现都不需要声明(会在Book的实现文件里声明),类型为parcelable

紧接着,在aidl同一包名路径下,新建对应的Book.kt实体,我这里的aidl包名路径是:
com.hicyh.aidlservicedemo

所以Java那边也是要在同样的包名路径在新建对应的实体:
com.hicyh.aidlservicedemo

否则不能正确使用:
截屏2020-10-27 下午5.56.26

Book具体完整的代码为:


package com.hicyh.aidlservicedemo

import android.os.Parcel
import android.os.Parcelable

/**
 * 这里的写法都已经是模版写法了,可以拿来修改一些属性,或者新增一些方法就可使用,因为下面的方法、构造函数都是必须的
 */
open class Book(var name: String?, var age: Int, var content: String?) : Parcelable {

    //无参构造函数,这是必须的,因为aidl里有可能返回空参数的Book实体, out inout流向标签
    constructor() : this(null, 0, null) {
    }

    constructor(parcel: Parcel) : this(
        parcel.readString(),
        parcel.readInt(),
        parcel.readString()
    ) {
    }

    override fun writeToParcel(parcel: Parcel, flags: Int) {
        parcel.writeString(name)
        parcel.writeInt(age)
        parcel.writeString(content)
    }

    //注意了,如果aidl中使用到 out 或者  inout ,那么必须手动实现readFromParcel方法
    fun readFromParcel(reply: Parcel) {
        name = reply.readString()
        age = reply.readInt()
        content = reply.readString()
    }


    override fun describeContents(): Int {
        return 0
    }

    override fun toString(): String {
        return "Book(name='$name', age=$age, content='$content')"
    }

    companion object CREATOR : Parcelable.Creator<Book> {
        override fun createFromParcel(parcel: Parcel): Book {
            return Book(parcel)
        }

        override fun newArray(size: Int): Array<Book?> {
            return arrayOfNulls(size)
        }
    }


}

确保包路径一致之后,我们的实体Book还要实现Parcelable接口,方便在aidl中传输,里面的属性我们可以随便定义,跟常规的类无差别,但是要特别注意代码中的注释。

编写服务端的主要接口IMyAidlInterface.aidl

我们新增四个方法,正好也验证一下数据流向标签in out inout的使用。


package com.hicyh.aidlservicedemo;

//1、使用自定义对象必须要导入包
import com.hicyh.aidlservicedemo.Book;

interface IMyAidlInterface {

    ...

    //2
    //使用in
    void setBookForIn(in Book book);
    //使用out
    void setBookForOut(out Book book);
    //使用inout
    void setBookForInOut(inout Book book);
    //返回值为自定义对象
    Book getBookForReturn();
}

可见,我们上面代码做了两步操作,1是导入Book,2是新增四个方法,分别实验流向标签以及返回值。

然后我们重新build一下代码,去AidlInterfaceImpl实现文件里把这四个方法实现了。

AidlInterfaceImpl新增四个实现方法:


 ...

 override fun setBookForIn(book: Book?) {
        Log.e(TAG, "服务端setBookForIn=${book.toString()}")
        //我们尝试去修改book
        book?.content = "在setBookForIn被服务端修改啦"
        Log.e(TAG, "服务端修改后=${book.toString()}")
    }

    override fun setBookForOut(book: Book?) {
        Log.e(TAG, "服务端setBookForOut=${book.toString()}")
        //我们尝试去修改book
        book?.content = "在setBookForOutn被服务端修改啦"
        Log.e(TAG, "服务端修改后=${book.toString()}")
    }

    override fun setBookForInOut(book: Book?) {
        Log.e(TAG, "服务端setBookForInOut=${book.toString()}")
        //我们尝试去修改book
        book?.content = "在setBookForInOut被服务端修改啦"
        Log.e(TAG, "服务端修改后=${book.toString()}")
    }

    override fun getBookForReturn(): Book {
        return Book("张三", 30, "长得帅")
    }

 ...

这样子一来,我们的服务端就已经写好了,现在该修改客户端了。

修改客户端支持自定义对象

aidl通信的时候,服务端的aidl文件一定要跟客户端的保持一致,所以我们需要把刚才服务端写的aidl文件复制过来到客户端:
截屏2020-10-28 上午9.32.58

因为Book实体是服务端客户端都必须要用到都,所以也要一模一样的搬过来,上图中注意事项已经说得很清楚了。

紧接着,我们在客户端中编写调用接口的方法:
ClientManage文件中新增:


    ...

    fun setBookForIn(book: Book) {
        mService?.setBookForIn(book)
    }

    fun setBookForOut(book: Book) {
        mService?.setBookForOut(book)
    }

    fun setBookForInOut(book: Book) {
        mService?.setBookForInOut(book)
    }

    fun getBook(): Book? {
        return mService?.bookForReturn
    }

在Activity中调用:


...

ClientManage.setBookForIn(Book("in 客户端过来的", 30, "客户端牛逼!"))

ClientManage.setBookForOut(Book("out 客户端过来的", 30, "客户端牛逼!"))

val inoutBook = Book("inout 客户端过来的", 30, "客户端牛逼!")
ClientManage.setBookForInOut(inoutBook)

Log.e("main", ClientManage.getBook().toString())
//我们再单独打印一下 inoutBook ,因为之前说这个流向标签是双向的,而我们在服务端已经修改了他的属性
//看看会不会联动到客户端这边
Log.e("inoutBook", inoutBook.toString())

运行两个端查看结果:

截屏2020-10-28 下午2.28.28

可以看到,我们即能实现传递了自定义对象Book,又看清楚了流向标志的具体使用,结论和我们之前解释的一样。

总结

  到这里,Android中Aidl的使用已经基本介绍完毕了,篇幅很长,很大原因是讲得太啰嗦了,但是代码是全的,也是经过验证过的,这里也会放出Demo,本来还想介绍一下aidl中怎样去使用回调方法,服务端怎么调客户端的方法(有没有发现我们写的都是客户端调服务端的?),但是这些都是举一反三了,如果理解了上面讲的那应该不难联想到怎样去实现,毕竟再讲下去,篇幅真的太长了!!!
【客户端demo】
【服务端demo】