IPC 机制
2.1 Android IPC 简介
- IPC 是 Inter-Process Communication 的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
- IPC 不是 Android 中所独有的,任何一个操作系统都需要有相应的IPC 机制,比如Windows 上可以通过剪贴板,管道和邮槽进行进程间通信,Linux 可以通过命名管道,共享内容,信号等来进行进程间通信。不同的操作系统有不同的方式实现。Android,它的进程间通信方式不能完全继承自Linux,在Android中最有特色的进程间通信方式就是Binder,除此之外Android上还支持Socket,通过Socket 也可以实现任意两个终端之间的通信,当然同一个设备上的两个进程通过Socket 通信自然也是可以的。
2.2 Android 中的多进程模式
2.2.1 开启多进程模式
- 在Android中使用多进程只有一种方法,那就是给四大组件(Activity,Service,Receiver,ContentProvider) 在AndroidMenifest 中指定android:process 属性。其实还有另一种非常规手段,通过JNI 在 native 层去fork 一个新的进程。
// 默认包名是me.ewriter
android:process="me.ewriter.test"
android:process=":test"
上面的代码中,有两种形式。带上: 的表示进程属于当前应用的私有进程,其他应用组件不可以和它跑在同一个进程。而进程名不以:开头的进程属于全局进程,其他应用通过ShareUID 的方式可以和它跑在同一个进程。上面的例子,我们的默认包名是me.ewriter。那么默认就会有me.ewriter,me.ewriter.test,me.ewriter:test三个进程
Android系统会为每一个应用分配唯一的UID,具有相同UID的应用才能共享数据。这里需要说明,两个应用通过ShareUID 跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID 并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录等
2.2.2 多进程模式的运行机制
- Android 为每个应用分配了一个独立的虚拟机,或者说为每个进程都分配了一个独立虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致了不同的虚拟机中访问同一个类的对象会产生多分副本。在书上的例子中就是me.ewriter.art_chapter2 和 me.ewriter.art_chapter2:remote 中都存在一个UserManager 类,并且这两个类是互不干扰的。
所有运行在不同进程的四大组件,只要它们之间需要通过内存共享数据,都会共享失败。这也是多进程所带来的主要影响。一般会造成下面几个方面的问题
- 静态成员和单例模式完全失效
- 线程同步机制完全失效
- SharePreferences 的可靠性下降
- Application 会多次创建
第一个问题例子中已经说明;第二个问题类似,由于不是一块内存了,那么不管是锁对象还是锁全局类都无法保证线程同步;第三个问题是因为SharePreference 不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,这是因为SharePrefences 底层是通过读写xml 文件来实现的;第四个问题当一个组件跑在一个新的进程中事,由于系统在创建新的进程的同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程。
2.3 IPC 基础概念介绍
- Serializable 和 Parcelable 接口可以完成对象的序列化过程,当我们需要通过Intent 和 Binder 传输数据时就需要使用Parcelable 或者Serializable。
2.3.1 Serializable 接口
- Serializable 是Java 所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。想让一个对象实现序列化只要让这个类实现Serializable 接口并声明serialVersionUID即可,实际上serialVersionUID也不是必须的,但是这会影响到反序列化。AS 中需要做下面设置
- 如何对对象序列化也很简单,参考下面的代码
// 序列化过程
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();
序列化的时候系统会把当前类的serialVersionUID 写入序列化文件中(也可能是其他媒介),当反序列化的时候系统回去检查文件中的 serialVersionUID ,看它是否和当前类的 serialVersionUID 是否一致。如果相同则可以反序列化,如果不同则说明当前类和序列化的类相比发生了某些变化,比如成员变量的数量,类型可能发生了变化 ,这个时候是无法正常反序列化的。
静态成员变量属于类,不属于对象,所以不会参与序列化过程;其次transient关键字标记的成员变量不参与序列化过程。
2.3.2 Parcelable 接口
下面是一个实现Parceable 的实例
protected User(Parcel in) {
userId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
book = in.readParcelable(Thread.currentThread().getContextClassLoader());
}
public static final Creator<User> CREATOR = new Creator<User>() {
@Override
public User createFromParcel(Parcel in) {
return new User(in);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(userId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
dest.writeParcelable(book, 0);
}
先介绍下Parcel,Parcel内部包装了可序列话的数据,可以在Binder 中传输。序列化功能由writeToParcel 方法来完成,最终是通过Parcel 中的一系列write方法来完成的;反序列化由CRATOR 来完成,其内部表明了如何创建序列化对象和数组,并通过Parcel 的一系列read方法来完成反序列化过程;describeContents 方法表示内容描述,几乎所有情况都返回0,仅当当前对象中存在文件描述时返回1;
注意在User(Parcel in) 方法中,由于book 是另一个可序列化对象,所以它的反序列化需要传递当前线程的上下文加载器,否则会找不到类,详细的方法说明见下表:
方法 | 功能 | 标记位 |
---|---|---|
createFromParcel(Parcel in) | 从序列化后的对象中创建原始对象 | |
newArray(int size) | 创建指定长度的原始对象数组 | |
User(Parcel in) | 从序列化后的对象中创建原始对象 | |
writeToParcel(Parcel dest, int flags) | 将当前对象写入序列化结构中,其中flags 标识有两种值:0或者1。为1时标识当前对象需要作为返回值返回,不能立即释放资源,自护所有情况都为0 | PARCELABLE_WRITE_RETURN_VALUE |
describeContents | 返回当前对象的内容描述,如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0 | CONTENTS_FILE_DESCRIPTOR |
- 像Intent, Bundle,Bitmap 等都是系统实现了Parcelable 接口的类。Serializable 是Java 的序列化接口,序列化和反序列化需要大量的I/O操作。Parcelable 主要用在内存序列化上。
2.3.3 Binder
Binder比较复杂,本节的侧重点是介绍Binder的使用以及上层原理:直观来说,Binder 是 Android 中的一个类,它实现了IBinder 接口;从IPC 角度来看,Binder 是 Android 中的一种跨进程通信方式,Binder 还可以理解为一种虚拟的物理设备,它的驱动是/dev/binder, 该通信方式在Linux 中没有;从Android Framework 角度来说,Binder 是 ServiceManager 连接各种 Manager(ActivityManager, WindowManager 等等) 和相应ManagerService 的桥梁;从Android 应用层看,Binder 是客户端和服务器端进行通信的媒介,当bindService 的时候,服务端会返回一个包含业务端调用的Binder 对象。通过这个Binder 对象,客户端就可以获取服务端提供的数据或服务,这里的服务包括普通服务和基于AIDL 的服务。
Android 开发中,Binder 主要用在Service 中,包括AIDL 和 Message ,其中普通Service 中的Binder 不涉及进程间通信,比较简单,无法触及Binder 的核心。而Messenger 的底层其实是AIDL ,所以这里选择用AIDL 来分析Binder 的工作机制
根据书中的例子创建了Book 及 IBookManager 的aidl 文件后,rebuild,如果创建正确,那么会在build 下面生成IBookManager.java 的文件,我们把它拷贝出来(附件中),看下里面的代码。
可以看到它继承了android.os.IInterface,同时它自己也还是一个接口,所有可以在Binder 中传输的接口都需要集成IInterface 接口。看下类中,它申明了两个方法,getBookList 和 addBook,这是我们在aidl文件中声明的方法,同时它还申明了两个整形id标识这两个方法,这两个id标识在transcat 过程中客户端所请求的到底是哪个方法。接着声明了一个内部类Stub。这个Stub 就是一个Binder 类(extends android.os.IInterface),当客户端和服务端都位于同一个进程时,方法调用不会走跨进程transact 过程。当处于不同进程,方法调用走transact 过程,这个过程由Stub 的内部代理类Proxy 完成。 这个接口的*核心实现就是它的内部类Stub 和 Stub 的内部代理类Proxy *;
关于Stub 和 Proxy 中的方法可以看附件中文件的注释
DESCRIPTOR: Binder 的唯一标识,一般用当前Binder 的类名表示
asInterface(android.os.IBinder obj) :用于将服务端的Binder 对象转换成客户端所需要的AIDL 接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于同一进程,那么此方法返回的就是服务端的Stub对象本身,否则返回的是系统封装后的Stub.proxy 对象
asBinder() : 返回当前Binder 对象
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
- 这个方法运行在服务端的Binder 线程池中,当客户发起跨进程请求时,远程请求会通过系统底层封装后交由此方法处理。 1.服务端通过code 确定客户端请求的目标方法是什么,接着从data 中取出目标方法所需要的 参数,然后执行目标方法 2.当目标方法执行完后,就像reply中写入返回值(如果存在)。如果返回false,那么客户端的请求会失败,我们可以用这个特性做权限验证,避免任何一个 进程调用我们的服务
Proxy#[方法]: 代理类中的接口方法,这些方法运行在客户端。当客户端远程调用此方法时,它的内部实现是:首先创建该方法所需要的参数,然后把方法的参数信息写入到_data中,接着调用transact方法来发起RPC请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。
通过上面的分析,大致了解了 Binder 的工作机制,但还是有两点需要额外说明一下:当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,因此如果一个远程方法很耗时的话是不能放在 UI 线程中请求的;其次,由于服务端的 Binder 方法运行在 Binder 的线程池中,所以 Binder 方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。下面这个图说明了 Binder 的工作机制
从上面的分析来看,我们完全可以不提供AIDL 文件 即可实现Binder ,之所以提供AIDL 文件,是为了方便系统帮我们生成代码。系统生成的那个类主要由两部分组成,它本身是Binder 的接口(继承IInterface),其次它的内部有一个Stub 类,继承自Binder。
Binder 的两个很重要的方法 linkToDeath 和 unLinkToDeath。Binder 运行在服务端进程,如果服务端进程由于某种原因异常终止,这时我们到服务端的Binder断裂(称为Binder 死亡),会导致我们的调用失败。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.d(TAG, "binder died. tname:" + Thread.currentThread().getName());
if (mRemoteBookManager == null)
return;
mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
mRemoteBookManager = null;
// TODO:这里重新绑定远程Service
}
};
2.4 Android 中的IPC 方式
2.4.1 使用Bundle
- Bundle 实现了 Parcelable 接口,所以方便在不同进程间传递。记住我们传输的数据必须能够被序列化
2.4.2 使用文件共享
Android 基于 Linux ,使得并发读/写可以没限制的进行,甚至两个线程同时对同一个文件进行写操作都允许,但是这可能会出现问题。
文件共享这种方式对格式没有要求,只要双方约定好即可。文件读写适合在对数据同步要求不高的进程之间进行通信,并且要妥善解决并发读/写的问题。
SharedPreferences 是一个特例,虽然是通过读写 xml 实现的,但是由于系统对它的读写有缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读写就ui变得不可靠。
2.4.3 使用Messenger
Messenger 可以翻译为信使,通过它可以在不同进程间传递 Message 对象,Message 中放入我们需要传递的数据即可。Messenger 是一种轻量级的 IPC 方案,它的底层是 AIDL. 下面是 Messenger 的两个构造方法,不管是 IMessenger 还是 Stub.asInterface ,都能表明它的底层是 AIDL.
Messenger 是串行处理请求而的,即它一次处理一个请求,因此我们在服务端不用考虑线程同步的问题,这是因为服务端不存在并发执行的情况。
public Messenger(Handler target) {
mTarget = target.getIMessenger();
}
public Messenger(IBinder target) {
mTarget = IMessenger.Stub.asInterface(target);
}
实现一个 Messenger 有如下几个步骤,分为服务端和客户端
服务端进程: 在服务端创建一个 Service 来处理客户端的链接请求,同时创建一个 Handler 并通过它来创建一个 Messenger 对象,然后在 Service 的 onBind 中返回这个 Messenger 对象底层的 Binder 即可。
客户端进程:首先要绑定服务端的 Service,绑定成功后用服务端返回的 IBinder 对象创建一个 Messenger, 通过这个 Messenger 就可以像服务端发送消息了,发消息的类型为 Message 对象。如果需要服务端能回应客户端,那就和服务端一样创建一个 Handler 并创建一个新的 Messenger,并把这个 Messenger 对象通过 replyTo 参数 传递给服务端,服务端通过这个 replyTo 参数就可以回应客户端。
2.4.4 使用AIDL
- Messenger 以串行处理进程间通信,所以在面对大量并发请求并不是很合适。同时 Messenger 的作用主要是传递消息, 很多时候我们要跨进程调用服务端的方法,这种情形 Messenger 就做不到,但是可以用 AIDL,Messenger 本质上也是 AIDL, 只是做了封装方便调用而已。
实现 AIDL 来进行进程间通信,分为客户端和服务端两个方面
- 服务端: 创建一个 Service 用来监听客户端的情趣,然后创建一个 AIDL 文件,将暴露给客户端的请求都在 AIDL 文件中声明,最后实现这个 AIDL 接口即可
- 客户端:绑定服务端 Service,绑定成功后,将服务端返回的 Binder 对象转换成 AIDL 接口所属类型就可以调用 AIDL 中的方法了
- 创建 AIDL 文件: 并不是所有数据类型都可以使用的,AIDL 支持下面这些数据类型
- 基本数据类型(int, long,char,boolean,double 等)
- String 和 CharSequence
- List: 只支持 ArrayList ,里面每个元素都必须能够被 AIDL 支持
- Map: 只支持 HashMap,里面每个 key , value 都必须被 AIDL 支持
- Parcelabe
- AIDL: 所有的 AIDL 接口本身也可以在 AIDL 文件中使用。
- AIDL 中除了基本数据类型,其他类型的参数必须表上方向: in,out 或者 inout,in 表示输入型参数,out 表示输出型参数,inout 表示输入输出型参数。不能一概的使用 out 或者 inout,因为这在底层实现是有开销的。AIDL 接口中只支持方法,不支持声明静态常量
- 远程服务端 Service 的实现 :参考 Demo 中的 BookManagerService
- 客户端的实现:参考 Demo 中的 BookManagerActivity
远程服务端和客户端的实现,建议结合上面的源码一起看。里面增加了关于 CopyOnWriteArrayList 和 RemoteCallbackList 相关的注释
默认情况下,我们的远程服务任何人都可以连接,因此我们必须给服务加入权限验证功能。这里介绍两种常用的方法:
- 第一种方法在 onBind 中验证,不同步就直接返回 null。验证的方法很多,比如使用 permission 验证。首先现在 AndroidManifest 中声明所需的权限,让后在 onBind 中做权限处理。如果自己内部的应用想要绑定我们的服务,只需要在它的 AndroidManifest 文件中使用 permission 即可
<permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" android:protectionLevel="normal" /> <use-permission android:name=“com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE” />
int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return null;
}
第二种方法在服务端的 onTransact 方法中进行权限验证,失败就返回 false。除了使用 permission 验证,还可以采用 Uid 和 Pid 来验证,通过 getCallingUid 和 getCallingPid 可以拿到客户端所属应用的 Uid 和Pid,通过这两个参数可以做一些验证,比如验证包名。
@Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { int check = checkCallingOrSelfPermission("com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE"); Log.d(TAG, "check=" + check); if (check == PackageManager.PERMISSION_DENIED) { return false; } String packageName = null; String[] packages = getPackageManager().getPackagesForUid(getCallingUid()); if (packages != null && packages.length > 0) { packageName = packages[0]; } Log.d(TAG, "onTransact: " + packageName); if (!packageName.startsWith("com.ryg")) { return false; } return super.onTransact(code, data, reply, flags); }
2.4.5 使用ContentProvider
ContentProvider 是 Android 中提供的专门用于处理不同应用见进行数据共享的方式。和 Messenger 一样,*ContentProvider 的底层实现也是 Binder *。实现的时候除了 CRUD 操作还有防止 SQL 注入和权限控制等。
实现 ContentProvider 只要继承 ContentProvider 并实现 onCreate、query、update、insert、delete 和 getType 这六个方法。getType 用来返回 Uri 请求对应的 MIME 类型,不关心这个选项的话只要返回 null 或者 / 即可。根据 Binder 的原理,这六个方法均运行在 ContentProvider 的进程中,除了 onCreate 由系统回调并运行在主线程里,其他五个方法均有外界回调并运行在 Binder 线程池中。
注册 ContentProvider, 其中 android:authorities 是唯一标识,通过这个属性外部应用可以访问我们的 BookProvider,因此必须是唯一的,可以在命名的时候加上我们的包名。如果想要限制外界程序访问可以加上 android:permission 属性,也可以细分为读权限和写权限,分别对应 android:readPermission 和 android:writePermission 属性。
ContentProvider 通过 Uri 来区分外界要访问的数据集合,为了知道外界访问什么表,需要单独为它们定义 Uri 和 Uri_Code 。我们可以用 UriMatcher 的 addURI 方法将 Uri 和 Uri_Code 相关联。
query、update、insert、delete 四大方法是存在多线程并发访问的,所以方法内部要做好线程同步
2.4.6 使用Socket
Socket 分为流式套接字和用户数据报套接字两种,分别对应于网络传输控制层中的 TCP 和 UDP 协议。使用 Socket 进行同行,需要声名下面的权限:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
具体实现可以查看 Demo
2.5 Binder 连接池
项目大了之后,可能 AIDL 会有很多,这时候如果新建很多 Service 会让应用看起来很中。因此我们要将所有的 AIDL 放在同一个 Service 中去管理。
Binder 连接池的主要作用是将每个业务模块的 Binder 请求统一转发到远程 Service 中去执行,从而避免了重复创建 Service 的过程
为了模拟,我们提供了两个 AIDL 接口(ISecurityCenter 和 ICompute)来模拟上面提到的多个业务模块都要使用 AIDL 的情况,其中 ISecurityCenter 接口提供加密解密功能,具体可以查看 BinderPoolActivity
2.6 选择合适的 IPC 方式
名称 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle 支持的数据类型 | 四大组件间的进程通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的既是通信 | 无并发访问的情形,交换简单的数据实用性不高的场景 |
AIDL | 功能强大,支持一对多并发通信,支持实时通信 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有 RPC 需求 |
Messenger | 功能一般,支持一对多串行通信,支持实时通信 | 不能很好处理高并发情形,不支持 RPC,数据通过 Message 进行传输,因此只能传输 Bundle 支持的数据类型 | 低并发的一对多即时通信,无 RPC 需求,或者无须返回结果的 RPC 需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作 | 可以理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 | 一对多的进程间的数据共享 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点麻烦,不支持直接的 RPC | 网络数据交换 |
本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!