IPC机制
1. Android IPC简介
IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
线程是CPU调度的最小单元,同时线程是一种有限的系统资源。而进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含与被包含的关系。
最简单的情况下,一个进程中可以只有一个线程,即主线程。在Android里面主线程也叫UI线程,在UI线程里才能操作界面元素。
很多时候,一个进程中需要执行大量耗时的任务,如果这些任务放在主线程中去执行就会造成界面无法响应,严重影响用户体验,在Android中有一个特殊的名字叫做ANR(Application Not Responding),即应用无响应。解决这个问题就需要用到线程,把一些耗时的任务放在线程中完成。
IPC不是Android中独有的,任何一个操作系统都需要有相应的IPC机制。对于Android来说,它是一种基于Linux内核的移动操作系统,它的进程间通信并不能完全继承自Linux。在Android中最有特色的进程间通信方式是Binder,通过Binder可以轻松地实现进程间通信。除了Binder,Android还支持Socket,通过Socket也可以实现任意两个终端之间的通信。
说到IPC的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通信。
2. Android中的多进程模式
通过给四大组件指定android:process属性,我们可以轻易地开启多进程模式。
2.1 开启多进程模式
正常情况下,在Android中多进程是指一个应用中存在多个进程的情况。首先,在Android中使用多进程只有一种方法,那就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性。还有另一种非常规的多进程方法,通过JNI在native层去fork一个新的进程,这种方法是属于特殊情况,咱不讨论。
如何在Android中创建多进程:
指定该属性之后,系统会为它创建一个单独的进程。进程名为**“com.ryg.chapter_2:romote"和"com.ryg.chapter_2.remote”**,入口Activity没有为它指定process属性,那它将运行在默认进程中,默认进程的进程名为包名。
通过shell来查看进程信息:验证是开启了3个进程。
【问题】上边process属性":remote"和"com.ryg.chapter_2.remote",这两种方式有什么区别吗?
区别是有的。":“的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,其完整包名为"com.ryg.chapter_2:remote”,第二种方式指定的是完整的命名方式,不会附加包名信息。另外,以":“开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中;而进程名不以”:"开头的进程属于全局进程,其他应用通过ShareUID方式可以和它跑在同一个进程中。
Android系统会为每一个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。
2.2 多进程模式的运行机制
一般来说,使用多进程会造成如下几个方面的问题:
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharedPreferences的可靠性下降
- Application会多次创建
我们也可以这么理解同一个应用间的多进程:它就相当于两个不同的应用采用了SharedUID的模式,这样能够更加直接地理解多进程模式的本质。
为了解决这个问题,系统提供了很多跨进程通信方法,虽然说不能直接地共享内存,但是通过跨进程通信,我们还是可以实现数据交互。实现跨进程通信的方式很多,比如通过Intent来传递数据,共享文件和SharedPreference,基于Binder的Messenger和AIDL以及Socket等。
3. IPC基础概念介绍
Serializable和Parcelable接口可以完成对象的序列化过程,但我们需要通过Intent和Binder传输数据时就需要使用Parcelable或者Serializable。
3.1 Serializable接口
Serializable是Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。
使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动完成默认的序列化过程:
实际上,甚至这个serialVersionUID也不是必需的,我们不声明这个serialVersionUID同样也可以实现序列化,但是这将会对反序列化过程产生影响。这个serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同才能够正常地被反序列化。
serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候系统会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以反序列化成功;否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型可能发生了改变,这个时候是无法正常反序列化的。
一般来说,我们应该手动指定serialVersionUID的值,比如1L,也可以根据当前类的结构自动去生成它的hash值,这样序列化和反序列化时两者是serialVersionUID是相同的,因此可以正常进行反序列化。
以下两点需要特别提一下,首先静态变量成员属于类不属于对象,所以不会参与序列化过程;其次用transient关键字标记的成员变量不参与序列化过程。
通过Serializable方式来实现对象的序列化,实现起来非常简单,几乎所有工作都被系统自动完成了。只需要采用ObjectOutputStream和ObjectInputStream即可轻松完成。
当然,系统的默认序列化过程也是可以改变的,通过重写writeObject和readObject即可。
3.2 Parcelable接口
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。
Parcelable的方法说明:
方法 | 功能 |
---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 |
newArray(int size) | 创建指定长度的原始对象数组 |
User(Parcel in) | 从序列化后的对象中创建原始对象 |
writeToParcel(Parcel out, int flags) | 将当前对象写入序列化结构中,其中flags标识有两种值:0或者1,为1时标识当前对象需要作为返回值返回,不能立即释放资源,几乎所有情况都为0 |
describeContents | 返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0 |
系统已经为我们提供了许多实现了Parcelable接口的类,他们都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是他们里边的每个元素都是可序列化的。
Serializable是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作。而Parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微麻烦点,但是它的效率很高,这是Android推荐的序列化方式,因此我们要搜选Parcelable。
Parcelable主要用在内存序列化上,Serializable用于将对象序列化到存储设备中或者将对象序列化后通过网络传输。
3.3 Binder
直接来说,Binder是Android中的一个类,它实现了IBinder接口。从IPC角度,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager等等)和响应ManagerService的桥梁;从Android应用层来说,Binder是客户端和服务端进行通信的媒介。
Android开发中,Binder主要用在Service中,包括AIDL和Messenger,其中普通Service中的Binder不涉及进程间通信,而Messenger的底层其实是AIDL,所以选择用AIDL来分析Binder的工作机制。
方法 | 描述 |
---|---|
DESCRIPTOR | Binder的唯一标识,一般用当前Binder的类名表示 |
asInterface(android.os.IBinder obj) | 用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象 |
asBinder | 用于返回当前Binder对象 |
onTransact | 运行在服务端中的Binder线程池中 |
Binder工作机制:
我们在实际开发中完全可以通过AIDL文件让系统去自动生成,手动去写的意义在于可以让我们更加理解Binder的工作原理,同时也提供了一种不通过AIDL文件来实现Binder的新方式。
为了解决Binder死亡问题,Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以重新发起连接请求从而恢复连接。
4. Android中的IPC方式
4.1 使用Bundle
四大组件中的三大组件(Activity、Service、Receiver)都是支持在Intent中传递Bundle数据的。由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。
4.2 使用文件共享
共享文件也是一种不错的进程间通信方式,两个进程通过读/写同一个文件来交换数据。通过文件共享这种方式来分项数据对文件格式是没有具体要求的。
文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写问题。
SharedPreference是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,底层实现上它采用XML文件来存储键值对。但是由于系统对它的读/写有一定的缓存策略,不建议在进程间通信中使用SharedPreference。
4.3 使用Messenger
Messenger可以翻译为信使,通过它可以在不同进程中传递Message对象,在Message中放入我们需要传递的数据,就可以轻松地实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。
4.4 使用AIDL
AIDL文件支持的数据类型有:基本数据类型、String和CharSequence、List、Map、Parcelable、AIDL。
4.5 使用ContentProvider
ContentProvider是Android中提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和Messenger一样,ContentProvider的底层实现同样也是Binder。
4.6 使用Socket
Socket也成为“套接字”,是网络通信中的概念,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。
5. Binder连接池
6. 选用合适的IPC方式
IPC方式的优缺点和适用场景: