2 IPC机制
2.1 Android IPC 简介
- IPC即Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
- 线程是CPU调度的最小单元,是一种有限的系统资源。进程一般指一个执行单元,在PC和移动设备上是指一个程序或者应用。进程与线程是包含与被包含的关系。一个进程可以包含多个线程。最简单的情况下一个进程只有一个线程,即主线程(例如Android的UI线程)。
- 任何操作系统都需要有相应的IPC机制。
- 在Android中,IPC的使用场景大概有以下:
- 有些模块由于特殊原因需要运行在单独的进程中。
- 通过多进程来获取多份内存空间。
- 当前应用需要向其他应用获取数据。
2.2 Android中的多进程模式
2.2.1 开启多进程模式
给四大组件在Manifest中指定android:process
属性。这个属性的值就是进程名。
tips:使用
adb shell ps
或adb shell ps|grep 包名
查看当前所存在的进程信息。
2.2.2 多线程模式的运行机制
Android为每个进程都分配了一个独立的虚拟机,不同虚拟机在内存分配上有不同的地址空间,导致不同的虚拟机访问同一个类的对象会产生多份副本。例如不同进程的Activity对静态变量的修改,对其他进程不会造成任何影响。
所有运行在不同进程的四大组件,只要它们之间需要通过内存在共享数据,都会共享失败。四大组件之间不可能不通过中间层来共享数据。
多进程会带来以下问题:
- 静态成员和单例模式完全失效。
- 线程同步锁机制完全失效。
这两点都是因为不同进程不在同一个内存空间下,锁的对象也不是同一个对象。 - SharedPreferences的可靠性下降。
SharedPreferences底层是 通过读/写XML文件实现的,并发读/写会导致一定几率的数据丢失。 - Application会多次创建。
由于系统创建新的进程的同时分配独立虚拟机,其实这就是启动一个应用的过程。
在多进程模式中,不同进程的组件拥有独立的虚拟机、Application以及内存空间。
实现跨进程的方式有很多:
- Intent传递数据。
- 共享文件和SharedPreferences。
- 基于Binder的Messenger和AIDL。
- Socket。
2.3 IPC基础概念介绍
主要介绍Serializable
、Parcelable
、Binder
。
2.3.1 Serializable接口
Serializable
是Java提供的一个序列化接口(空接口),为对象提供标准的序列化和反序列化操作。- 只需要一个类去实现
Serializable
接口并声明一个serialVersionUID
即可实现序列化。 - 如果不手动指定
serialVersionUID
的值,反序列化时当前类有所改变(比如增删了某些成员变量),那么系统就会重新计算当前类的hash值并赋值给serialVersionUID
。这个时候当前类的serialVersionUID
就和序列化数据中的serialVersionUID
不一致,导致反序列化失败,程序就出现crash。 - 静态成员变量属于类不属于对象,不参与序列化过程,其次
transient
关键字标记的成员变量不参与序列化过程。
2.3.2 Parcelable接口
Parcelable
内部包装了可序列化的数据。序列化功能由
writeToParcel
方法完成,最终是通过Parcel
的一系列writer方法来完成。@Override public void writeToParcel(Parcel out, int flags) { out.writeInt(code); out.writeString(name); }
反序列化功能由
CREATOR
来完成,其内部表明了如何创建序列化对象和数组,通过Parcel
的一系列read方法来完成。public static final Creator<Book> CREATOR = new Creator<Book>() { @Override public Book createFromParcel(Parcel in) { return new Book(in); } @Override public Book[] newArray(int size) { return new Book[size]; } }; protected Book(Parcel in) { code = in.readInt(); name = in.readString(); }
内容描述功能由
describeContents
方法完成,几乎所有情况下都应该返回0,仅当当前对象中存在文件描述符时返回1。public int describeContents() { return 0; }
Serializable
是Java的序列化接口,使用简单但开销大,序列化和反序列化过程需要大量I/O操作。而Parcelable
是Android中的序列化方式,适合在Android平台使用,效率高但是使用麻烦。Parcelable
主要在内存序列化上,Parcelable
也可以将对象序列化到存储设备中或者将对象序列化后通过网络传输,但是稍显复杂,推荐使用Serializable
。
2.3.3 Binder
- Binder是Android中的一个类,实现了
IBinder
接口。从IPC角度说,Binder是Andoird的一种跨进程通讯方式。从Android Framework角度来说,Binder是ServiceManager
连接各种Manager(ActivityManager·
、WindowManager
)和相应ManagerService
的桥梁。从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService时,服务端返回一个包含服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务器端提供的服务或者数据(包括普通服务和基于AIDL的服务)。 - Android中Binder主要用于
Service
,包括AIDL和Messenger。普通Service的Binder不涉及进程间通信,Messenger的底层其实是AIDL,所以下面通过AIDL分析Binder的工作机制。
2.3.3.1 由系统根据AIDL文件自动生成.java文件
- Book.java
表示图书信息的实体类,实现了Parcelable接口。 Book.aidl
Book类在AIDL中的声明。package com.ryg.chapter_2.aidl; parcelable Book;
- IBookManager.aidl
定义的管理Book实体的一个接口,包含getBookList
和addBook
两个方法。
系统为IBookManager.aidl生产的Binder类,在gen
目录下的IBookManager.java类。
IBookManager继承了IInterface
接口,所有在Binder中传输的接口都需要继承IInterface接口。结构如下:
- 声明了
getBookList
和addBook
方法,还声明了两个整型id分别标识这两个方法,用于标识在transact
过程中客户端请求的到底是哪个方法。 - 声明了一个内部类
Stub
,这个Stub
就是一个Binder类,当客户端和服务端位于同一进程时,方法调用不会走跨进程的transact
。当二者位于不同进程时,方法调用需要走transact
过程,这个逻辑有Stub
的内部代理类Proxy
来完成。 - 这个接口的核心实现就是它的内部类
Stub
和Stub
的内部代理类Proxy
。
2.3.3.2 Stub和Proxy类的内部方法和定义
- DESCRIPTOR
Binder的唯一标识,一般用Binder的类名表示。 - asInterface(android.os.IBinder obj)
将服务端的Binder对象转换为客户端所需的AIDL接口类型的对象,如果C/S位于同一进程,此方法返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy对象。 - asBinder
返回当前Binder对象。 - onTransact
这个方法运行在服务端的Binder线程池中,由客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。该方法的原型是java public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags)
- 服务端通过code确定客户端请求的目标方法是什么,
- 接着从data取出目标方法所需的参数,然后执行目标方法。
- 执行完毕后向reply写入返回值(如果有返回值)。
- 如果这个方法返回值为false,那么服务端的请求会失败,利用这个特性我们可以来做权限验证。
- Proxy#getBookList 和Proxy#addBook
- 这个方法运行在客户端,首先该方法所需要的输入型对象Parcel对象_data,输出型Parcel对象_reply和返回值对象List。
- 然后把该方法的参数信息写入_data(如果有参数),3
- 接着调用transact方法发起RPC(远程过程调用),同时当前线程挂起,
- 然后服务端的onTransact方法会被调用知道RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。
之所以提供AIDL文件,是为了方便系统为我们生成代码,我们完全可以自己实现Binder。
2.3.3.3 可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知。
声明一个
DeathRecipient
对象。DeathRecipient
只有一个方法binderDied
,当Binder死亡的时候,系统就会回调DeathRecipient
方法。private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient(){ @Override public void binderDied(){ if(mBookManager == null){ return; } mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0); mBookManager = null; // TODO:接下来重新绑定远程Service } }
Binder有两个很重要的方法
linkToDeath
和unlinkToDeath
。通过linkToDeath
为Binder设置一个死亡代理。mService = IBookManager.Stub.asInterface(binder); binder.linkToDeath(mDeathRecipient,0);
- 另外,可以通过Binder的
isBinderAlive
判断Binder是否死亡。
2.4 Android中的IPC方式
主要有以下方式:
- Intent中附加extras来传递消息
- 共享文件
- Binder方式
- 四大组件之一的ContentProvider
- Socket
2.4.1 使用Bundle
四大组件中的三大组件(Activity、Service、Receiver)都支持在Intent中传递Bundle
数据。Bundle实现了Parcelable接口,**当我们在一个进程中启动了另一个进程的Activity、Service、Receiver,可以再Bundle中附加我们需要传输给远程进程的消息并通过Intent发送出去。被传输的数据必须能够被序列化。
2.4.2 使用文件共享
一些概念:
- 两个进程通过读写同一个文件来交换数据。还可以通过
ObjectOutputStream
/ObjectInputStream
序列化一个对象到文件中,或者在另一个进程从文件中反序列这个对象。注意:反序列化得到的对象只是内容上和序列化之前的对象一样,本质是两个对象。 - 文件并发读写会导致读出的对象可能不是最新的,并发写的话那就更严重了(书本原文,个人理解这里的严重应该是指数据丢失。)。所以文件共享方式适合对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写问题。
SharedPreferences
底层实现采用XML文件来存储键值对。系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences
文件的缓存,因此在多进程模式下,系统对它的读/写变得不可靠,面对高并发读/写时SharedPreferences
有很大几率丢失数据,因此不建议在IPC中使用SharedPreferences
。
2.4.3 使用Messenger
Messenger可以在不同进程间传递Message对象。是一种轻量级的IPC方案,底层实现是AIDL。
具体使用时,分为服务端和客户端:
服务端:创建一个Service来处理客户端请求,同时创建一个Handler并通过它来创建一个Messenger,然后再Service的onBind中返回Messenger对象底层的Binder即可。
private final Messenger mMessenger = new Messenger (new xxxHandler());
- 客户端:绑定服务端的Sevice,利用服务端返回的IBinder对象来创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,消息类型是
Message
。如果需要服务端响应,则需要创建一个Handler并通过它来创建一个Messenger(和服务端一样),并通过Message
的replyTo
参数传递给服务端。服务端通过Message的replyTo
参数就可以回应客户端了。 - 总而言之,就是客户端和服务端 拿到对方的Messenger来发送
Message
。只不过客户端通过bindService
而服务端通过message.replyTo
来获得对方的Messenger。 - Messenger中有一个
Hanlder
以串行的方式处理队列中的消息。不存在并发执行,因此我们不用考虑线程同步的问题。
2.4.4 使用AIDL
如果有大量的并发请求,使用Messenger就不太适合,同时如果需要跨进程调用服务端的方法,Messenger就无法做到了。这时我们可以使用AIDL。
流程如下:
- 服务端需要创建Service来监听客户端请求,然后创建一个AIDL文件,将暴露给客户端的接口在AIDL文件中声明,最后在Service中实现这个AIDL接口即可。
- 客户端首先绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
注意事项:
- AIDL支持的数据类型:
- 基本数据类型、String、CharSequence
- List:只支持ArrayList,里面的每个元素必须被AIDL支持
- Map:只支持HashMap,里面的每个元素必须被AIDL支持
- Parcelable
- 所有的AIDL接口本身也可以在AIDL文件中使用
- 自定义的Parcelable对象和AIDL对象,不管它们与当前的AIDL文件是否位于同一个包,都必须显式import进来。
如果AIDL文件中使用了自定义的Parcelable对象,就必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型。
package com.ryg.chapter_2.aidl; parcelable Book;
AIDL接口中的参数除了基本类型以外都必须表明方向in/out。AIDL接口文件中只支持方法,不支持声明静态常量。建议把所有和AIDL相关的类和文件放在同一个包中,方便管理。
void addBook(in Book book);
- AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,管理数据的集合直接采用
CopyOnWriteArrayList
来进行自动线程同步。类似的还有ConcurrentHashMap
。 因为客户端的listener和服务端的listener不是同一个对象,所以
RecmoteCallbackList
是系统专门提供用于删除跨进程listener的接口,支持管理任意的AIDL接口,因为所有AIDL接口都继承自IInterface
接口。public class RemoteCallbackList<E extends IInterface>
它内部通过一个Map接口来保存所有的AIDL回调,这个Map的key是
IBinder
类型,value是Callback
类型。当客户端解除注册时,遍历服务端所有listener,找到和客户端listener具有相同Binder对象的服务端listenr并把它删掉。- 客户端RPC的时候线程会被挂起,由于被调用的方法运行在服务端的Binder线程池中,可能很耗时,不能在主线程中去调用服务端的方法。
2.4.5 使用ContentProvider
- ContentProvider是四大组件之一,其底层实现和Messenger一样是Binder。ContentProvider天生就是用来进程间通信,只需要实现一个自定义或者系统预设置的ContentProvider,通过ContentResolver的query、update、insert和delete方法即可。
- 创建ContentProvider,只需继承ContentProvider实现
onCreate
、query
、update
、insert
、getType
六个抽象方法即可。除了onCreate
由系统回调并运行在主线程,其他五个方法都由外界调用并运行在Binder线程池中。
2.4.6 使用Socket
Socket可以实现计算机网络中的两个进程间的通信,当然也可以在本地实现进程间的通信。
服务端Service监听本地端口,客户端连接指定的端口,建立连接成功后,拿到Socket
对象就可以向服务端发送消息或者接受服务端发送的消息。
以上集中IPC方式都是感性的总结,具体代码请参考这里。
2.5 Binder连接池
AIDL是一种最常用的IPC方式,是日常开发中涉及IPC时的首选。前面提到AIDL的流程是 客户端在Service的onBind方法中拿到继承AIDL的Stub对象,然后客户端就可以通过这个Stub对象进行RPC。
那么如果项目庞大,有多个业务模块都需要使用AIDL进行IPC,随着AIDL数量的增加,我们不能无限制地增加Service,我们需要把所有AIDL放在同一个Service中去管理。
流程是:
- 服务端只有一个Service,我们应该把所有AIDL放在一个Service中去管理,不同业务模块之间是不能有耦合的
- 服务端提供一个
queryBinder
接口,这个借口能够根据业务模块的特征来返回响应的Binder对象给客户端 - 不同的业务模块拿到所需的Binder对象就可以进行RPC了