一、概述
首先我们看下谷歌官方对AIDL的介绍
AIDL(Android Interface Defination Language):安卓接口定义语言。在 Android 中,一个进程通常无法访问另一个进程的内存。因此,为进行通信,进程需将其对象分解成可供操作系统理解的原语,并将其编组为可供您操作的对象。编写执行该编组操作的代码较为繁琐,因此 Android 会使用 AIDL 为您处理此问题。
总而言之,如果在一个线程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它,客户端可以实现简介调用服务端对象的方法。
二、AIDL原理
2.1、调用图
2.2、关键类和方法
AIDL本质是系统提供了一套可以快速实现Binder的工具。关键类和方法:
- AIDL接口:继承
IInterface
- Stub类:
Binder
的实现类,服务端通过这个类来提供服务,通常由编译器生成。 - Proxy类:服务端的本地代理,客户端通过这个类来调用服务端的方法。通过
asInterface
方法获得stub
接口的实例 - 客户端调用:将服务端返回的
Binder
对象,转换成客户端所需要的AIDL
接口类型对象,如果客户端与服务端位于同一进程,则直接返回Stub
对象本身,否则返回系统封装后的Stub.proxy
对象 - asBinder:运行在服务端的
Binder
线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。 - transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的
onTransact()
直到远程请求返回,当前线程才继续执行。
三、使用AIDL的方法(以下内容均来自谷歌官网)
主要有以下三个步骤:
3.1、 创建.aidl文件
AIDL文件的编写是通过Java构建,文件的编写更像是在编写一个接口文件,只需要接口声明和方法签名,不同之处在于部分数据类型上的差异,AIDL支持以下数据类型:
- Java 编程语言中的所有原语类型(如 int、long、char、boolean 等)
String
CharSequence
-
List
:List
中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。您可选择将List
用作“泛型”类(例如,List)。尽管生成的方法旨在使用List
接口,但另一方实际接收的具体类始终是ArrayList
。 -
Map
:Map
中的所有元素必须是以上列表中支持的数据类型,或者您所声明的由 AIDL 生成的其他接口或 Parcelable 类型。不支持泛型 Map(如Map<String,Integer>
形式的Map
)。尽管生成的方法旨在使用Map
接口,但另一方实际接收的具体类始终是HashMap
。
定义方法时,需要注意:
- 方法可带零个或多个参数,返回值或空值。
- 所有非原语参数均需要指示数据走向的方向标记。这类标记可以是
in
、out
或inout
(见下方示例)。
原语默认为in
,不能是其他方向。 - 生成的
IBinder
接口内包含.aidl
文件中的所有代码注释(import 和 package 语句之前的注释除外)。 - 可以在 AIDL 接口中定义
String
常量和int
字符串常量。例如:const int VERSION = 1;
。 - 方法调用由
transact()
代码分派,该代码通常基于接口中的方法索引。由于这会增加版本控制的难度,因此您可以向方法手动配置事务代码:void method() = 10;
。 - 使用
@nullable
注释可空参数或返回类型。
以下是.aidl
文件示例
// IRemoteService.aidl
package com.example.android
// Declare any non-default types here with import statements
/** Example service interface */
internal interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
val pid:Int
/** Demonstrates some basic types that you can use as parameters
* and return values in AIDL.
*/
fun basicTypes(anInt:Int, aLong:Long, aBoolean:Boolean, aFloat:Float,
aDouble:Double, aString:String)
}
您只需将 .aidl
文件保存至项目的 src/
目录内,这样在构建应用时,SDK 工具便会在项目的 gen/
目录中生成 IBinder
接口文件。生成文件的名称与 .aidl
文件的名称保持一致,区别在于其使用 .java
扩展名(例如,IRemoteService.aidl
生成的文件名是 IRemoteService.java
)。
3.2、实现接口
当构建应用时,Android SDK 工具会生成以 .aidl
文件命名的 .java
接口文件。生成的接口包含一个名为 Stub
的子类(例如,YourInterface.Stub
),该子类是其父接口的抽象实现,并且会声明 .aidl
文件中的所有方法,上一章提到的asInterface
方法也是在该Java文件中生成。
到这里我们只是完成接口文件的编写,如要实现 .aidl
生成的接口,请扩展生成的 Binder 接口(即Stub
子类),并实现继承自 .aidl
文件的方法。
以下示例展示使用匿名实例实现 IRemoteService
接口(由以上 IRemoteService.aidl
示例定义)的过程:
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int =
Process.myPid()
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
现在,binder
是 Stub
类的一个实例(一个 Binder
),其定义了服务的远程过程调用 (RPC) 接口。在下一步中,我们会向客户端公开此实例,以便客户端能与服务进行交互。
在实现 AIDL 接口时,应注意遵守以下规则:
- 由于无法保证在主线程上执行传入调用,因此您一开始便需做好多线程处理的准备,并对您的服务进行适当构建,使其达到线程安全的标准。
- 默认情况下,RPC 调用是同步调用。如果您知道服务完成请求的时间不止几毫秒,则不应从 Activity 的主线程调用该服务,因为这可能会使应用挂起(Android 可能会显示“Application is Not Responding”对话框)— 通常,您应从客户端内的单独线程调用服务。
- 您引发的任何异常都不会回传给调用方。
3.3、向客户端公开接口
在为服务实现接口后,您需要向客户端公开该接口,以便客户端进行绑定。如要为您的服务公开该接口,请扩展 Service
并实现 onBind()
,从而返回实现生成的 Stub
的类实例(如前文所述)。以下是向客户端公开 IRemoteService
示例接口的服务示例。
class RemoteService : Service() {
override fun onCreate() {
super.onCreate()
}
override fun onBind(intent: Intent): IBinder {
// Return the interface
return binder
}
private val binder = object : IRemoteService.Stub() {
override fun getPid(): Int {
return Process.myPid()
}
override fun basicTypes(
anInt: Int,
aLong: Long,
aBoolean: Boolean,
aFloat: Float,
aDouble: Double,
aString: String
) {
// Does nothing
}
}
}
现在,当客户端(如 Activity)调用 bindService()
以连接此服务时,客户端的 onServiceConnected()
回调会接收服务的 onBind()
方法所返回的 binder
实例。
客户端还必须拥有接口类的访问权限,因此如果客户端和服务在不同应用内,则客户端应用的 src/
目录内必须包含 .aidl
文件(该文件会生成 android.os.Binder
接口,进而为客户端提供 AIDL 方法的访问权限)的副本。
当客户端在 onServiceConnected()
回调中收到 IBinder
时,它必须调用 YourServiceInterface.Stub.asInterface(service)
,以将返回的参数转换成 YourServiceInterface
类型。例如:
var iRemoteService: IRemoteService? = null
//用于控制远程服务器的连接
val mConnection = object : ServiceConnection {
// Called when the connection with the service is established
override fun onServiceConnected(className: ComponentName, service: IBinder) {
// Following the example above for an AIDL interface,
// this gets an instance of the IRemoteInterface, which we can use to call on the service
iRemoteService = IRemoteService.Stub.asInterface(service)
}
// Called when the connection with the service disconnects unexpectedly
override fun onServiceDisconnected(className: ComponentName) {
Log.e(TAG, "Service has unexpectedly disconnected")
iRemoteService = null
}
}
四、总结
如要调用通过 AIDL 定义的远程接口,服务端必须执行以下步骤:
- 在项目的
src/
目录中加入.aidl
文件, - 声明一个
IBinder
接口实例(基于 AIDL 生成)。 - 实现
ServiceConnection
。 - 调用
Context.bindService()
,从而传入ServiceConnection
的实现。 - 在
onServiceConnected()
实现中,您将收到一个IBinder
实例(名为service
)。调用XXX.Stub.asInterface((IBinder)service)
,以将返回的参数转换为XXX
类型。 - 调用您在接口上定义的方法。您应始终捕获
DeadObjectException
异常,系统会在连接中断时抛出此异常。您还应捕获SecurityException
异常,当 IPC 方法调用中两个进程的 AIDL 定义发生冲突时,系统会抛出此异常。 - 如要断开连接,请使用您的接口实例调用
Context.unbindService()
。
总结一下:
- 当我们编写
.aidl
文件时,会生成Stub
内部子类,客户端通过这个子类与服务端进行通信;Stub
子类中有asInterface
方法,我们可以通过这个方法获取客户端服务的实例(当服务端与客户端在同一进程返回的是Stub
本身,如果在不同的进程,返回的是Stub.Proxy
代理); - 当服务端使用
bindService
进行服务绑定,会传入对应的ServiceConnection
对象,ServiceConnection.onServiceConnected
回调会接收服务端onBind
方法返回的binder
实例。可以调用RemoteService
的asInterface
方法将IBinder
对象转换成对应的RemoteService
实例,当客户端与服务端在同一个进程,则直接返回Stub
对象本身,否则返回系统封装后的Stub.proxy
对象。 -
onTransact
方法在Stub
子类中被定义,客户端使用RemoteService
的某个方法时,.aidl
文件生成的java方法中会使用transact
方法,该方法被调用用来分发请求(实际上对其中的方法进行编号,然后通过根据请求编号来调用对应的方法,编号一般按方法定义顺序,从0开始)。Proxy
是Stub
类的内部类,服务端的本地代理,也有对应的asBinder
方法。
扩展
当有多个模块都需要AIDL来进行IPC,此时需要为每个模块创建特定的AIDL文件,那么相应的Service就会很多。必然会出现系统资源耗费严重,应用过度重量级的问题。解决方法是建立Binder连接池,即将每个业务模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。
工作原理:每个业务模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对
应的Binder对象。服务端只需要一个Service并提供一个queryBinder接口,它会根据业务模块的特征来
返回相应的Binder对象,不同的业务模块拿到所需的Binder对象后就可以进行远程方法的调用了。