Android IPC
IPC简介
IPC即Inter-Process Communication(进程间通信)。指的是两个进程间进行数据交换的过程。Android间进程通信的方式并不完全继承于Linux,它有自己独特的一套进程间通信的方式。Android中实现IPC中最有特色的是Binder,通过Binder可以轻松地实现IPC。另外,Android还支持Socket,不仅可以实现一个设备上的两个进程间通信,还可以实现两个终端之间的通信。
Android中的多进程模式
开启多进程模式
正常情况下,Android中的多进程是指一个应用中存在多进程的情况,因此,此处暂不讨论两个应用之间的多进程情况。
在Android中使用多进程只有一个方法,在manifest文件中给四大组件制定android:process
属性,另外还有一种非常规的方法,在JNI在natice层中fork一个新的进程,暂不讨论。
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".SecondActivity" android:process=":remote">
</activity>
<activity android:name=".ThirdActivity" android:process="com.daijie.mulitprocesstest.remote">
</activity>
上述代码中,分别为SecondActivity和ThirdActivity指定了process属性,这意味着在启动这两个Acitvity的时候,系统将为它创建一个单独的进程,进程名分别为"com.daijie.mulitprocesstest:remote"和"com.daijie.mulitprocesstest.remote"。而MainActivity没有指定process属性,则运行在默认的进程中,默认的进程名是包名。
可以看到,在process中“:”开头的含义则是在当前进程名前面附上当前的包名,而另外一种不包含“:”的则是一种完整的命名方式。其次,以“:”开头的进程属于当前应用的私有进程,不可以与其他应用的组件跑在同一个进程中,而不以“:”开头的进程则属于全局进程,其他应用可以通过ShareUID的方式和它跑在同一个进程中
Android系统会为每一个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用间通过ShareID跑在同一个进程是有要求的,不仅需要ShareID相同,还需要签名相同才可以。这种情况下,不管它们是否运行在同一进程中,它们可以互相访问对方的私有数据(比如data目录和组件信息等),而如果跑在同一进程中,则还可以共享内存数据,就像一个应用的两个部分。
以上的方法看起来很简单就开启了多进程,但是多进程远远没那么简单,还有许多事情要进行处理。
多进程的运行机制
开启多进程模式后,会出现各种奇怪的现象,比如以下的全局静态变量。
public class UserManager {
public static int sUserId = 1;
}
在MainActivity中重新给sUserId赋值为2,然后在运行在另一个进程中的SecondActivity打印sUserId的值,发现还是原来的1。
出现这种情况的原因是,SecondActivity运行在另一个进程中。因为Android中为每一个进程分配了一个独立的虚拟机,而不同的虚拟机在内存分配上有着不同的地址空间,这就导致了在不同虚拟机上访问同一个类的对象可能产生多份副本。而MainActivityg和SecondActivity由于运行在不同进程上,所以所持有的UserManaer并非同一个对象,修改后只会影响当前进程中的对象。
一般情况下,使用多进程可能产生以下的问题。
- 静态成员和单例模式失效
- 线程同步机制完全失效
- SharePreferences的可靠性下降
- Application会多次创建
第1个问题和第2个问题是显而易见的,毕竟都不在同一块内存了,无论是锁对象还是全局类都无法保证线程同步,因为不同的进程锁的都不是同一个对象。第3个问题是因为SharePreferences不支持两个进程同时进行写操作,否则会造成一定几率的数据缺失,因为其底层是通过读/写xml文件实现的,所以并发读/写都可能出现问题。
第4个问题是因为当一个组件跑在一个新的进程中,系统在创建新的进程的同时要分配独立的虚拟机,而这个过程其实就是一个启动一个应用的过程,相当于把这个应用又重新启动的一遍,既然都重新启动了,那么自然是要创建新的Application对象。所以可以这么理解,运行在同一个进程的组件是属于同一个虚拟机和同一个Application的,换言之,运行在不同进程的组件是属于两个不同的虚拟机和Application的。
IPC基础概念
IPC中,主要包含三方面的内容,Serializable,Paercelable,Binder。其中数据的传输需要先通过序列化,而Android上进行序列化主要依赖Serializable接口和Paercelable接口,序列化后是通过Intent和Binder进行数据的传输。
Serializable接口
Serializable是Java提供的一个序列化的空接口,为对象提供了标准的序列化和反序列化操作。可以通过声明一个静态int类型的serialVersionID
的值来保证序列化和反序列化的类serialVersionID相同而被正确的反序列化。
Parcelable接口
Parcelable是Android提供的一个序列化接口,需要对其提供的方法进行实现。Android为我们提供了许多实现Parcelable接口的类,比如Intent,Bundle,Bitmap等,而List和Map也可以序列化,前提是子元素也必须都是可序列化的。
Serializable和Parcelable
Serializable虽然简单但是要进行大量的I/O操作,开销很大,而Parcelable是Android中的序列化方式,更适合用在Android平台上。而通过Parcelable主要用于在内存序列化话上,虽然序列化后通过网络传输也是可以的,但是会稍显复杂,这种情况还是首选使用Serializable。
Binder
Binder是Android中的一个类,它实现了IBinder接口。从IPC的角度说,Binder是Android中的一种跨进程传输的方式,其中,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder。
从Android Framework的角度来说,Binder是ServiceManager连接各种Manager(Acitivty Manager、Window Manager,等等和相应ManagerService的桥梁。
从Android应用层的角度来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的数据包括普通服务和基于AIDL的服务。
Android开发中,Binder主要用在Service中,其中包含AIDL和Messenger。其中普通Service的Binder不涉及到进程间通信,,所以较为简单,无法触及到Binder的核心,而Messenger的底层其实也属于AIDL,所以可以使用AIDL来分析Binder的工作机制。
通过基于AIDL的服务分析Binder
我们需要新建一个AIDL实例,而SDK会为我们自动产生出AIDL所对应的Binger类,然后我们就可以分析Binder的工作过程。先新建三个文件Book.java,Book.aidl,IBookManager.aidl。
Book.java
public class Book implements Parcelable {
private int bookId;
private String bookName;
// setter and getter
// parcelable的实现
}
Book.aidl
package com.daijie.aldlapp;
parcelable Book;
IBookManager.aidl
package com.daijie.aldlapp;
// Declare any non-default types here with import statements
import com.daijie.aldlapp.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
以上代码中,Book.java是一个表示图书信息的类,它实现了Parcelable接口。Book.aidl是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法getBookList和addBook,分别用于远程获取书籍列表和添加一本书。
注意,在aidl中,尽管Book.aidl和IBookManager.aidl都在同一个包中,但是IBookManager仍需要导入Book类。
编写完上述代码后,在Android Studio执行make project,就会生成AIDL对应的java文件,代码如下。
package com.daijie.aldlapp;
public interface IBookManager extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
*/
public static abstract class Stub extends android.os.Binder implements com.daijie.aldlapp.IBookManager {
private static final java.lang.String DESCRIPTOR = "com.daijie.aldlapp.IBookManager";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.daijie.aldlapp.IBookManager interface,
* generating a proxy if needed.
*/
public static com.daijie.aldlapp.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.daijie.aldlapp.IBookManager))) {
return ((com.daijie.aldlapp.IBookManager) iin);
}
return new com.daijie.aldlapp.IBookManager.Stub.Proxy(obj);
}
@Override
public android.os.IBinder asBinder() {
return this;
}
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
switch (code) {
case INTERFACE_TRANSACTION: {
reply.writeString(DESCRIPTOR);
return true;
}
case TRANSACTION_getBookList: {
data.enforceInterface(DESCRIPTOR);
java.util.List<com.daijie.aldlapp.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(DESCRIPTOR);
com.daijie.aldlapp.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = com.daijie.aldlapp.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.daijie.aldlapp.IBookManager {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public java.util.List<com.daijie.aldlapp.Book> getBookList() throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.daijie.aldlapp.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.daijie.aldlapp.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void addBook(com.daijie.aldlapp.Book book) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List<com.daijie.aldlapp.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.daijie.aldlapp.Book book) throws android.os.RemoteException;
}
接下来,可以通过这个系统生成的Binder生成的Binder类来分析Binder的工作原理。
IBookManager是一个接口,并且继承了android.os.IInterface接口(注意,所以在Binder中传输的接口都需要继承这个接口)。在这个接口中,声明了两个方法getBookList和addBook,显然也是我们在IBookManager中声明的方法,同时它还声明了两个id用于标识这两个方法,这两个方法用于标志在transact过程中客户端请求的到底是哪个方法。它声明了一个内部类Stub,这个Stub就是一个Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,而当两者位于不同进程的时,方法调用需要走transact过程,这个逻辑由Stub的内部代理Proxy完成。综上所述,这个接口的核心实现是它的内部类Stub和Stub的内部类Proxy。
下面来详细介绍针对这两个类的每个方法的含义。
DESCRIPTOR
Binder类的唯一标识,一般用当前Binder类的类名表示。
asInterface(android.os.IBinder obj)
用于将服务端的Binder对象转换成客户端所需要的AIDL接口类型的对象。这种转换过程是区分进程的。如果客户端和服务端位于同一进程,则此方法返回的就是服务端的Stub对象本身,否则返回系统封装后的Stub.proxy对象
onTrancsact
函数原型为onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
这个方法运行在服务顿的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。服务端通过code可以确定客户端所请求的目标方法是什么,接着从data中取出目标方法所需要的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向reply中写入返回值(如果目标方法有返回值的话)。需要注意的是,如果这个方法返回false,那么客户端的请求就会失败,因此我们可以利用这个特性来做权限验证,避免随便一个进程都能远程调用我们的服务
Proxy#getBookList
这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现是这样的:首先创建这个方法所需要的输入型Parcel对象_data、输出型Parcel对象_reply和返回值对象List;然后把该方法的参数信息写入_data(如果有参数的话);接着调用transact方法来完成RPC(远程方法调用)请求,同时将当前线程挂起;然后服务端的onTransact方法会被调用,直至RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;最后返回_reply的数据。
Proxy#addBook
执行过程和Proxy#getBookList是一样的,但因为addBook没有返回值所以不需要从_reply中取出返回值。
通过以上的分析,已经可以初步了解Binder的工作机制了,但是还需要注意一下两点。
客户端发起远程请求时,由于当前线程会被挂起直至服务器进程返回值,所以如果调用的远程方法比较耗时,那么则不能在UI现在中发起这个请求,否则会出现ANR。其次由于服务端的Binder方法运行在Binder的线程池中,所以Binder无论是否耗时都必须采用同步的方式实现,因为它已经是运行在一个线程中了。
下图是Binder的工作机制图
上述过程中,我们完全可以不使用AIDL文件即可实现Binder,而AIDL文件只是方便系统为我们生成代码的手段。
Binder死亡监听
Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候客户端到服务端的Binder断裂(称之为Binder死亡)会导致远程调用失败,更为关键的是,客户端不知道Binder连接已经断裂,那么客户端功能就会受到影响。为了解决这个问题,Binder提供了两个很重要的配对方法linkToDeath
和unlinkToDeath
,通过linkToDeath我们可以给Binder设置一个死亡代理,当Binder死亡的时候,系统回调binderDied方法,然后我们就可以移除走之前绑定的Binder死亡代理并重新绑定远程服务。
用法
首先声明一个DeathRecipient对象。DeathRecipient是一个接口,其内部只有一个方法binderDied方法。当Binder死亡的时候,系统就会回调这个方法,然后我们就可以移除之前绑定的死亡代理并重新绑定远程服务。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (mMyAIDLService == null) {
return;
}
mMyAIDLService.asBinder().unlinkToDeath(this, 0);
mMyAIDLService = null;
// 重绑定
}
};
其次,在客户端绑定远程服务成功之后,给binder设置死亡代理
mService =MyAIDLService.Stub.asInterface(binder);
binder.linkToDeath(mDeathRecipient,0);
以上代码中,linkToDeath的第二个参数是标记位,直接设置为0即可。经过以上两个步骤就可以给Binder设置死亡代理,当Binder死亡的时候就可以接收到通知。此外,也可以通过Binder的方法isBinderAlive
判断Binder是否死亡。