AIDL快速使用上手
AIDL即Android接口定义语言,是用来实现跨进程通信的一种模板接口语言,AS可以根据我们编写的AIDL生成对应的Java代码,以方便我们的使用。它底层是使用Binder进行通信的,但是自己手写的话是还是比较麻烦的,因此可以使用AIDL定义接口语言,然后经过构建后就会生成对应的代码,减少我们的工作量。
之前在学习AIDL的时候也写过Demo,但时间久了就容易忘,当再写的时候又得去查资料,因此这里记录一下AIDL的使用,方便日后查询。
新建AIDL文件
首先直接右键选择new一个AIDL文件,命名为IMyServer
,他默认会生成一个basicTypes
方法,里面的参数是支持跨进程通信的一些基本类型。可以看到,AIDL和Java接口几乎是一样的,可以让我们轻松上手。
// IMyServer.aidl
package com.feng.server;
// Declare any non-default types here with import statements
interface IMyServer {
/**
* 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);
}
跨进程传递数据
由生成的默认方法可以看到,基本数据类型int,long,boolean,float,double,String
在AIDL中都是支持的,也就是说基本类型都是可以进行跨进程传递的,另外还支持List
和Map
集合,它们最终会转化成ArrayList
和HashMap
进行传输。
但是实际中这些类型肯定是不够用的,因此需要我们自己实现的对象类型。而自定义的对象类型为了能够实现跨进程的能力,必然能够支持序列化。在Android中有两种方法,一个是实现Serialzable
接口,一个是实现Parcelable
接口。虽然Serializable和Parcelable都可以实现对象的序列化,但是Serializable是java的序列化接口,实现简单,但是开销大,序列化和反序列化有大量io操作;Parcelable是Android的接口,效率很高,Android中推荐使用Parcelable。
使用说明:
在对象序列化于内存中,尽量使用Parcelable
在对象序列化于储存设备中,使用Serializable
在对象序列化于网络传输中,使用Serializable
而AIDL主要是为了实现进程中的通信,是序列化于内存中的,因此使用Parcelable比较好。
比如我们定义一个Person
对象
data class Person(var age: Int, var name: String?) : Parcelable {
constructor(parcel: Parcel) : this(parcel.readInt(), parcel.readString())
// 注意这个方法
fun readFromParcel(parcel: Parcel) {
age = parcel.readInt()
name = parcel.readString()
}
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(age)
parcel.writeString(name)
}
override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<Person> {
override fun createFromParcel(parcel: Parcel) = Person(parcel)
override fun newArray(size: Int): Array<Person?> = arrayOfNulls(size)
}
override fun toString() = "[${age},${name}]"
}
实现Parcelable
接口,必须实现writeToParcel
和describeContents
两个方法。writeToParcel是将数据写入Parcelable中,可以将需要进行传递的字段写入Parcel中进行传递。describeContents可以返回0
或者CONTENTS_FILE_DESCRIPTOR
,对于一个普通Bean,可以直接返回0。
还有注意,向Parcel写入字段的顺序是有要求的,先写入的数据在读取的时候也要先读出来,类似于队列一样,先写先读。可以看上面的Person
类,在writeToParcel
方法中先写入age
后写入name
,那么在对应的构造方法中也是先读age
属性后读name
。
实现Parcelable还需要在对象中定义一个静态对象CREATOR
,这个对象是Parcelable.Creator
类型的,它负责构造对象。在跨进程传递后,会被调用CREATOR
来将Parcel转化成对象。
经过上述的修改,此时的Person就已经具有序列化的能力了,也就是能够跨进程传递了,于是我们就可以在AIDL中使用它了。AIDL主要是声明一系列的接口方法,这些方法应该是服务端的方法,也就是这里的方法会在服务端中实现,由客户端进行调用。
// IMyServer.aidl
package com.feng.server;
parcelable Person;
interface IMyServer {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void sendPerson(inout Person person);
Person getPerson();
}
我们在aidl中定义了两个方法,sendPerson
和getPerson
,一个是将Person
对象发送到服务端,一个是从服务端获取Person
对象,刚好用来测试Person
的跨进程能力。
虽然Person
已经实现了Parcelable
,但若要在aidl中使用,还是需要声明的,就像import
一样。可以看到上面的aidl文件中,在import的位置加上了parcelable Person
,这样才可以在aidl中使用Person。
当然,也可以新建另一个aidl文件,但是这个文件中不写接口,只用来声明这些Parcelable
类,然后在aidl接口文件中import
这个aidl声明文件,如下:
//Person.aidl
package com.feng.server;
parcelable Person;
然后此时的IMyServer
中应该这样写:
package com.feng.server;
import com.feng.server.Person;
interface IMyServer {
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString);
void sendPerson(inout Person person);
Person getPerson();
}
这里的Person.aidl
文件名是可以不设置为Person
的,当然最好跟声明的对象保持一致,而且该文件中也是可以声明多个Parcelable
对象的。若是要使用Person.aidl
的话,在新建AIDL的时候可能会提示Person已存在而无法创建,这时候可以先改为其他名字,然后再右键-refactor-rename
修改回来。或者先定义Person.aidl
,再定义Person.java
。
修饰符
还有就是对象定向修饰符,就像在sendPerson(inout Person person)
中的inout
一样。这样的修饰符一共有三个,in/out/inout
。基本类型默认为in
,自定义的类型必须要指明。
in
:数据由客户端流向服务端,即对象Person
会由客户端传递给服务端,即正常的传递。服务端接收的对象内部数据与客户端是的一致的,相当于复制了一个对象传给了服务端。out
:数据由服务端流向客户端,若要使用这种tag
,Person
对象还要增加一个默认的空构造方法。在服务端接收到Person后,这个Person
的内部数据都为空,也就是不保留内部数据,实际上会调用Person
的空构造方法构造一个对象。虽然服务端拿到的这个对象并没有数据,但是它却是一个类似于客户端那个对象的分身的存在,也就是说服务端对这个Person对象的字段进行修改,会同步到客户端。比如客户端调用sendPerson(Person)
传递的Person.age = 10
,而在服务端中将收到的Person的age
改为20,此时客户端的那个Person对象的age
就会变成20。inout
:综合了in和out,即客户端既能传递带数据的对象,服务端对他的修改也能同步到客户端。使用这个tag需要给Person对象增加一个readFromParcel(Parcel)方法,这个方法用于从Parcel中读取数据。注意读取字段的顺序要与写入的顺序一致。
为了更好的应对这些定向tag,最好在定义Person的时候就加上空构造方法和readFromParcel方法。
注意包名
在新建aidl文件时,默认使用的包名为项目的包名,因此,定义的Person
类必须放在该包目录下,也就是新建项目的那个默认包,否则会报错找不到这个类,也就是说Person的包名必须和在AIDL的包名一致。
因此,当客户端和服务端不在同一个项目中的时候,为了实现aidl通信,需要将aidl复制到另一个项目中,此时一定要注意aidl的包名,保证aidl的包名和Person的包名一致。
生成代码
做完以上这些后可以直接build/rebuild project
,然后就会生成对应的Java代码。该代码分为两个部分,接口实现部分和对象占位部分。如下图中的IMyServer.java
和Person.java
,其中IMyServe
r是主要的实现代码,而Person.java
则是一个空文件,我们声明的parcelable都会生成对应的占位文件,因为上面只声明了一个Person
对象,因此也只生成了一个。
服务端
这个IMyServer.java
就是对AIDL的实现,生成这个后就可以编写具体的客户端和服务端代码了。首先编写服务端代码:
// MyService.java
class MyService : Service() {
override fun onBind(intent: Intent): IBinder = object : IMyServer.Stub() {
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String?
) {
"basic type:[${anInt},${aLong},${aBoolean},${aFloat},${aDouble},${aString}]".logD()
}
override fun sendPerson(person: Person?) {
person?.logD()
person?.age = 19
}
override fun getPerson() = Person(23, "李华")
}
}
AIDL基本上都是基于Service
实现的,因此这里服务端也是一个Service
。这里定义了一个MyService
,继承自Service
,只实现了onBind
方法。在onBind
中,我们的返回对象就是IMyServer.Stub
。这个Stub是AS根据我们的AIDL文件生成的IMyServer
的一个静态内部抽象类,它继承自Binder
并且包含有我们的定义的方法。这个Stub就是实现跨进程通信的基础,因此在onBind中直接返回一个Stub对象,并实现了我们在aidl中定义的那三个方法。实现都比较简单,都是打印一下传递过来的对象的信息,logD()
是定义的一个拓展方法,内部调用的是Log.d()。其中在sendPerson
中改了person的age
属性,是为了测试inout
的双向传递能力。
写完Service后不要忘记在Manifest
中注册:
<service
android:name=".MyService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.feng.server.service" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
注意要将exported
设置为true
,这样就可以在其他进程中调用到了。还要设置intent-filter
,因为其他项目中是无法访问到MyService
对象的,只能通过intent-filter
进行绑定。这里因为是在两个项目中访问的,所以没有设置service的进程,它默认运行在当前项目进程中,若是在同一个项目中进行测试,可以设置process
属性,以让它运行在其他进程中。
客户端
服务端完成之后,就要编写客户端代码了:
class MainActivity : AppCompatActivity() {
// IMyServer接口,用于调用服务端代码
private var iMyServer: IMyServer? = null
// 连接属性,在连接上服务端Service后,将onBind返回的IBinder转化成IMyServer
private val conn = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
iMyServer = IMyServer.Stub.asInterface(service)
"bind connected".logD()
}
override fun onServiceDisconnected(name: ComponentName?) {
"bind disconnected".logD()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 按钮,点击进行绑定
bind.setOnClickListener {
val intent = Intent().apply {
action = "com.feng.server.service"
addCategory(Intent.CATEGORY_DEFAULT)
setPackage("com.feng.servce")
}
bindService(intent, conn, Context.BIND_AUTO_CREATE).logD()
}
// 按钮,点击开始进行交互,分别测试三个方法
send.setOnClickListener {
iMyServer?.basicTypes(12, 13L, true, 13.1F, 13.2, "from client")
val person = Person(18,"client Person")
iMyServer?.sendPerson(person)
person.logD()// 打印person对象,该对象在服务端中age被改为了19,这里会显示age为19
iMyServer?.person.logD()
}
}
override fun onDestroy() {
unbindService(conn)
super.onDestroy()
}
}
上面就是在Activity
中绑定服务的过程,在绑定上Service
的时候,在onConnected
中会将IBinder
转化成IMyServer
。这里的IBinder
就是Service
中的onBind
方法返回的对象,若是同一个进程该对象就是它本身,若是不同的进程中则会转化成BinderProxy
对象。在生成的IMyServer.Stub
的静态方法asInterface
中,会根据这种情况进行转换。因此我们可以直接使用IMyServer
来进行调用Server的方法而不用考虑跨进程的问题。
注意,在不同的项目中是无法访问到MyService
对象的,因此绑定服务只能通过隐式绑定,即在intent
中设置action
和category
,另外通过隐式绑定的话需要加上查找包名,即设置package
为service
所在程序的包名。
总结
可见在AIDL的帮助下,我们跨进程的代码非常简单,就像是在同一个进程下一样,可以直接通过接口引用调用服务端的方法。而背后都是AIDL帮我们生成的代码去实现的,这些代码全部在生成的文件IMyServer.java
中,可以根据这个文件简单了解一下背后如何通过Binder
进行通信。