第二章 IPC机制
学习清单:
Android中的多进程模式
-
IPC基础概念
-
序列化
Serializable接口
Parcelable接口
Binder
-
-
Android中的IPC方式
Bundle
文件共享
Messenger
AIDL
ContentProvider
Socket
Binder连接池
如何选用合适的IPC方式
一.Android中的多进程模式
在谈IPC之前, 我们首先需要理解Android中的多进程模式.
a. 什么是线程? 什么是进程?
线程是CPU调度的最小单元
一个进程可以有多个线程
一个进程至少要有一个线程, 即主线程, 在Android中也叫做UI线程
注意: 如果一个进程中需要执行一个耗时的操作, 将其放在主线程执行, 则会造成应用无响应, 也就是ANR(Application Not Responding), 如果要解决这个问题, 就需要用多线程, 将耗时操作交给别的线程处理
b. 多进程有什么用?
某些模块因特殊原因要运行在单独进程中
为加大一个应用可使用的内存,需通过多进程来获取多份内存空间
1. 开启多进程模式:
- 在Android中使用多进程只有一种方法, 那就是给四大组件(Activity | Service | Receiver | ContentProvider) 在AndroidMenifest中指定
android:process
属性, 没有指定process
属性时, 默认进程的进程名是包名.
补充: 在为
android:process
属性命名时, 有两种方式:
android:process=":remote"
: 以 " : " 开头是在当前进程名前附加上包名的简写方式, 以这种方式开头的进程属于私有进程, 其他应用不可以和它跑在同一个进程中
android:process="com.example.main.remote"
: 完整的命名方式, 该方式属于全局进程, 其他应用通过ShareUID方式可以和它跑在同一个线程
2. 开启多进程模式所带来的问题:
① 静态成员和单例模式完全失效
- Android系统为每一个进程都分配了一个独立的虚拟机, 导致在不同的虚拟机中访问同一个类的对象会产生多个副本
② 线程同步机制完全失效
- 原因同上
③ SharedPreferences的可靠性下降
- SharedPreferences不支持两个进程同时进行读写操作,即不支持并发读写,有一定几率导致数据丢失
④ Application会多次创建
- Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次
二.IPC基础概念
a. IPC是什么?
IPC(Inter-Process Communication,跨进程通信):指两个进程之间进行数据交换的过程
任何一个操作系统都有对应的IPC机制
b. IPC的使用场景:
- 当某个进程需要向别的进程获取数据时
c. Android的进程架构:
- 每一个Android进程都是独立的,且都由两部分组成,一部分是用户空间,另一部分是内核空间,如下图:
1. 序列化:
a. 序列化的介绍:
- 含义: 序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地
- 场景: 需要通过Intent和Binder等传输类对象就必须完成对象的序列化过程
- 两种方式:实现Serializable/Parcelable接口
b.Serializable接口:
实现方式:
实现Serializable接口
为该类指定SerialVersionUID (可选)
注意: 不指定UID可以实现序列化, 但是会影响反序列化. 所以我们应该手动去设定UID的值:
private static final long serialVersionUID = 1L;
c. Parcelable接口:
实现方式:
实现Parcelable接口
- 实现接口中的各种方法, 各方法功能如下:
b.Serializable接口和Parcelable接口的比较:
2. Binder:
a.概念:
从API的角度: 是一个类, 实现Binder接口
从IPC的角度: 是Android中的一种跨进程通信方式
从Framework角度: 是ServiceManager连结各种Manager和相应ManagerService的桥梁
从应用层的角度: 是客户端和服务端进行通信的媒介, 客户端通过连接他来获取服务端提供的服务或者数据
b.Android是基于Linux内核基础上设计的, 却没有把管道/消息队列/共享内存/信号量/Socket等一些IPC通信手段作为Android的主要IPC方式, 而是采用了Binder机制, 其优点有:
- 传输效率高、可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较:
方式 | 拷贝次数 | 操作难度 |
---|---|---|
Binder | 1 | 简易 |
消息队列 | 2 | 简易 |
Socket | 2 | 简易 |
管道 | 2 | 简易 |
共享内存 | 0 | 复杂 |
- 数据从发送方的缓存区拷贝到了内核和缓存区, 而接收方的缓存区与内核的缓存区映射的是同一个物理地址, 节省了一次数据拷贝时间, 如图:
实现C/S架构方便:Linux的众IPC方式除了Socket以外都不是基于C/S架构,而Socket主要用于网络间的通信且传输效率较低。Binder基于C/S 架构 ,Server端与Client端相对独立,稳定性较好。
安全性高:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Binder机制为每个进程分配了UID/PID且在Binder通信时会根据UID/PID进行有效性检测。
c.Binder框架定义了四个角色: Server, Client, ServiceManager和Binder驱动
其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间, 如图:
下面简单介绍这四个角色:
- ServiceManager: 服务的管理者, 将Binder名字转换为Client中对改Binder的引用, 使得Client可以通过Binder名字获得Service中Binder实体的引用, 如图:
-
Binder驱动:
与硬件设备没有直接关系, 其工作方式与设备驱动程序是一样的, 工作于内核态
提供open(), mmap(), poll(), ioctl()等标准文件操作
以字符驱动设备中的misc设备注册在设备目录/dev下, 用户通过/dev/binder访问它
负责进程间binder通信的建立, 传递, 计数管理及数据的传递交互等底层支持
驱动和应用程序之间定义了一套接口协议, 主要功能有ioctl()接口实现, 由于ioctl()灵活方便且能一次调用实现先写后读以满足同步交互, 因此不必分别调用write()和read()接口
其代码位于linux目录的driver/misc/binder.c中
-
Service&Client:
服务器&客户端. 在Binder驱动和Service Manager提供的基础设施上, 进行Client-Server之间的通信
d.代理模式Proxy: 给某个对象一个代理对象, 并由代理对象控制对原对象的访问, 如图:
代理模式的组成:
Abstract Subject (抽象主题) : 声明Real Subject和Proxy的共同接口,这样在任何可以使用Real Subject的地方都可以使用Proxy
Real Subject (真实主题) : 定义了proxy所代表的Real subject
-
Proxy Subject (代理主题) :
内部含有一个Real Subject的引用, 可在任何时候操作目标对象
提供一个代替Real Subject相同的接口, 可在任何时候替代目标对象
e. Binder 工作模式:
服务器端:在服务端创建好了一个Binder对象后,内部就会开启一个线程用于接收Binder驱动发送的消息,收到消息后会执行onTransact(),并按照参数执行不同的服务端代码
Binder驱动:在服务端成功Binder对象后,Binder驱动也会创建一个mRemote对象(也是Binder类),客户端可借助它调用transact()即可向服务端发送消息
客户端:客户端要想访问Binder的远程服务,就必须获取远程服务的Binder对象在Binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binder对象的暴露给客户端的方法
3. IPC方式
由上图可以发现, 这些IPC方式全都是通过Binder实现的, 只是封装的方法不同, 接下来我们分别介绍这六种IPC方式
1.使用Bundle
a. Bundle: 支持在Activity, Service和Receiver之间通过Intent.putExtra()传递bundle数据
Intent intent = new Intent();
Bundle bundle = new Bundle();
bundle.putString("xxx","xxx");
intent.putExtra("data", bundle);
b. 原理: Bundle实现了Parcelable接口, 它可以方便的在不同的进程中传输
c. 注意: Bundle不支持的数据类型无法在进程间传输
2.使用文件共享
a. 文件共享: 两个进程通过读/写同一个文件来交换数据。比如A进程把数据写入文件,B进程通过读取这个文件来获取数据
b. 适用情况:对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题
c. 虽然SharedPreferences也是文件存储的一种,但不建议采用
- 原因: 系统对SharedPreferences的读/写有一定的缓存策略,即在内存中有一份该文件的缓存,因此在多进程模式下,其读/写会变得不可靠,甚至丢失数据
3.使用Messenger
a. Messenger: 信使, 轻量级IPC方式, 通过它在不同进程间传递Message对象, 而在Message中放我们要传递的数据, 从而实现IPC
相关记忆:
- Handler: 主要用于线程之间的数据通信
- Messenger: 进程间的数据通信
b. 特点:
底层实现的是AIDL, 即对AIDL进行了封装, 更便于进行进程间通信
其服务端以串行的方式来处理客户端的请求, 不存在并发执行的情形, 故无需考虑线程同步的问题
可在不同进程中传递Message对象, Messenger可支持的数据类型即Message可支持的数据类型
- arg1, arg2, what字段: int
- obj字段: Object对象, 支持系统提供的Parcelable对象
- setData: Bundle对象
- 有两个构造函数, 分别接收Handler对象和Binder对象
c. 实现方法
-
服务端:
创建一个Service来处理客户端连接请求
创建一个Handler来获取Messenger对象
在Service的
onBind()
中返回这个Messenger对象底层的Binder
-
客户端:
绑定服务端的Service
通过绑定成功后返回的IBinder对象创建一个Messenger对象
如果需要客户端能够回应服务端:
- 创建一个Handler和一个新的Messenger
- 将Messenger这个对象通过Message的replyTo参数传递给服务端
- 服务端通过replyTo参数就能够回应客户端
d. Messenger的缺点:
主要传递的是Message, 难以实现远程方法调用
以串行的方式处理客户端发来的消息, 不适合高并发场景
解决方法: 使用AIDL来实现IPC
4.使用AIDL
a.AIDL(Android Interface Definition Language,Android接口定义语言): 可以利用它定义客户端与服务均认可的编程接口, 以便二者使用进程间通信 (IPC) 进行相互通信, AIDL会生成一个服务端的代理类, 通过它客户端实现间接调用服务端的方法
b. 支持的数据类型:
基本数据类型(int, long, char, boolean, double等)
String和CharSequence
List: 只支持ArrayList, 里面每个元素都必须被AIDL支持
Map: 只支持HashMap, 里面每个元素都必须被AIDL支持, 包括key和value
Paecelable: 所有实现了Parcelable接口的对象
AIDl: 所有的AIDL接口本身也可以在AIDL文件中使用
注意: 除了基本数据类型, 其他类型的参数必须标上方向: in, out, inout, 用于表示数据的流向
- in
* 表示数据只能由客户端流向服务端 * 服务端将会接收到这个对象的完整数据, 但在服务端修改它不会对客户端输入的对象产生影响
- out
* 表示数据只能由服务端流向客户端 * 服务端将会接收到这个对象的的空对象, 但在服务端对接收到的空对象有任何修改之后客户端将会同步变动
- inout
* 表示数据可在服务端与客户端之间双向流通 * 服务端将会接收到客户端传来对象的完整信息, 且客户端将会同步服务端对该对象的任何变动
c. 两种AIDL文件
用于定义parcelable对象, 以供其他AIDL文件使用AIDL中非默认支持的数据类型的
用于定义方法接口, 以供系统使用来完成跨进程通信的
注意:
- 自定义的Parcelable对象必须把java文件和自定义的AIDL文件显式的import进来, 无论是否在同一包内
- AIDL文件用到自定义Parcelable的对象, 必须新建一个和它同名的AIDL文件, 并在其中声明它为Parcelable类型
d. AIDL本质上是系统提供了一套可快速实现Binder的工具. 关键类和方法:
AIDL接口: 继承IInterface
Stub类: Binder的实体类, 服务端通过这个类来提供服务
Proxy类: 服务器的本地代理, 客户端通过这个类来获取服务
-
asInterface(): 客户端调用, 将服务端返回的Binder对象, 转换成客户端所需的AIDL接口类型对象. 返回对象:
若客户端和服务端位于同一进程, 则直接返回Stub对象本身
否则, 返回系统封装后的Stub.proxy对象
asBinder(): 根据当前调用情况返回代理Proxy的Binder对象
onTransact(): 运行服务端的Binder线程池中, 当客户端发起跨进程请求时, 远程请求会通过系统底层封装后交由此方法处理
transact(): 运行在客户端, 当客户端发送远程请求的同时将当前线程挂起. 之后调用服务端的onTransact()直到远程请求返回, 当前线程才继续执行
e. 实现方法
-
服务端:
创建一个AIDL文件
创建一个Service, 实现AIDL的接口函数并暴露AIDL接口
-
客户端:
通过bindService绑定服务端的Service
绑定成功后, 将服务端返回的Binder对象转化为AIDL接口所属的类型, 进而调用AIDL中提供的方法
总结: 服务端里的某个Service为和它绑定的特定客户端提供Binder对象, 客户端通过AIDL的静态接口asInterface()将Binder对象转化为AIDL接口的代理对象, 通过这个代理我们就可以发送远程调用请求
f. 可能产生ANR的情形
-
对于客户端
调用服务端的方法是运行在服务端的Binder线程池中, 若所调用的方法里执行了较耗时的任务, 同时会导致客户端线程长时间阻塞, 易导致客户端ANR
在onServiceConnected()和onServiceDisconnected()里直接调用服务端的耗时方法, 易导致客户端ANR
-
对于服务端
服务端的方法本身就运行在服务端的Binder线程中, 可在其中执行耗时操作, 而无需再开启子线程
回调客户端Listener的方法是运行在客户端的Binder线程中, 若所调用的方法里执行了较耗时的任务, 易导致服务端ANR
提示: 解决客户端频繁调用服务器方法导致性能极大损耗的办法 : 实现观察者模式. 即当客户端关注的数据发生变化时, 再让服务端通知客户端去做相应的业务处理
g. AIDL解注册失败
原因:Binder进行对象传输实际是通过序列化和反序列化进行,即Binder会把客户端传递过来的对象重新转化并生成一个新的对象,虽然在注册和解注册的过程中使用的是同一个客户端对象,但经过Binder传到服务端后会生成两个不同的对象。另外,多次跨进程传输的同一个客户端对象会在服务端生成不同的对象,但它们在底层的Binder对象是相同的
解决办法:当客户端解注册的时候,遍历服务端所有的Listener,找到和解注册Listener具有相同的Binder对象的服务端Listener,删掉即可
需要用到RemoteCallBackList:Android系统专门提供的用于删除跨进程listener的接口。其内部自动实现了线程同步的功能
5.使用ContentProvider
a. ContentProvider: Android中提供的专门用于不同应用间进行数据共享的方式,底层实现使用Binder
b. 特点:
相比于AIDL,使用更加简单,无需关心底层细节
系统预置了许多ContentProvider如:通讯录信息、日程表信息等
ContentProvider主要以表格的形式来组织数据,使用方法与数据库很类似
ContentProvider还支持文件数据,如图片、视频等
c. 使用方法:
-
服务端:
-
新建一个类继承自ContentProvider, 并实现六个抽象方法:
onCreate(): ContentProvider的创建, 一般用于一些初始化动作
query(): 对数据表进行查询操作
insert(): 对数据表进行插入操作
delete(): 对数据表进行删除操作
update(): 对数据表进行更新操作
getType(): 返回一个Uri请求所对应的MIME(媒体)类型, 如图片、视频
注意:
- update, insert和delete均运行在Binder线程池中, 而onCreate运行在主线程(UI线程), 即不能在onCreate中执行耗时操作
- 一个SQLiteDatabase内部对数据库的操作有同步处理, 但多个SQLiteDatabase之间无法同步
注册ContentProvider
-
- 客户端: 通过getContentResolver()获取到ContentResolver对象, 并通过此对象执行相应的操作
6.使用Socket
a. Socket: 套接字, 是网络通信中的概念, 分为: 流式套接字和用户数据报套接字, 分别对应TCP和UDP
网络传输协议:
TCP: 面向连接的协议, 提供稳定的双向通信功能, 具有很高的稳定性
UDP: 无连接, 提供不稳定的单向通道, 也可实现双向功能, 效率更高, 但不能保证数据安全送达
b. 注意:
不能在主线程(UI线程)中访问网络
使用Socket进行通信, 需要声明权限
c. 使用方法:
-
服务端:
创建一个Service, 在线程中建立端口, 等待客户端连接请求
在与客户端连接后, 会生成一个新的Socket, 通过它可以和客户端进行数据传输
在于客户端断开连接后, 关闭相应的Socket并结束线程
-
客户端:
开启一个线程, 通过Socket发出连接请求
连接成功后, 通过这个Socket读取服务端消息
断开连接, 关闭Socket
以上六种IPC方式的优缺点及使用场景如下图:
4.Binder连接池
背景: 当多个业务模块都需要使用到AIDL来进行IPC时, 则需要为每一个模块创建对应的aidl文件, 与之对应的则要创建许多的Service服务. 这样会极大的占用我们有限的系统资源
-
功能: 将每一个模块的Binder请求都统一转发到一个远程Service中去执行, 避免重复创建新的Service
- 原理: 每个业务模块有着自己的AIDL接口并实现, 然后向服务端提供自己的唯一标识和Binder对象. 服务端只需要一个Service并提供一个queryBinder接口, 它将根据业务模块的标识来返回相应的Binder对象, 不同的业务模块拿到自己的所需的Binder对象就可以进行对应的远程操作
-
实现:
-
AIDL接口
实现相关模块的AIDL接口
-
创建一个IBinderPool.aidl文件, 获取相关模块所需的AIDL接口
interface IBinderPool { IBinder queryBinder(int binderCode); }
创建一个BinderPool文件, 实现IBinderPool接口
public static class BinderPoolImpl extends IBinderPool.Stub { public BinderPoolImpl() { super(); } @Override public IBinder queryBinder(int binderCode) throws RemoteException { IBinder binder = null; switch (binderCode) { case BINDER_SECURITY_CENTER: { binder = new SecurityCenterImpl(); break; } case BINDER_COMPUTE: { binder = new ComputerImpl(); break; } default: break; } return binder; } }
- Binder连接池的具体实现, 来绑定远程服务
服务端: 远程服务BinderPoolService实现, 在onBind()处返回实例化的IBinderPool实体类对象
-
客户端:
通过BinderPool类中的getInstance(Context)获取BinderPoll类实例
通过BinderPool类实例里实现的queryBinder(int)方法获取所需要的Binder对象
-
总结
IPC是指两个进程之间进行数据交换的过程
Android中的IPC方式底层都是由Binder实现的, 由此可见Binder在Android中的重要性
要根据不同的需求, 使用不同的IPC方式, 因地制宜
要多用, 不然会忘