Android进程间通信通常使用的Binder的方式,而Binder则是以C/S模式实现的,即Binder的主体作为服务端提供服务,Binder的引用端作为客户端请求服务。但是Binder是在Framework层使用C++实现的,为了方便Android应用端的开发,对Native层的Binder进行包装实现了Java层的Binder,并又进一步提供了AIDL的实现方式来简化进程间调用,这就是本篇文章所记录的内容。

Binder的媒介

作为C/S架构的实现,Binder需要客户端和服务端。服务端也就是Binder的主体,需要注册在ServiceManager中;客户端会通过Binder的名字获取到对应Binder的代理,然后就可以进行通信了。但是在应用层开发中,我们肯定不能直接注册Binder的,因此需要匿名Binder的实现,而匿名Binder应用最广泛的就是四大组件的Service了。

1
2
3
4
5
class ServerService : Service() {
override fun onBind(intent: Intent?): IBinder? {
// 在这里返回Binder实体作为服务端
}
}

通过bindService方式启动的服务,可以实现onBind方法来返回Binder实体作为服务端对象。当服务被绑定后,该Service进程会被拉起来,此时该Service进程就是服务端了。绑定者所在的进程会拿到IBinder的代理对象,然后两个进程就通过IBinder来进行进程间的交互。

服务端

前面说了Binder获取的方式,后面则是Binder的实现了。为了简化Binder的实现,Android提供了AIDL的方式来封装各种Binder通用的操作,让开发者仅需关注具体的业务功能即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
plugins {
...
}

android {
...
buildFeatures {
aidl = true
}
}

dependencies {
...
}

首先需要在build.gradle.kts中开启AIDL的支持,即在android闭包下新增buildFeatures闭包,然后声明aidl=truegroovy语法也是一样的。然后在源码的同级目录下新增aidl目录app\src\main\aidl,该目录就是存放AIDL文件的目录。

如果直接通过右键-new-AIDL-new aidlFile创建AIDL文件的话,会在aidl目录下生成一个和当前应用同样的包名目录,然后新的AIDL文件则是在这个目录下。实际上,这个包名是不必和应用的包名一致的,可以随意定义。AIDL文件不会有任何IDE提示的,需要我们完全手写。

1
2
3
4
5
6
7
8
app\src\main\aidl\com\server\aidl\IServer.aidl

package com.server.aidl;

interface IServer {
// 定义了一个方法,服务端提供文本
String getTextFromServer();
}

接下来需要点击AS的build -> make project触发文件的生成,AS会根据AIDL文件去生成对应的Binder类,然后我们继承该类即可。生成的文件在app\build\generated\aidl_source_output_dir\debug\out\com\server\aidl\IServer.java中,当然我们不需要关注他在哪,我们直接使用即可。新建一个类继承IServer.Stub,然后实现方法即可。如果提示找不到这个类的话,尝试clean一下再重新build

1
2
3
4
5
6
7
8
9
/**
* Binder的实体,作为服务端使用,继承IServer.Stub后只需要关注需要实现的方法即可
*/
class Server : IServer.Stub(){

override fun getTextFromServer(): String {
return "我是服务端返回的文本"
}
}

然后在Service中的onBind将该对象返回即可。

1
2
3
4
5
class ServerService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return Server()
}
}

注意在manifest中声明时,exported属性必须设置成true并且定义intent-filter,因为我们是要在另一个app中绑定该服务的。另外,在exported设为true后,还需要通过权限进行控制,这里偷懒就不加权限了。

1
2
3
4
5
6
7
8
9
10
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<application>
<service
android:name=".ServerService"
android:exported="true">
<intent-filter>
<action android:name="com.example.server.action" />
</intent-filter>
</service>
</application>
</manifest>

至此,服务端所做的工作已经完成了。总结下来就是创建一个aidl文件并声明所支持的接口方 法,然后build生成对应的类,然后再继承生成的xxx.Stub类实现支持的接口方法即可。

客户端

客户端要做的工作更简单了,首先创建一个新的项目,然后在build.gradle.kts中开启aidl的支持,然后将服务端创建的aidl复制到客户端的目录下。注意:复制过来的AIDL文件的包名必须和服务端保持一致。然后Build一下生成具体的代码。

接下来就是在客户端中绑定服务端的Service,然后拿到IBinder对象,然后就可以进行交互了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class MainActivity : AppCompatActivity() {

private lateinit var mBtnRequest: Button
private lateinit var mTextView: TextView
private var mServer: IServer? = null

private val mConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
// 绑定服务后拿到IServer,后续就通过IServer进行进程间交互
mServer = IServer.Stub.asInterface(service)
}

override fun onServiceDisconnected(name: ComponentName?) {
mServer = null
}
}

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContentView(R.layout.activity_main)
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
insets
}
init()
}

private fun init() {
mBtnRequest = findViewById(R.id.btn_request)
mTextView = findViewById(R.id.text)
mBtnRequest.setOnClickListener {
// 点击后通过IServer获取到服务端返回的字串
mTextView.text = mServer?.textFromServer ?: "default"
}
val intent = Intent().also {
// 服务端Service的action
it.action = "com.example.server.action"
// 服务端Service所在的app的包名
it.`package` = "com.example.serverdemo"
}
bindService(intent, mConnection, BIND_AUTO_CREATE)
}

override fun onDestroy() {
super.onDestroy()
unbindService(mConnection)
}

}

服务端很简单,在Activity启动时绑定服务端的服务,然后拿到IBinder通过生成的IServer.Stub.asInterface转换成IServer,然后就可以直接进行交互了。因为我们是在客户端app中绑定服务端app的,在Android 11以后需要在Manifest中声明要绑的包名,否则无法绑定成功。

1
2
3
4
5
6
7
8
9
10
<manifest
<!-- 必须声明服务端的包名 -->
<queries>
<package android:name="com.example.serverdemo"/>
</queries>

<application
</application>

</manifest>

AIDL支持的类型

前面的示例说明了AIDL的交互方式,从代码层面上看,在客户端拿到IServer后,可以直接调用方法进行交互,即以同步的方式进行跨进程的调用。既然实际是跨进程调用的,那么实现上肯定是有一些限制的。如参数的类型,就不是全部类型都能使用的。

默认数据类型

基本数据类型:byte, char, int, long, float, double, boolean, 以及基本类型对应的数组

字串类型:CharSequence, String, 以及字串类型对应的数组

集合类型:List, Map, 最新的版本中集合类型已经不局限于ArrayListHashMap

除了以上类型外,如果想要传递自定义的类型,则需要实现Parcelable接口。

手动定义Parcelable

首先在代码目录中创建一个User类,然后实现Parcelable接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 注意包名!
package com.example.server

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

data class User(
val name:String,
val age:Int
):Parcelable {
constructor(parcel: Parcel) : this(
parcel.readString().toString(),
parcel.readInt()
) {
}

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

override fun describeContents(): Int {
return 0
}

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

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

然后在aidl目录中新建一个User.aidl注意该文件的包名必须和User类的包名保持一致

1
2
3
4
// User.aidl

package com.example.server;
parcelable User;

然后实际使用的地方也需要进行导包,注意as不会提示导包,必须要手动输入:

1
2
3
4
5
6
7
8
9
package com.server.aidl;
// 必须进行import!!
import com.example.okhttpdemo3.User;
interface IServer {
String getTextFromServer();
void otherTypes(
in User user
);
}

自动生成Parcelable

前面手动生成Parcelable的地方,User类定义在了代码目录中,因此当复制aidl文件到客户端的时候,必须要把User类的文件也同时复制过去,并且包名路径要完全一致,这无疑是很麻烦的。因此可以使用aidl自动生成Parcelable的方式来实现User类。

1,删除前面定义的User类文件

2,修改User.aidl内容

1
2
3
4
5
6
7
// User.aidl

package com.example.server;
parcelable User {
int age;
String name;
}

这样build之后会自动生成一个User类,不需要再写那些复杂的逻辑了。

属性修饰符

在前面的自定义Parcelable时,可以看到在AIDL使用自定义Parcelable类型的时候,加上了属性修饰符in。实际上,除了byte, char, int, long, float, double, boolean, CharSequence, String外,其他的类型作为参数时都必须添加属性修饰符。

in

数据从客户端传递到服务端,也是最常用的调用方式。也就是数据作为参数从客户端传入,可以理解为值传递,就是在服务端对这个参数进行修改的话,是影响不到客户端的。

1
2
3
4
5
6
7
8
9
10
package com.server.aidl;
// 必须进行import!!
import com.example.okhttpdemo3.User;
interface IServer {
String getTextFromServer();
void otherTypes(
// 定义成in类型
in User user
);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 服务端的Binder实体
class Server : IServer.Stub() {
private val TAG = "Server"
override fun getTextFromServer(): String {
return "服务端返回的字串"
}

override fun otherType(user: User?) {
Log.d(TAG, "user.name = ${user?.name}, user.age = ${user?.age}")
// 在服务端将传递的参数的值变化下
user?.age = 80
}
}

// 客户端调用
mBtnRequest.setOnClickListener {
val user = User().also {
it.name = "张三"
it.age = 23
}
mServer?.otherType(user)
Log.d(TAG, "after call, name = ${user.name}, age = ${user.age}")
}

打印的结果值如下,可以看到即使在服务端修改了User的字段,实际上客户端的User仍没有发生变化:

1
2
com.example.serverdemo    D  user.name = 张三, user.age = 23
com.example.clientdemo D after call, name = 张三, age = 23

out

数据由服务端流向客户端,这种方式服务端无法获取到User的数据,但是对User的修改却能反馈到客户端中去。将前面aidl文件中的in修改为out后,在分别build并运行后,输入的log如下:

1
2
com.example.serverdemo    D  user.name = null, user.age = 0
com.example.clientdemo D after call, name = null, age = 80

可以看到,在客户端实际传入的是一个有内容的User,但是在服务端中是无法读取到User的内容的。但是在服务端修改User后,客户端的User也修改了,并且客户端的原始内容被清空了。因此,out修饰符就是提供一个对象,用于接收服务端对该对象的修改。

inout

数据双向流通,即服务端能获取到客户端传入的内容,客户端也能收到服务端对该内容的修改。将前面aidl文件中的in修改为out后,在分别build并运行后,输入的log如下:

1
2
com.example.serverdemo    D  user.name = 张三, user.age = 23
com.example.clientdemo D after call, name = 张三, age = 80

可以看到,服务端能读取到User的内容,并且对User内容的修改也能反馈到客户端。这种修饰符修饰下,对跨进程调用的方法,表现形式是和本进程内的调用java方法一样的。

总结

AIDLAndroid提供的跨进程调用的工具,实际上AIDL文件是不会被打包到app中的,只是根据其内容来生成代码文件,将跨进程那一套通用的逻辑都自动生成了,方便我们只关注业务逻辑部分。从使用上来说,AIDL一般在系统应用中才用的比较多,因为很多系统应用间都需要互相交互,使用AIDL无疑是非常方便的。而对于普通应用,一般是大体量的app会将本身分成多个进程,然后去使用AIDL实现进程间交互。