Android修炼系列(39),AIDL 有很多要注意的地方

官方提醒,只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,我们才有必要使用 AIDL。如果我们无需跨不同应用执行并发 IPC,则应通过 实现 Binder 来创建接口;或者,如果我们想执行 IPC,但不需要处理多线程,请 使用 Messenger 来实现接口。

本文主要是对于 AIDL 使用上的一点思考,如想学习更多 AIDL 的基础知识,可自行看下 官网文档

场景:我创建了两个应用,一个 app 作为服务端: BlogService,一个 app 作为客户端: BlogSample

其实同应用的多进程更常见一些,设计成多进程的目的嘛:

  • 最主要的就是可以最大限度的获取系统资源,毕竟系统为每个进程分配的资源有限

  • 进程间资源隔离,当前进程挂掉也不影响其他进程,很适合 Service 或一些辅助业务模块

但多进程也会带来一些问题,要注意:

  • 静态成员和单例模式失效(不同进程访问同一个类会产生多个副本)
  • 线程同步机制失效 (不同进程锁的不是同个对象)
  • SharedPreferences 可靠性下降 (并发写操作,会造成丢数据)
  • Application 会多次创建 (每启动一个进程都会被分配一个新的虚拟机)

定义AIDL接口

这里简单说下,AIDL 的创建步骤:

  1. 在服务端 BlogService 工程内创建一个 .aidl 文件,文件路径可以放于 main/aidl 目录下,注意这里 /com/blog/service 路径可自定义,并非必须要与包名一致。在构建该应用时,AS 会帮我们自动生成基于该 .aidl 文件的 IBinder 接口,并将其保存到项目的 build/generated/ 目录中

image.png

  1. 在服务端,实现接口。AS 帮我们自动生成的 IBinder 接口,其拥有一个名为 Stub 的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。我们要在 Stub 类内实现上面定义的 pullFromService 方法

image.png

  1. 向客户端公开接口。其实就是在 BlogService 工程内创建一个 Service 服务,通过重写 onBind(),从而能给连接上的客户端返回 Stub 类的实现

image.png

扫描二维码关注公众号,回复: 14124768 查看本文章
  1. 在服务端 Manifest 文件内注册该 Service 组件,注意 android:exported=true,才能被其他 app 调用。这里添加了 action 属性,用来给其他 app 提供隐式调用绑定该服务。

image.png

调用AIDL接口

刚刚在服务端定义好了 com.blog.service.BlogService 服务,接下来就是在客户端 BlogSample 内去绑定该服务了,就是常规的 bindService 接口,不用想的太复杂。

  1. 首先还需要将服务端的 .aidl 文件(连同目录)拷贝一份到客户端,目的可以理解为,这个 .aidl 文件,就是服务端与客户端通信的协议,客户端拿到了这个 .aidl 文件,才能知道服务端提供了什么接口。二来,想在客户端使用 IBlogManager,不能无中生有啊,先要保证编译能通过吧。BlogSample 目录见下

image.png

  1. 客户端内我创建了一个 BlogServiceActivity 类,并调用 bindService() 以连接 BlogService 服务时,客户端的 onServiceConnected() 回调会接收服务端的 onBind() 方法所返回的 binder 实例。

image.png

  1. 客户端拿到了 IBlogManager 对象了, 当然就能调用其内部定义的方法了

image.png

  1. 运行结果

image.png

传递类型

默认情况下,AIDL 支持下列数据类型:

  • Java 编程语言中的所有原语类型(如 intlongcharboolean 等)
  • 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 对象为例,有几点要注意:

  1. BlogInfo 必须实现 Parcelable 接口,只有序列化了才能在进程间传输。
  2. 需要新建一个 BlogInfo.aidl 文件
  3. AIDL 接口使用 BlogInfo 时,必须使用 import

我们依然可以将 BlogInfo 相关文件放在 aidl 目录下,这么做的好处就是方便拷贝到客户端(相当于通信协议,服务端和客户端各一份才行啊),见下

image.png

同时我们需要在 .build 文件中指明 srcDirs 目录:

image.png

这是 BlogInfo 类代码,具体 Parcelable 用法就不用说了:

image.png

这是 BlogInfo.aidlIBlogManager 内容:

image.png

接下来的调用步骤,与上面完全一样。将 aidl 目录拷贝到客户端,客户端连接服务,获取 IBlogManager ,再调用对应接口。

抽象类

上面栗子,我们在 IBlogManager#pushToService 中直接传递的是 BlogInfo 对象,这可能不太优雅,实际使用上,我们可能需要的是传递一个超类,这怎么办呢?

其实很简单,下面以 AbstractBlogInfo 为例:

  1. 在反序列化时,通过 className 获取子类对象,并执行 readFromParcel 方法,子类重写该接口来进行反序列化操作。writeToParcel 基类接口负责序列化 className

image.png

  1. 创建 AbstractBlogInfo.aidl 同名文件:

image.png

  1. 这是实际子类 BlogInfo1,子类不需要再创建同名 .aidl 文件了:

image.png

  1. 接下来就可以在 IBlogManager.aidl 中正常使用 AbstractBlogInfo 了。

image.png

  1. 修改下服务端 BlogService 对应代码:

image.png

  1. 将 aidl 目录文件拷贝到客户端,在客户端直接传递 BlogInfo1 对象:

image.png

  1. 运行ok

image.png

AIDL接口

所有的 AIDL 接口本身也可以在 AIDL 文件中使用,下面我们以 IBlogListener.aidl 为例。

场景:要实现,客户端向服务端注册监听器,当 pushToService 方法被调用后,就通知客户端。

  1. 定义 IBlogListener AIDL 接口

image.png

  1. IBlogManager AIDL 中使用:

image.png

  1. 修改服务端 BlogService 代码逻辑并重写方法:

image.png

  1. 修改客户端调用逻辑:

image.png

  1. 先运行服务端 app,再启动客户端 app, 运行结果:

image.png

代码很简单,但有个坑在里面,那就是上面的代码无法取消注册监听。想想肯定会如此,在多进程中,服务端接收到的注册与解注册的 IBlogListener 肯定不是同一个啊,即使客户端传递的是一个对象,但此对象经过序列化与反序列化后,最终生成的会是一个全新的对象。那有什么办法可以解决吗?

RemoteCallbackList

RemoteCallbackList 是系统专门提供的用于删除跨进程 listener 的接口,使用起来也很简单,修改下 BlogService

image.png

实测有效。

那为啥 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

image.png

如果使用 outinout,则需要在 Parcelable 接口对象内添加 readFromParcel 方法,注意这不是重写或重载方法,这是上文的 BlogInfo

image.png

客户端创建 BlogInfo 对象并赋值传递到服务端:

image.png

服务端接收 BlogInfo ,并打印值:

image.png

运行结果:

image.png

我们发现值并没有正确传递过来,所以我们要正确选择合适的定向tag

扩展兼容

在写博客 demo 的时候,就深有感触,由于服务端和客户端维护的是一份 AIDL 协议,那如果服务端 app, 和客户端 app 分开开发呢?或者一方代码变更后,双方线上无法保证同时发版呢?

这是不是就要求我们在开发阶段,脑袋里就要有兼容性的概念。

aidl 开发上,常见的做法就是:

  1. 服务端 aidl 内提供获取版本信息的接口。
  2. 客户端绑定远程服务时,intent 带上要请求的版本信息。

场景1: 服务端变更了,推出了 IBlogManager1.aidl, 但客户端没有升级。我们只需要 :

  1. 客户端在绑定服务时,intent 带上版本号:

image.png

  1. 在服务端根据版本号,来返回对应 IBinder 对象:

image.png

场景2: 客户端更新了,但服务端没有升级

  1. 尝试加载 IBlogManager1.aidl 新服务,如果新服务未上线,则使用老服务。这里使用了 try catch,虽然功能没问题,但很不优雅。

image.png

身份校验

默认情况下,我们的远程服务任何人都可以连接,但这并不是我们愿意看到的。在 AIDL 中进行身份校验,常见的有两种方式:

  • 可以在 onBind 方法中进行验证,验证不通过就直接返回 null。
  • 可以在服务端的 onTransact 方法中进行验证,如果验证失败就直接返回false。

这两种方式又常用 permissionPID、UID 的方式校验。

permission

以自定义 permission 为例:

  1. 在服务端 manifest 中的 BlogService 添加 android:permission 标签,其表示启动服务或绑定到服务所必需的权限的名称。如果 startService()bindService() 或 stopService() 的调用方尚未获得此权限,该方法将不起作用,且系统不会将 Intent 对象传送给服务

image.png

  1. 在服务端 manifest 中 定义一个 app.blog.service.permission 权限:

image.png

  1. 在客户端中添加该权限

image.png

是不是很简单,如果客户端没有权限就启动 BlogService 会直接报错。关于自定义权限的知识点就不在这里介绍了。

这里有个坑的地方要注意,就是 checkCallingPermission 方法不能在 onBind 方法中调用,否则会一直返回 PackageManager.PERMISSION_DENIED,因为 onBind 方法并没有运行在 Binder 线程池中,可以在 onTransact() 中调用。

image.png

package

我们也可以重写 BinderonTransact 方法,在里面做检验,上面说了 permission,常用的还有包名校验:

  1. 其实很简单,通过调用者的 UID 获取对应包名,代码直接看吧:

image.png

中断监听

我们知道当服务端进程由于某些原因异常终止,这个时候我们到服务端的 Binder 连接就会中断,会导致我们的远程调用失败。

目前常用的监听手段,包括:

  • onServiceDisconnected:当客户端应用 A 和 服务端应用 B 正连接时,如果服务端 B 被杀死,那么二者的连接会立即中断,A 的 ServiceConnectiononServiceDisconnected 会被调用。
  • DeathRecipient:当 Binder 死亡的时候,系统会回调此方法,使用方法也很简单,直接在客户端 onServiceConnected(name: ComponentName?, service: IBinder?) 回调中注册,注意要与 unlinkToDeath 配合使用:

image.png

好了,代码都上传到了 github,下文介绍实现 Binder

本节完。

参考资料

  1. developer.android.com/guide/compo…

猜你喜欢

转载自juejin.im/post/7095612574150410253