提到IPC的使用场景就必须提到多进程,如果只有一个进程,又何必使用进程间通信。多进程的情况分为两种:第一种是应用本身需要采用多进程模式实现(比如通过多个进程来获取多份内存空间);第二种是当前应用需要向其他应用获取数据,由于是两个应用,因此必须采用跨进程通信方式。
Android中的多进程模式
我们不讨论两个应用之间的多进程情况。
1、开启多进程
,Android中使用多进程有两种方法:第一种是给四大组件在AndroidManifest中指定 android:process 属性;第二种是非常规方式,通过JNI在native层去fork一个新的进程。第二种情况属于特殊情况,暂时不考虑。
2、多线程模式的运行机制
如果用一句话形容多进程,那就是:“当应用开启了多进程以后,各种奇怪的现象都出现了”,开启多线程只需要给四大组件指定 android:process 属性,但是是否正常运行就是另外一回事了。看个例子:
有 MainActivity 和 SecondActivity,其中 SecondActivity 指定运行在一个新的进程中,并且项目还新建一个 UserManager 类,类中有个public 的静态变量:
1 | public class UserManager{ |
在 MainActivity 的 onCreate 中把 sUserId 的值改为2,打印sUserId,之后再启动 SecondActivity ,在 SecondActivity 中打印 sUserId 。
可以发现在 MainActivity 中打印的值是2,在 SecondActivity 中打印的值是 1 ,看到这里,大家应该明白了多进程带来的问题,绝非只是指定一个 android:process 这么简单。
分析:我们知道,Android 系统为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机中访问同一个类的对象会产生多份副本。拿上面的例子来说,两个进程中都存在一个 UserManager 类,并且这两个类是互相不干扰的,在一个进程中修改 sUserId 的值只会影响当前进程,对其他进程不会造成任何影响。
通过以上可以知道,运行在不同进程中的四大组件,只要他们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。一般来说,使用多进程会造成如下几个方面的问题:
- 静态成员和单例模式完全失效
上面做了分析
- 线程同步机制完全失效
既然都不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步,因为不同进程锁的不是同一个对象
- SharedPreference 的可靠性下降
是因为 SharedPreference 不支持两个进程同时去执行写操作,否则会导致可能的数据丢失(因其本质是通过读写xml文件来实现的)
- Application 会多次创建
这个问题是显而易见的,由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,因此相当于系统又把应用重新启动了一遍,自然就创建了新的Application。还可以这么理解,运行在同一个进程中的组件是属于同一个虚拟机和同一个Application的;同理,运行在不同进程中的组件是属于两个不同的虚拟机和Application。
IPC 基础概念介绍
IPC 中的基础概念包括3方面内容: Serializable 接口、Parcelable接口、Binder。
1、Serializable 接口
Serializable 是Java提供的一个空的序列化接口,为对象提标准的序列化和反序列化操作。使用 Serializable 实现序列化非常简单,只需要类实现 Serializable 接口,并且在类的声明中指定一个类似下面的标识:
private static final long serialVersionUID = 12345L
实际上,这个 serialVersionUID 也不是必需的,因为serialVersionUID 的机制是这样的: 序列化时,系统会把当前类的 serialVersionUID 写入序列化的文件中;当反序列化的时候,会去检测文件中的 serialVersionUID 是否和当前类的 serialVersionUID 一致,如果一致说明序列化的类版本和当前类的版本是相同的,就可以成功反序列化;否则的话,说明当前类和序列化的类相比发生了某些变换,就无法正常反序列化。以下例子说明 Serializable 的使用:
1 | //序列化的类 |
另外,系统默认的序列化过程也是可以改变的,通过重写 writeObject 和 readObject 方法即可,只不过大部分情况下我们无需去重写这两个方法。
2、Parcelable 接口
Parcelable 也是一个接口,只要实现这个接口,类的对象就可以实现序列化并通过 Intent 和 Binder 传递。
具体使用方法可以查看官方文档
既然 Parcelable 和 Serializable 都能实现序列化并且都可用于 Intent 间的数据传递,那如何取舍呢?Serializable 是Java中的序列化接口,序列化和反序列化需要大量I/O操作;而Parcelable 是 Android 中的序列化方式,主要用在内存序列化上,使用起来稍显麻烦,但是效率高,所以这是 Android 官方推荐的序列化方式。综上所述,将对象序列化存储到设备或者通过网络传输时使用 Serializable ,否则使用 Parcelable 。
3、Binder
Binder 是 Android 中的一种 IPC 方式,还可以理解为一种虚拟的物理设备,它的设备驱动是 dev/binder。
Android 中的 IPC 方式
使用Bundle
由于Bundle 实现了 Parcelable 接口,所以它可以方便地再不同的进程间传输,基于这一点,当我们在一个进程中启动了另一个进程的 Activity、Service 和 Receiver,我们就可以在Bundle 中附加我们需要传给其他进程的信息,并通过Intent 发送出去,这是一种最简单的进程间通信方式。
使用文件共享
共享文件是一种不错的进程间通讯方式,适合在对数据同步要求不高的进程间通信。当然,SharedPreferences 是个特例,由于系统对它的读写会有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,面对高并发的的读写会有很大几率丢失数据,因此不建议在进程间通信中使用 SharedPreferences。
使用 Messenger
顾名思义可以翻译成信使,通过它可以在不同的进程中传递 Message 对象,它是轻量级的 IPC 方案,底层实现是 AIDL 。Messenger 只是一串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理。具体可以参考官方文档
使用 AIDL
由于 Messenger 服务端只能串行处理,所以可以使用 AIDL 来实现跨进程调用。具体内容可以参考官方文档
使用 ContentProvider
这是Android 中提供的专门用于不同应用建进行数据共享的方式。
使用 Socket
Socket 是网络通信中的概念,也称为“套接字”,它分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中的TCP和UDP协议。
选择合适的 IPC 方式
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件之间的通信 |
文件共享 | 简单易用 | 不适合高并发 | 无并发,数据实时性要求不高 |
AIDL | 功能强大,支持一对多,支持实时 | 使用复杂 | 一对多通信且有RPC需求 |
Messenger | 一对多串行通信,支持实时 | 高并发困难,不支持RPC,只能传输Bundle支持的数据 | 低并发的一对多通信 |
ContentProvider | 数据源访问功能强大 | 理解为受约束的AIDL,主要提供数据源的 CRUD 操作 | 一对多的进程间数据共享 |
Socket | 支持一对多并发实时通信,支持字节流 | 实现繁琐 | 网络数据交换 |