官方提醒,只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,我们才有必要使用 AIDL。如果我们无需跨不同应用执行并发 IPC,则应通过 实现 Binde来创建接口;或者,如果我们想执行 IPC,但不需要处理多线程,请 使用 Messenger来实现接口。
本文主要是对于 AIDL 使用上的一点思考
场景:我创建了两个应用,一个 app 作为服务端: BlogService
,一个 app 作为客户端: BlogSample
。
其实同应用的多进程更常见一些,设计成多进程的目的嘛:
-
最主要的就是可以最大限度的获取系统资源,毕竟系统为每个进程分配的资源有限
-
进程间资源隔离,当前进程挂掉也不影响其他进程,很适合
Service
或一些辅助业务模块
但多进程也会带来一些问题,要注意:
- 静态成员和单例模式失效(不同进程访问同一个类会产生多个副本)
- 线程同步机制失效 (不同进程锁的不是同个对象)
- SharedPreferences 可靠性下降 (并发写操作,会造成丢数据)
- Application 会多次创建 (每启动一个进程都会被分配一个新的虚拟机)
定义AIDL接口
这里简单说下,AIDL 的创建步骤:
- 在服务端
BlogService
工程内创建一个.aidl
文件,文件路径可以放于main/aidl
目录下,注意这里/com/blog/service
路径可自定义,并非必须要与包名一致。在构建该应用时,AS 会帮我们自动生成基于该.aidl
文件的IBinder
接口,并将其保存到项目的build/generated/
目录中
- 在服务端,实现接口。AS 帮我们自动生成的
IBinder
接口,其拥有一个名为Stub
的内部抽象类,用于扩展Binder
类并实现 AIDL 接口中的方法。我们要在Stub
类内实现上面定义的pullFromService
方法
- 向客户端公开接口。其实就是在
BlogService
工程内创建一个Service
服务,通过重写onBind()
,从而能给连接上的客户端返回Stub
类的实现
- 在服务端
Manifest
文件内注册该Service
组件,注意android:exported=true
,才能被其他 app 调用。这里添加了action
属性,用来给其他 app 提供隐式调用绑定该服务。
调用AIDL接口
刚刚在服务端定义好了 com.blog.service.BlogService
服务,接下来就是在客户端 BlogSample
内去绑定该服务了,就是常规的 bindService
接口,不用想的太复杂。
- 首先还需要将服务端的
.aidl
文件(连同目录)拷贝一份到客户端,目的可以理解为,这个.aidl
文件,就是服务端与客户端通信的协议,客户端拿到了这个.aidl
文件,才能知道服务端提供了什么接口。二来,想在客户端使用IBlogManager
,不能无中生有啊,先要保证编译能通过吧。BlogSample
目录见下
- 客户端内我创建了一个
BlogServiceActivity
类,并调用bindService()
以连接BlogService
服务时,客户端的onServiceConnected()
回调会接收服务端的onBind()
方法所返回的binder
实例。
- 客户端拿到了
IBlogManager
对象了, 当然就能调用其内部定义的方法了
- 运行结果
传递类型
默认情况下,AIDL 支持下列数据类型:
- Java 编程语言中的所有原语类型(如
int
、long
、char
、boolean
等) String
CharSequence
List
Map
- 支持
Parcelable
接口的类对象 - AIDL 接口本身也可以在 AIDL 文件中使用
其中 List
中的所有元素必须是以上列表中支持的数据类型,或者我们所声明的由 AIDL 生成的其他接口或 Parcelable 类型。我们可选择将 List
用作“泛型”类(例如,List<String>
)。尽管生成的方法旨在使用 List
接口,但另一方实际接收的具体类始终是 ArrayList
。
Map
中的所有元素必须是以上列表中支持的数据类型,或者我们所声明的由 AIDL 生成的其他接口或 Parcelable 类型。不支持泛型 Map(如 Map<String,Integer>
形式的 Map)。尽管生成的方法旨在使用 Map
接口,但另一方实际接收的具体类始终是 HashMap
。
Parcelable 对象
接下来以 BlogInfo
对象为例,有几点要注意:
BlogInfo
必须实现Parcelable
接口,只有序列化了才能在进程间传输。- 需要新建一个
BlogInfo.aidl
文件 - AIDL 接口使用
BlogInfo
时,必须使用import
。
我们依然可以将 BlogInfo
相关文件放在 aidl
目录下,这么做的好处就是方便拷贝到客户端(相当于通信协议,服务端和客户端各一份才行啊),见下
同时我们需要在 .build
文件中指明 srcDirs
目录:
这是 BlogInfo
类代码,具体 Parcelable
用法就不用说了:
这是 BlogInfo.aidl
和 IBlogManager
内容:
接下来的调用步骤,与上面完全一样。将 aidl
目录拷贝到客户端,客户端连接服务,获取 IBlogManager
,再调用对应接口。
抽象类
上面栗子,我们在 IBlogManager#pushToService
中直接传递的是 BlogInfo
对象,这可能不太优雅,实际使用上,我们可能需要的是传递一个超类,这怎么办呢?
其实很简单,下面以 AbstractBlogInfo
为例:
- 在反序列化时,通过
className
获取子类对象,并执行readFromParcel
方法,子类重写该接口来进行反序列化操作。writeToParcel
基类接口负责序列化className
。
- 创建
AbstractBlogInfo.aidl
同名文件:
- 这是实际子类
BlogInfo1
,子类不需要再创建同名.aidl
文件了:
- 接下来就可以在
IBlogManager.aidl
中正常使用AbstractBlogInfo
了。
- 修改下服务端
BlogService
对应代码:
- 将 aidl 目录文件拷贝到客户端,在客户端直接传递
BlogInfo1
对象:
- 运行ok
AIDL接口
所有的 AIDL 接口本身也可以在 AIDL 文件中使用,下面我们以 IBlogListener.aidl
为例。
场景:要实现,客户端向服务端注册监听器,当 pushToService
方法被调用后,就通知客户端。
- 定义
IBlogListener
AIDL 接口
- 在
IBlogManager
AIDL 中使用:
- 修改服务端
BlogService
代码逻辑并重写方法:
- 修改客户端调用逻辑:
- 先运行服务端 app,再启动客户端 app, 运行结果:
代码很简单,但有个坑在里面,那就是上面的代码无法取消注册监听。想想肯定会如此,在多进程中,服务端接收到的注册与解注册的 IBlogListener 肯定不是同一个啊,即使客户端传递的是一个对象,但此对象经过序列化与反序列化后,最终生成的会是一个全新的对象。那有什么办法可以解决吗?
RemoteCallbackList
RemoteCallbackList
是系统专门提供的用于删除跨进程 listener
的接口,使用起来也很简单,修改下 BlogService
。
实测有效。
那为啥 RemoteCallbackList
就可以呢?
这个 RemoteCallbackList
内有个 Map 结构专门用来保存 AIDL 的回调,其中 key 是 IBinder
类型,value 是 Callback
类型(即我们真正的远程 listener)。
引用《Android开发艺术探索》
虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的 Binder 对象是同一个,利用这个特性,就可以实现上面的功能。当客户端解注册的时候,我们只要遍历服务端所有的 listener,找出那个和解注册 listener 具有相同 Binder 对象的服务端 listener,并把它删掉即可,这就是 RemoteCallbackList 为我们做的事情。
定向tag
在上面传递自定义对象时,可以看到我使用了一个 in
,
void pushToService(in BlogInfo info);
这个叫定向tag
,是指示数据走向的方向标记,这类标记可以是:
in
out
inout
其中 in
表示数据只能从客户端流向服务端,out
表示数据只能能服务端流向客户端,inout
表示数据可以在服务端和客户端双向流动。
上面的例子是使用的 in
, 下面以 out
为例,这是 IBlogManager
。
如果使用 out
或 inout
,则需要在 Parcelable
接口对象内添加 readFromParcel
方法,注意这不是重写或重载方法,这是上文的 BlogInfo
客户端创建 BlogInfo
对象并赋值传递到服务端:
服务端接收 BlogInfo
,并打印值:
运行结果:
我们发现值并没有正确传递过来,所以我们要正确选择合适的定向tag
。
扩展兼容
在写博客 demo 的时候,就深有感触,由于服务端和客户端维护的是一份 AIDL 协议,那如果服务端 app, 和客户端 app 分开开发呢?或者一方代码变更后,双方线上无法保证同时发版呢?
这是不是就要求我们在开发阶段,脑袋里就要有兼容性的概念。
在 aidl
开发上,常见的做法就是:
- 服务端
aidl
内提供获取版本信息的接口。 - 客户端绑定远程服务时,
intent
带上要请求的版本信息。
场景1: 服务端变更了,推出了 IBlogManager1.aidl
, 但客户端没有升级。我们只需要 :
- 客户端在绑定服务时,
intent
带上版本号:
- 在服务端根据版本号,来返回对应
IBinder
对象:
场景2: 客户端更新了,但服务端没有升级
- 尝试加载
IBlogManager1.aidl
新服务,如果新服务未上线,则使用老服务。这里使用了try catch
,虽然功能没问题,但很不优雅。
身份校验
默认情况下,我们的远程服务任何人都可以连接,但这并不是我们愿意看到的。在 AIDL 中进行身份校验,常见的有两种方式:
- 可以在
onBind
方法中进行验证,验证不通过就直接返回 null。 - 可以在服务端的
onTransact
方法中进行验证,如果验证失败就直接返回false。
这两种方式又常用 permission
和 PID、UID
的方式校验。
permission
以自定义 permission
为例:
- 在服务端
manifest
中的BlogService
添加android:permission
标签,其表示启动服务或绑定到服务所必需的权限的名称。如果startService()
、bindService()
或stopService()
的调用方尚未获得此权限,该方法将不起作用,且系统不会将 Intent 对象传送给服务
- 在服务端
manifest
中 定义一个app.blog.service.permission
权限:
- 在客户端中添加该权限
是不是很简单,如果客户端没有权限就启动 BlogService
会直接报错。关于自定义权限的知识点就不在这里介绍了。
这里有个坑的地方要注意,就是 checkCallingPermission
方法不能在 onBind
方法中调用,否则会一直返回 PackageManager.PERMISSION_DENIED
,因为 onBind
方法并没有运行在 Binder
线程池中,可以在 onTransact() 中调用。
package
我们也可以重写 Binder
的 onTransact
方法,在里面做检验,上面说了 permission
,常用的还有包名校验:
- 其实很简单,通过调用者的 UID 获取对应包名,代码直接看吧:
中断监听
我们知道当服务端进程由于某些原因异常终止,这个时候我们到服务端的 Binder
连接就会中断,会导致我们的远程调用失败。
目前常用的监听手段,包括:
- onServiceDisconnected:当客户端应用 A 和 服务端应用 B 正连接时,如果服务端 B 被杀死,那么二者的连接会立即中断,A 的
ServiceConnection
的onServiceDisconnected
会被调用。 - DeathRecipient:当
Binder
死亡的时候,系统会回调此方法,使用方法也很简单,直接在客户端onServiceConnected(name: ComponentName?, service: IBinder?)
回调中注册,注意要与 unlinkToDeath 配合使用: