本文谨作为读书笔记使用
学习内容:(IPC 机制)
- Android 中的多进程概念及注意事项
- 序列化机制
- Binder (重点)
- Bundle、文件共享、AIDL、Messenger、ContentProvider 和 Socket 等进程间通信方式
- Binder 连接池
- 各种进程间通讯的优缺点和使用场景
1. IPC 简介
- IPC(Inter-Process Communication),进程间通信,两个进程之间数据交换的过程
- 进程与线程
- 进程:执行单元(一般指一个程序或应用)
- 线程:CPU 调度的最小单元。
- 关系:
- 一个进程包含多个线程,主线程为 UI 线程,只有 UI 线程能更新UI。
- 为避免 ANR (Application Not Response),在子线程中进行耗时操作
- 不同平台下的 IPC 机制:
- Windows :剪贴板 && 管道 && 油槽
- Linux :命名管道 && 共享内存 && 信号量
- Android : Binder && Socket
- 使用场景
- 应用因为某些原因自身需要采用多进程模式实现
- 为增大一个应用可使用的内存,使用多进程获取多份内存空间,原因是 Android 最单个应用能使用的最大内存做了限制。
2. Android 的多进程模式
使用方法(唯一)
- 在 AndroidMenifest 中为四大组件指定 android:process 属性
- (非常规手段,WTF??)通过 JNI 在 native 层 fork 一个新的进程。
进程命名问题
完整命名,如
com.whdalive.xxx.remote
属于全局进程,其他应用通过 ShareUID 方式可以和他跑在同一个进程下,此时需要二者具有相同的签名。在这种情况下,二者共享 data 目录、组件信息、内存数据。简写命名,如
:remote
属于私有进程,其他应用的组件不可以和它跑在同一个进程中。
运行机制与问题
静态成员和单例模式完全失效
线程同步机制完全失效
↑ 原因:不处在同一块内存。每个进程分配一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,因此不同虚拟机中访问同一个对象会产生多个副本。SharedPreferences 的可靠性下降
原因:SharedPreferences 不支持两个进程同时进行写操作,有一定几率导致数据丢失。(底层就是 XML 文件)Application 多次创建
原因:当一个组件跑在新进程中的时候,系统要在创建新的进程同时分配独立的虚拟机,所以相当于启动一个应用的过程,自然会创建新的 Application
3. IPC 基础概念介绍
Serializable
静态成员变量不属于对象,并不会参与序列化过程
transient 关键字标记的成员变量不会参与序列化过程
Parcelable
Binder
多角度介绍
IPC 角度:跨进程通信方式
- 可理解为虚拟的物理设备,设备驱动是
/dev/binder
- Android Framework 角度:Binder 是 ServiceManager 连接各种 Manager 和相应 ManagerService 的桥梁
Android 应用层角度:Binder 是客户端和服务端通信的媒介。
AIDL
自己动手写 AIDL 需要了解以下内容,源码见 随书源码
系统会为我们生成同名的 .java 类,其继承 IInterface 这个接口,同时自身也还是个接口:
- 首先声明我们在 .aidl 文件中声明的方法,同时声明整型的 id 标识分别标识方法,该 id 用于标识在 transact 过程中客户端请求的是哪个方法。
- 接着声明一个内部类 Stub,其实质就是一个 Binder
- Stub 的内部代理类 Proxy 处理以下逻辑:只有客户端和服务端处在不同进程时,方法调用才需要走 transact 过程
核心:Stub 及其 内部代理类 Proxy
DESCRPTOR
:Binder 的唯一标识,一般用当前 Binder 的类名表示asInterface(android.os.IBinder obj)
:用于将服务端的 Binder 对象转换成客户端所需的 AIDL 接口类型的对象。同进程下返回服务端的 Stub 对象本身,否则返回 Stub.Proxy 对象asBinder
:返回当前 Binder 对象onTransact
:运行在服务端中的 Binder 线程池中,跨进程时,远程请求会通过系统底层封装后交由此方法来处理。
方法原型如下:public Boolean onTransact(int code,android.os.Parcel data,android.os.Parcel reply,int flags)
- code:确定客户端请求方法
- data:取出目标方法所需的参数
- reply:目标方法执行完毕后的返回值
- 返回false时,客户端的请求会失败->该特性可用作权限验证
Proxy.[Method]
:Proxy 类中开发者声明的方法。运行在客户端,内部实现:首先创建该方法所需要的输入型参数 Parcel 对象 _data 和输出型参数 Parcel 对象 _reply,然后把参数写入 data 中,接着调用 transact 方法来发起 RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的 onTransact 方法会被调用直到 RPC 过程返回后,当前线程继续执行,并从 _reply 中去除 RPC 过程的返回结果。两个注意事项:
1 如果远程方法耗时,那么不能在 UI 线程中发起词远程i请求
2 Binder 方法无论耗时与否都应该采用同步的方式去实现
Binder 死亡代理 – linkToDeath 和 unlinkToDeath
通过 linkToDeath 给 Binder 设置死亡代理,当 Binder 死亡时,会收到通知,此时可以重新发起连接请求从而恢复连接。首先,声明一个 DeathRecipient 对象,其本身是一个接口,内部只有一个 binderDied 方法,当 Binder 死亡的时候,系统会回调 binderDied 方法。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){ @Override public void binderDied(){ if (mBookManager == null) return; mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0); mBookManager = null; //TODO:这里重新绑定远程 Service } };
其次,客户端绑定远程服务成功后,给 binder 设置死亡代理
mService = IMessageBoxManager.Stub.asInterface(binder); binder.linkToDeath(mDeathRecipient,0);
4. Android 中的 IPC 方式
Bundle
- 范围:Activity、Service、Receiver 支持在 Intent 中传递 Bundle 数据
- 原理:Bundle 实现了 Parcelable 接口
- 限制:传递的数据必须能够被 序列化
文件共享
范围:适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。可以传递文本文件,其包含文本信息,或者序列化对象,也可以是 XML 文件
原理:两个进程通过 读/写同一个文件来交换数据
限制:要尽量避免并发写这种情况或者考虑使用线程同步来限制多个线程的写操作。
SharedPreferences 不适合用于进程间通信,原因如下:
系统对 SharedPreferences 的读/写有缓存策略,即内存中有 SharedPreferences 文件的缓存,因此多进程模式下,系统对 SharedPreferences 的读写不可靠
Messenger:
范围:轻量级的 IPC 方案,主要用于传递消息
原理:底层实现是 Binder
限制:串行处理,一次处理一个请求,不适合高并发情形
使用:
服务端进程
创建一个 Service 来处理客户端的连接请求,同时创建一个 Handler 并通过它来创建一个 Messenger 对象,然后在 Service 的 onBind 方法中返回底层的 Binder 即可。
客户端进程
绑定服务端的 Service,通过返回的 IBinder 对象创建 Messenger,以此发送 Message 类型消息。
AIDL
范围:处理大量的跨进程并发请求
原理:Binder
限制:线程同步的问题
使用:
服务端
首先创建 Service 监听客户端的连接请求,然后创建 AIDL 文件,并在此 AIDL 文件中声明暴漏给客户端的接口,最后在 Service 种实现这个 AIDL
接口即可
客户端
绑定服务端的 Service,之后将返回的 Binder 对象转换成 AIDL 接口所属的类型,之后调用相应的方法
支持的数据类型
- 基本数据类型(int、long、char、boolean、double等)
- String 和 CharSequence
- List:只支持 ArrayList
- Map:只支持 HashMap
- Parcelable:所有实现Parcelable 接口的对象
- AIDL:所有的 ADL 接口本身也可以在 AIDL 文件中使用
注意事项
- 自定义的Parcelable 对象必须创建一个同名的 AIDL 文件,并声明为 Parcelable 类型,并 improt 进来
- 建议将所有和 AIDL 相关的类和文件都放入同一个包中,方便把整个包复制到客户端工程中。
- CopyOnWriteArrayList 支持并发读/写代替直接使用 ArrayList,类似的使用 ConcurrentHashMap 代替直接使用 HashMap
- RemoteCallbackList 是系统专门提供的用于删除跨进程 listener 的接口。RemoteCallbackList 是一个泛型,支持管理人一的 AIDL 接口,原理是内部持有一个 Map 结构专门用来保存所有的 AIDL 回调,其中 value:Callback 封装了真正的远程 listener
- 远程被调用的方法运行在服务端的 Binder 线程池中,同时客户端线程挂起,因此需要在子线程调用远程耗时方法。
- 客户端 onServiceConnected 和 onServiceDisconnected 方法运行在 UI 线程,不可直接调用远程耗时方法
- 服务端方法本身运行在服务端的 Binder 线程池,因此本身就可以执行耗时操作,此时不建议开线程进行异步任务
权限验证
- 在 onBind 中进行验证,不通过直接返回 null -> 应用无法绑定到我们的服务
- 在 onTransact 中验证,不通过直接返回 false -> 客户端请求失败,服务端不执行 AIDL 的方法。
- 具体验证方式:
- permission 验证
- Uid 和 Pid 验证
ContentProvider
- 范围:专门用于不同应用间数据共享的方式,以表格形式组织数据,可以包含多个表,同时也支持文件数据,比如图片、视频等。
- 原理:Binder
- 限制:线程同步的问题
- 使用:例子看书吧。
- 注意事项
- android:authorities 和 android:permission 属性
- 通过 ContentResolver 的 notifyChange() 方法通知数据发生改变。
Socket
- 范围/原理:通过网络传输字节流
- 流式套接字 -> TCP;用户数据报套接字 -> UDP
- 注意事项:
- 不能在主线程访问网络
5. Binder 连接池
原因:避免随着项目变得庞大之后,无限制的根据 AIDL 增加 Service,使得增加系统资源的开销。此时将所有的AIDL 放在同一个 Service 中管理。
工作机制:每个业务模块创建自己的 AIDL 接口并实现此接口,此时不同模块间不能存在耦合,单独实现细节,然后向服务端提供自己的唯一标识和对应的 Binder 对象;对于服务端,只需要一个 Service,服务端提供一个 queryBinder 接口,该接口根据业务模块的特征返回 Binder 对象,不同的业务模块拿到所需的 Binder 对象即可调用远程方法。
由此可见:Binder 连接池的主要作用就是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行。
注意事项:
- BinderPool 采用单例实现
- BinderPool 存在断线重连机制,当远程服务意外终止时,BinderPool 会重新建立连接,此时如果业务模块的 Binder 调用出现异常,则需要手动重新获取最新的 Binder 对象
6. 合适的 IPC 方式