IPC是Inter-Proess Communication的缩写,意思是跨进程通信,即两个进程之间进行数据交换的过程。今天我们就来聊聊Android中的IPC机制。
IPC基础
多进程
进程和线程
线程是CPU调度的最小单元,是一种有限的系统资源。而进程则一般指一个执行单元,在PC和移动设备上指一个程序或应用。
一个进程可以包含多个线程,在Android中,主线程为UI线程,只有在UI线程中才能操作界面元素。
进程和线程之间的关系其实就相当于公司和部门之间的关系一样。一个进程就相当于一个公司,线程就相当于公司里的部门,各司其职,可以并行工作。最小的公司可能只有一个部门,即最基础的进程只有一个线程。至于多进程也很好理解,有些公司很庞大,下属又有好几个小公司,就像一个庞大的应用可能会有多个进程一样。
开启多进程模式的方式
常用方法为在AndroidMenifest中给四大组件指定android:process
属性。(用JNI在native层fork一个新的进程也可以实现,但不属于常规方法)
有两种效果不同的命名方法:
- 省略包名,如
android:process=":remote"
,表示进程名为com.example.myapplication:remote。属于当前应用的私有进程,其他进程的组件不能和他跑在同一进程中。 - 完整命名的进程,如
android:process="com.example.myapplication.remote"
。属于全局进程,其他应用可以通过ShareUID方式和他跑在用一个进程中。
关于UID
Android系统为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。
两个应用通过ShareUID跑在同一进程的条件:ShareUID相同且签名也相同。
满足上述条件的两个应用,无论是否跑在同一进程,它们可共享data目录,组件信息。
若跑在同一进程,它们除了可共享data目录、组件信息,还可共享内存数据。它们就像是一个应用的两个部分。
多进程产生的问题
- 静态成员和单例模式失效;
- 线程同步机制失效;
- SharePreferences的可靠性下降;
- Application会重复创建。
不同的进程就是不同的JVM虚拟机,那么就会产生问题1和问题2。由于SharePreferences底层是基于XML文件的读写实现的,那么并发读写肯定很容易出问题,即问题3。Android系统会为新的进程分配独立虚拟机,相当于系统又把这个应用重新启动了一次,于是就出现了问题4。
序列化
进程中通信会涉及到序列化的相关内容,序列化表示将一个对象转换成可存储或可传输的状态。序列化后的对象可以在网络上进行传输,也可以存储到本地。
两种类型的变量不会参与序列化:
- 静态成员变量属于类,不属于对象。
- 用transient关键字标记的成员变量。
Java原生的序列化接口为 Serializable ,Android独有的序列化接口为 Parcelable。它们之间的区别是什么呢?
Serializable的作用是为了保存对象的属性到本地文件、数据库、网络流以方便数据传输,当然这种传输可以是程序内的也可以是两个程序间的。而Android的Parcelable的设计初衷是因为Serializable效率过慢,为了在程序内不同组件间以及不同Android程序间(AIDL)高效的传输数据而设计,这些数据仅在内存中存在,Parcelable是通过IBinder通信的消息的载体。
它们的优劣如何?
Parcelable的性能比Serializable好,在内存开销方面较小,所以在内存间数据传输时推荐使用Parcelable,如activity间传输数据,而Serializable可将数据持久化方便保存,所以在需要保存或网络传输数据时选择Serializable,因为android不同版本Parcelable可能不同,所以不推荐使用Parcelable进行数据持久化。
Serializable
Serializable是Java提供的序列化接口,使用起来很简单,只需要在类的声明中指定一个serialVersionUID即可(不指定也行),代码如下:
public class Person implements Serializable {
private static final long serialVersionUID = 44260788630571004L;
public int id;
public String name;
...
}
serialVersionUID是用于在反序列化时进行校验,相当于是一个版号的作用,如果serialVersionUID不同,说明类可能被改动过了,没法反序列化。
序列化的过程都由系统自动完成了。我们只需使用ObjectOutputStream和ObjectInputStream即可完成序列化和反序列化的操作。代码如下:
public static void main(String[] args) {
Person person = new Person(1,"Jerry");
try {
//序列化
ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("cache.txt"));
outputStream.writeObject(person);
outputStream.close();
//反序列化
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("cache.txt"));
Person myPerson = (Person) inputStream.readObject();
inputStream.close();
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
Parcelable
Parcelable接口是Android根据应用场景(多进程通信)提供的一种序列化方式。只要实现这个接口,一个类的对象就可以实现序列化并通过Intent和Binder传递。
相对Serializable,Parcelable就要复杂一些,需要实现writeToParcel、describeContents函数以及静态的CREATOR变量,实际上就是将如何打包和解包的工作自己来定义,而序列化的这些操作完全由底层实现。
public class MyParcelable implements Parcelable {
private int mData;
private String mStr;
public int describeContents() {
return 0;
}
// 写数据进行保存
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mData);
out.writeString(mStr);
}
// 用来创建自定义的Parcelable的对象
public static final Parcelable.Creator CREATOR
= new Parcelable.Creator() {
public MyParcelable createFromParcel(Parcel in) {
return new MyParcelable(in);
}
public MyParcelable[] newArray(int size) {
return new MyParcelable[size];
}
};
// 读数据进行恢复
private MyParcelable(Parcel in) {
mData = in.readInt();
mStr = in.readString();
}
}
由于Parcelable针对android平台进行过优化,效率要高于Serializable,缺点就是使用起来麻烦一些。
Binder
概念
- 从API角度:是一个类,实现IBinder接口。
- 从IPC角度:是Android中的一种跨进程通信方式。
- 从Framework角度:是ServiceManager连接各种Manager和相应ManagerService的桥梁。
- 从应用层:是客户端和服务端进行通信的媒介。客户端通过它可获取服务端提供的服务或者数据。
Android是基于Linux内核基础上设计的,大家都知道,Linux内核有多种进程间通信的方式:管道/消息队列/共享内存/Socket等,为啥还要搞个Binder 出来呢?
性能方面:
Binder相对于传统的Socket方式,更加高效。Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次,共享内存方式一次内存拷贝都不需要,但实现方式又比较复杂。
安全方面:
Binder机制从协议本身就支持对通信双方做身份校检,从而大大提升了安全性。
一些理解
感觉网上好多大佬都讲过Binder机制,长篇大论,内容不可谓不细致,不过对于新手而言有点难以理解,我一开始看的时候就感觉云里雾里的。由于细致的讲解网上到处都是,我这种菜鸟也不可能讲的更好,所以下面我还是来谈谈我粗浅的认识吧。
首先是Binder机制的四个组件
Client、Server、ServiceManager以及Binder驱动。
Binder通信采用C/S架构,那么一开始的问题就是哪个是客户端哪个是服务端呢?
答案是不一定。乍一看,Client、Server一个客户端一个服务端,似乎挺合理的,但其实在Binder机制中Server有可能也是客户端,而服务端是ServiceManager。
至于Binder驱动,则可以看成是一个连接客户端和服务端的桥梁,相当于一个虚拟的硬件设备。
进程间通信其实就是交互数据对吧,那么Client就是要获取数据的进程,Server就是提供数据的进程。
我的理解是:Binder 机制就是 Client 和 Server 在 ServiceManager 进程的管理下,通过Binder驱动进行通信。
先从ServiceManager说起
ServiceManager 是系统级的服务,单独运行在一个进程中,从字面意思来看,它是用来管理 Service 的。Service 就是 Server 提供的服务。那么它是怎么管理的呢?
首先,假设如果让我们自己管理一堆服务我们会怎么办呢?
直接用服务的名字么?大部分人应该会选择给每个服务分配一个独一无二的ID吧,其实 ServiceManager 也是这么做的。
只不过ServiceManager用的是句柄。句柄是啥?英文Handle,简单点说就是一个整数值,可以看作是 service 的ID。
ServiceManager录了所有系统service所对应的Binder句柄,它的核心功能就是维护好这些句柄值。因此对于Server来说,在ServiceManager中注册就是创建自己的句柄值,而Client查询也是查询目标服务的句柄值。通过句柄值,Client 就可以获取对应的 Service 的Binder 代理,从而完成通信。
那么,之前提到 ServiceManager 也是一个单独的进程,那么其他进程是如何获取它的句柄值的呢?
ServiceManager 本身的句柄值固定为0,即handle=0。因此,无论是Client,还是 Server 都可以通过这个固定值直接获得 ServiceManager 的Binder代理,从而和ServiceManager进行通信。
另外,句柄值都是ServiceManager 维护的,不同进程中指代相同Binder实体的句柄值可能是不同的,即A进程拿到的句柄值和B进程拿到的句柄值可能不同,但指代的是同一个 Serivce。
Binder 机制的流程
- Android系统启动时会启动,ServiceManager进程。ServiceManager 注册自己的handle值为0。
- ServiceManager在死循环中,不停地去读内核中binder driver,看是否有新的请求(注册Service或查询Service)
- Server 创建一个handle为0的代理binder,向ServiceManager注册自己。
- Server 进入无限循环,不停地去读内核中binder driver的请求数据,如果是发送给自己的,解包Parcel对象,处理并将结果返回。
- Client 创建一个handle为0的代理binder,向ServiceManager 查询Service 的handle值。
- Client 通过请求到的 handle值,获得该service 的代理binder ,通过代理binder调用service的方法。