前言
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 长什么样子。
这里我新建了一个项目,然后在项目模块上右键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
。
上面所列出来的类型统统不需要导入包,只管使用用即可,但是有一点除外,就是你自己定义的实体类,必须要导入包名。下面会讲到如何导入,现在我们已经了解了数据类型了。
定义方法
定义方法的时候遵循以下规则:
- 方法可带零个或多个参数,返回值或空值。
- 所有非基础数据类型参数均需要指明数据流动的方向标记。这类标记可以是
in
、out
或inout
,基本数据类型默认且只能是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文件。
新建两个方法:
// 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就会帮我们新建一个真正的接口,提供给你去实现你方法的业务逻辑:
如果你看不到这个接口,那么你重新build一下项目即可看到
第二步:
在Java路径下,新增两个空白文件待用:AidlInterfaceImpl
以及AidlService
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
}
}
其实就是把我们的实现文件,在Service
的onBind()
方法中把实现类的实例返回即可,如此一来,服务端就已经编写完成了!
不要忘记了在清单文件里把这个服务注册上去,同时开放访问权限,如下:
<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
的包名都要一致,看图:
所以我建议的做法是,编写好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()}")
}
}
}
第四步
一个完整的示例已经编写完了,是时候运行看效果了,首先我们先把服务端项目运行起来,然后再把客户端运行,查看打印的结果就知道了
可以看到打印结果,我们已经成功连接了两个项目,并进行通信,客户端给服务端发消息,并且得到返回值。
实现思路
我们现在完成了一个完整的从服务端到客户端的编码,可以发现其实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();
}
我们在原有的基础上新增了两个方法,setStringList
和getMap
,这两个方法分别演示了如何在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())
}
...
然后在你想要的地方调用即可:
aidl中传递自定义对象
需要在aidl中传递的对象必须是由 AIDL 生成的其他接口或 Parcelable 类型,这里我们新建一个Book类做示例。这个类是服务端和客户端都要同时使用的,所以这个类在两个端都必须要有。
编写服务端的Book类
还是在原有的基础上修改项目,在aidl文件夹下新增Book.aidl文件:
可以看到,我们声明实体类很简单的两行代码,里面具体的属性、实现都不需要声明(会在Book的实现文件里声明),类型为parcelable
紧接着,在aidl同一包名路径下,新建对应的Book.kt实体,我这里的aidl包名路径是:
com.hicyh.aidlservicedemo
所以Java那边也是要在同样的包名路径在新建对应的实体:
com.hicyh.aidlservicedemo
否则不能正确使用:
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文件复制过来到客户端:
因为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())
运行两个端查看结果:
可以看到,我们即能实现传递了自定义对象Book,又看清楚了流向标志的具体使用,结论和我们之前解释的一样。
总结
到这里,Android中Aidl的使用已经基本介绍完毕了,篇幅很长,很大原因是讲得太啰嗦了,但是代码是全的,也是经过验证过的,这里也会放出Demo,本来还想介绍一下aidl中怎样去使用回调方法,服务端怎么调客户端的方法(有没有发现我们写的都是客户端调服务端的?),但是这些都是举一反三了,如果理解了上面讲的那应该不难联想到怎样去实现,毕竟再讲下去,篇幅真的太长了!!!
【客户端demo】
【服务端demo】