1.Android IPC介绍
IPC是Inter-Process Communication的缩写,含义为进程间的通信或者跨进程通信,指的是两个进程之间进行数据交换的过程。首先,我们要区分一下"进程"和"线程"。按照操作系统的描述,线程是CPU调度的最小单元,同时线程是一中有限的资源。进程一般指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此,进程和线程是包含与被包含的关系。最简情况下,一个进程中只有一个线程通常我们称为主线程。
IPC不是Android所独有,任何一个操作系统都需要有相应的IPC 机制,比如Windows上可以通过剪切板、管道、邮槽等进行进程间的通信。在Android中,最有特色的进程间通信方式就是Binder了,通过Binder可以轻松实现任意两个终端的通信,当然,同一个设备上的两个进程通过Socket进行通信自然也是可以的。
提到IPC就要提到多进程,只有面对多进程这样的场景下,才需要考虑进程间通信。
2.Android中的多进程模式
在正式介绍进程通信之前,我们必须先理解Android中的多进程模式。通过给四大组件指定android:process属性,我们可以轻易地开启多进程模式。这么做看起来很简单,但是实际使用过程中却暗藏杀机,多进程远远没有我们开始想象的那么简单,有时候多进程带来的负面影响甚至大于带来的好处。
2.1开启多进程模式
一般情况下,在Android中多进程是指一个应该存在多个进程的情况,因此这里暂时不讨论两个应用之间的多进程情况。首先,在Android中使用多进程只有一种发放,那就是给四大组件(Activity、Service,Receiver、ContentProvider)在AndroidMenifest中指定android:process属性,除此之外没有其他方法,也就说我们无法给一个线程或者一个实体类指定其运行的进程。非常规的方法是通过JNI在native层去fork一个新的进程,但是这种方法属于特殊情况,非一般创建多进程的方式。
2.1开启多进程模式
一般情况下,在Android中的多进程是指一个应用中存在多个进程的情况。
在Android中开启多进程通常只有一种方法,就是给四大组件(Activity、Service、Receiver、ContentProvider)在AndroidMenifest中指定android:process属性。(特殊情况下可以用JNI的方式fork一个进程出来)
2.2 多进程模式的运行机制
看了多进程的开启方式,或许你会觉得Android多进程也很简单嘛。只设置多进程的确很简单,但是用起来就不那么简单了。当应用开启的多进程之后,各种奇怪的问题就都出来了。
1、共有变量的值是单独的一套,所以在
我们知道Android系统为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。
所有运行在不同进程中的四大组件,只要他们之间需要用内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件中间都需要一个中间层来共享数据。
一般来说,使用多进程会造成一下几个方面的问题:
1.静态成员和单例模式完全失效
2.线程同步机制完全失效
3.SharedPreferences的可靠性下降
4.Application会多次创建
以上问题的核心在于多进程模式中,不同进程的组件拥有独立的虚拟机,Application以及内存空间。
为了解决这个问题,系统提供了很多跨进程通信的方法,虽然不能直接共享内存,但是通过跨进程通信,我们还是可以实现数据的交互。跨进程通信的方式有很多,比如通过Intent来传递数据,共享文件和SharedPreferences,基于Binder的Messenger和AIDL以及Socket等。
3.IPC基础概念介绍
为了更好的理解跨进程通信,就不得不提到Serializable接口、Parcelable接口和Binder。
3.1 Serializable接口
Serializable是Java提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable接口类实现序列化很简单,只要我们在类定义的时候实现这个接口就可以,类似
public class User implements Serializable{
}
答案是只是这么写有时候会出现问题,还要定义这样一个变量(具体原因后面说)
private static final long serialVersionUID=23423524524L;
通过Serializable方式可以轻松实现对象的序列化,几乎所有的工作都被系统自动完成了。如何进行对象的序列化和反序列化呢?使用ObjectOutputStream和ObjectInputStream即可轻松实现。如下
//序列化过程
User user = new User(0,"jake",true);
ObjectOutputStream out = new ObjectOutputStream(
new FileOutputStream("cache.txt"));
out.writeObject(user);
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt")));
User newUser=(User) in.readObject();
in.close();
上述代码演示了采用Serializable方式序列化对象的典型过程,是不是很简单?只需要把实现了Serializable接口的User对象写到文件里就可以快速恢复了,恢复后的对象newUser和user的内容完全一样,但是两者不是同一个对象。
上面说实现了Serialzable接口之后,不指定serialVersionUID大部分情况下仍然可以运行,但是有时候会出现问题,具体是什么呢?
我们要明白,系统既然提供了SerialVersionUID,那么它一定是有用的。事实上,这个serialVersionUID是来辅助序列化和反序列化的,原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同的时候才能正常被反序列化。
serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),当反序列化的时候,系统会去比对文件中的serialVersionUID和类中的serialVersionUID是否一致。如果一致,就说明序列化的类的版本和当前类的版本是相同的,这时候可以成功反序列化;反之,说明当前类和序列化的类相比发生了变换,此时无法正常反序列化,会报如下错误:
java.io.InvalidClassException:Main;local calss incompatible:stream classdesc serialVersionUID=871136882083044,loacl class serialVersionUID=8711368010083043.
一般来说,我们应该手动指定serialVersionUID的值,比如1L,也可以让Android Studio根据当前类的结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是相同的,因此可以正常进行反序列化。如果不手动指定serialVersionUID的值,反序列化时有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类的hash值,并把它赋值给serialVersionUID,这时当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。所以我们可以明显感觉到serialVersionUID的作用,当我们手动指定了它以后,就可以在很大程度上避免反序列化失败。
需要说明的是:
1.静态成员变量属于类不属于对象,所以不会参加序列化过程
2.用transient关键字标记的成员变量不参与序列化过程
系统默认的序列化过程是可以改变的,通过实现如下两个方法即可重写系统默认的序列化和反序列化过程。
private void writeObject(java.io.ObjectOutputStream out) throws IOException{
//write "this" to "out"
}
private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{
}
3.2 Parcelable接口
Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。Parcel内部包装了可序列化的数据,可以在Binder中自由传输。从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParel方法来完成。其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化过程;内容描述功能由describeContents方法来完成,几乎在所有情况下这个方法都返回0,仅当当前对象存在文件描述时,此方法返回1。
系统已经为我们提供了许多实现了Parcelable接口的类,它们都是可以直接序列化的,比如Intent、Bundle、Bitmap等,同时List和Map也可以序列化,前提是它们里面的每一个元素都是可序列化的。
Serializable和Parcelable接口改如何选取呢?
Serializable接口是Java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量I/O操作。而parcelable是Android中的序列化方式,因此更适合用在Android平台上,它的缺点是使用起来稍微有点麻烦,但是效率高,也是Android推荐的序列化的方式。因此首选Parcelable。
Parcelable主要用在内存序列化上;如果把对象序列化到存储设备上或者将对象序列化后通过网络传输,建议用Serializable。
3.3 Binder
Binder是一个很深的话题,网上有很多人写的关于Binder的文章,但是少有人解释清楚。这里整理一下Binder的使用和上层原理.
直观地说,Binder是Android中的一个类,它实现了IBinder接口,从IPC角度来说Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有;从Android Framework角度,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,等等)和对应ManagerService的桥梁;
Android开发中,Binder主要用在Service中,包括AIDL和Messager,其中普通Service中的Binder不涉及进程间通信,所以较为简单,无法触及Binder的核心,而Messager的底层其实是AIDL,
4.Android中的IPC方式
前面说了IPC的基础知识:序列化和Binder,下面开始分析一下各种跨进程通信的方式。具体方式有很多,如:
1.通过在Intent中附加extras来传递信息
2.通过共享文件的方式来共享数据
3.采用Binder方式来跨进程通信
4.ContentProvider支持跨进程访问
5.通过网络通信实现数据传递,Socket实现IPC
等等
4.1使用Bundle
我们知道,四大组件中的三大组件(Activity、Service、Receiver)都支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便地在不同的进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,我们就可以在Bundle中附加我们需要传输给远程进程的信息并通过Intent发送出去。当然,我们传输的数据必须能够被序列化,比如基本数据类型、实现了Serializable接口的对象、实现了Parcellable接口的对象以及其他Android支持的特殊对象。
4.2 使用文件共享
共享文件也是一种不错的进程间通信的方式,两个进程通过读/写同一个文件来交换数据,比如A进程把数据写入文件,B进程通过读取这个文件来获取数据。我们知道在windows系统上,一个文件如果被加入排斥锁会导致其他线程无法对其进行访问,包括读写。Android系统是基于Linux的,使得并发读写并没有什么限制,甚至两个线程同事对一个文件进行写操作都是允许的,尽管这样可能出现问题。
当然SharedPreferences是个特例,众所周知,SharedPreferences是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,在底层实现上采用XML文件来存储键值对,每个应用的SharedPreferences文件都可以在当前包所在的data目录下查看到。一般来说,它的目录位于/data/data/package name/shared_prefs目录下,其中那个package name表示当前应用的包名。从本质来说,SharedPreferences也属于文件的一种,但是系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发的读写访问,Sharedpreferences有很大几率会丢失数据,因此不建议在进程间通信中使用SharedPreferences。