《Android开发艺术探讨》之 Android IPC 介绍
IPC是 Inter-Proscess Communication的缩写,含义为进程间的通讯或者跨进程通讯,是指两个进程之间进行数据交换的过程。按操作系统的中的描述,线程是CPU调度最小的单元,同时线程是一种有限的系统资源,而进程是指一个执行单元,在PC和移动设备上指一个程序或者一个应用。一个进程可以包含多个线程,因此进程和线程是包含于被包含的关系。
IPC的使用场景就必须提到多进程,只有面对多进程这种场景下,才需要考虑进程间通讯。多进程的情况分为两种:第一种是一个应用因为某些原因自身需要采用多进程模式来实现,原因有很多,应用特殊原因需要运行的单独的进程中,或者为了加大一个应用可使用内存所以需要通过多进程来获取多分内存空间。另外一种情况是:当前应用需要向其他应用获取数据,由于是两个应用,所以必须采取跨进程方式来获取所需要数据。
Android中的多进程模式
开启Android多进程模式很简单,就是给四大组件(Activity,Service,Receiver,ContentProvider)在AndroidMenifest中指定android:process属性。另外还有一种非常规的做法,那就是通过JNI在native层去fork一个新的进程。
给process指定多进程有两种不同的形式
多进程运行机制
我们知道Android为每一个应用分配了一个独立的虚拟机,或者说为每一个进程都分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机访问同一个类的对象会产生多分副本。
所有运行在不同进程中的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响,一般来说,使用多进程会造成如下几方面的问题。
IPC基础概念介绍
User user = new User("xia","123455");
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
out.write(user);
out.close();
//反序列化
ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
User newUser = (User)in.readObject();
in.close();
serialVersionUID是用来辅助序列化和反序列化过程的,原则上序列化后的数据中serialVersionUID只有和当前类serialVersionUID相同才能够正常的被反序列化。serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他中介),但反序列化的时候会去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,如果一致就说明序列化的版本和当前版本是相同的,这个时候可以成功的反序列化,否则就说明当前类和序列化的类相比发生了某些变换,比如成员变量的数量、类型发生了改变,这个时候无法正常反序列化。
一般来说,我们应该手动指定serialVersionUID的值,如1L,也可以根据自身结构自动去生成它的hash值,这样序列化和反序列化时两者的serialVersionUID是相同的。如果不指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某些成员变量,那么系统就会重新计算当前类型的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化数据中的serialVersionUID不一致,于是反序列化失败,程序就会出现crash。所以避免反序列化过程的失败。比如当版本升级后,我们很可能删除了某个成员变量也可能增加了一些新的成员变量,这个时候序列化过程仍然能够成功,程序可以最大限度地恢复数据,相反,如果不指定serialVersionUID的话,程序则会挂掉。当然我们还要考虑另外一种情况,如果类的结构发生了非常规性的改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过,但是反序列化还是会失败,因为类结构有了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构对象。
静态成员变量属于类不属于对象,所以不会参与序列化过程,其次用transient关键字标记的成员变量不参与序列化配置。
- Parceable 接口
Parceable也是一个接口,只有实现这个接口,一个类的对象就可以实现序列化并可以通过Intent和Binder传递。
public class User implements Parcelable {
public int UserId;
public String userName;
public boolean isMale;
protected User(Parcel in) {
//从序列化后的对象中创建原始对象
UserId = in.readInt();
userName = in.readString();
isMale = in.readByte() != 0;
}
public static final Creator CREATOR = new Creator() {
@Override
public User createFromParcel(Parcel in) {
//从序列化后的对象中创建原始对象
return new User(in);
}
@Override
public User[] newArray(int size) {
//创建指定长度的原始对象数组
return new User[size];
}
};
@Override
public int describeContents() {
/**
返回当前对象的内容描述。如果含有文件描述符,返回1,否则返回0,几乎所有情况都返回0
*/
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
/**将当前对象写入序列化结构中,其中flags,标识有0或1
为1时标识当前对象需要作返回值返回,不能立即释放资源,几乎所有情况 都为0**/
dest.writeInt(UserId);
dest.writeString(userName);
dest.writeByte((byte) (isMale ? 1 : 0));
}
}
Parcel内部包装了可序列化的数据,可以在Binder中自由传输,从上述代码中可以看出,在序列化过程中需要实现的功能有序列化、反序列化和内部描述序列化功能由writeParcel方法完成,最终是通过Parcel中的一系列write方法来完成的。反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel一系列read方法来完成反序列化过程;内容描述功能由describeContents来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1.系统已经提供了许多实现Parcelable接口的类,它们都是可以直接序列化的,如:Intent、Bundle、Bitmap等,同时List 和 Map也可以序列化,前提时它们里面每个元素都是可序列化的。
既然Parcelable 和Serializable 都可以用于Intent间的数据传递,那么如何选择了。
- Serializable是Java中的序列化接口,其使用起来简单但是开销大,序列化和反序列化过程都需要大量的 I/O操作。
- Parcelable是Android中的序列化方式,更适用于在Android平台上,它的缺点就是用起来稍微麻烦,但效率很高,这是Android推荐方式,因此,首选Parcelable。但通过Parcelable将对象序列化到存储设备中或将对象序列化后通过网络传输也都是可以的,但是这个过程会稍显复杂,因此这种情况下建议使用Serializable。
- Binder
Binder是一个非常复杂,这里只是介绍下Binder的使用及上层实现原理。
Binder是Android中的一个类,它实现了IBinder的接口。从IPC角度来说,Binder是Android中一种跨进程的通讯方式,Binder还可以理解为一种虚拟物理设备,它的设备驱动是 /dev/binder,该通讯方式在Linux中没有;从Android Framework,角度来说,Binder是ServiceManger连接各种Manger(ActivityManger 、WindowManger,等等)和相应的MangeSrervice的桥梁;从Android应用层来说,
Binder是客户端和服务端进行通讯的媒介,当bindSrervice的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以用获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。普通Srervice中的Binder不涉及进程间通信,下面通过AIDL来分析Binder的工作过程。
//Book.java
public class Book implements Parcelable{
int id;
String type;
public Book(int id, String type) {
this.id = id;
this.type = type;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", type='" + type + '\'' +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.id);
dest.writeString(this.type);
}
protected Book(Parcel in) {
this.id = in.readInt();
this.type = in.readString();
}
public static final Creator CREATOR = new Creator() {
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
}
```java
// Book.aidl
package com.example.xiahao.myapplication;
parcelable Book;
// IBookManager.aidl
package com.example.xiahao.myapplication;
// Declare any non-default types here with import statements
import com.example.xiahao.myapplication.Book;
interface IBookManager {
List getBookList();
void addBook(in Book book);
}
上面三个文件中,Book.java是一个表示图书信息的类,它实现了Parcelable接口。Book.aidl 是Book类在AIDL中的声明。IBookManager.aidl是我们定义的一个接口,里面有两个方法 getBookList() 和addBook(),其中getBookList用于从远程服务端获取图书列表,而addBook是添加一本书。虽然Book类已经和IBookManager位于相同的包中,但IBookManager仍然需要导入Book类,这就是AIDL的特殊之处。builde的项目,系统为我们在gen目录下生产IBookManage.java的类,接下来我们需要根据这个系统生成的IBookManag类来分析Binder的工作原理
/*
* This file is auto-generated. DO NOT MODIFY.
* Original file: /Users/xiahao/Documents/WorkSpace/AndroidStudioProjects/MyApplication/app/src/main/aidl/com/example/xiahao/myapplication/IBookManager.aidl
*/
package com.example.xiahao.myapplication;
public interface IBookManager extends android.os.IInterface {
/**
* Local-side IPC implementation stub class.
*/
public static abstract class Stub extends android.os.Binder implements com.example.xiahao.myapplication.IBookManager {
private static final java.lang.String DESCRIPTOR = "com.example.xiahao.myapplication.IBookManager";
/**
* Construct the stub at attach it to the interface.
*/
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
/**
* Cast an IBinder object into an com.example.xiahao.myapplication.IBookManager interface,
* generating a proxy if needed.
*/
public static com.example.xiahao.myapplication.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) &&
(iin instanceof com.example.xiahao.myapplication.IBookManager))) {
return ((com.example.xiahao.myapplication.IBookManager) iin);}
return new com.example.xiahao.myapplication.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.example.xiahao.myapplication.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(DESCRIPTOR);
com.example.xiahao.myapplication.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = com.example.xiahao.myapplication.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
com.example.xiahao.myapplication.IOnNewBookArrivedListener _arg0;
_arg0 = com.example.xiahao.myapplication.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
this.unegisterListener(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.example.xiahao.myapplication.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.example.xiahao.myapplication.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.example.xiahao.myapplication.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.example.xiahao.myapplication.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void addBook(com.example.xiahao.myapplication.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.example.xiahao.myapplication.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.example.xiahao.myapplication.Book book) throws android.os.RemoteException;
}
可以看到根据IBookManager.aidl系统为我们生成了IBookManager.java这个类,它继承了IInterface这个接口,同时它自己也还是个接口,所以可以在Binder中传输的接口都需要继承IInterface接口。
首先,它声明了两个方法getBookList 和 addBook ,这就是我们在IBookManger.aidl中所声明的方法,同时它还声明了两个整数的id分别用于标识这两个方法,这两个id用标识在transact过程客户端请求的到底是哪个方法。接着,还声明了一个内部类Stub,这个Stub就是一个Biner类,当客户端和服务端都位于同一个进程中,方法调用不会走跨进程的transact过程,而当两者位于不同的进程中,方法需要走transact过程,这个逻辑由Stub的内部代理类 Proxy来完成。所以这个接口的实现核心就是它的内部类Stud和Stub的内部代理类 Proxy。
Proxy#getBookList
这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值,所以他不需要从 _reply中取出返回值。
注意:当客户端发起远程请求时,由于当前线程会被挂起直至服务器返回数据,所以如果一个远程的方法是很耗时的话,那么不能再UI线程中发起次远程请求;其次,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式实现,因为它已经运行在一个线程中了。为了更好的说明Binder,下面给出一个工作机制的图:
接下来,介绍下Binder的两个很重要的方法 linkTodeath 和 unlinkTodeath,如果服务端的Binder连接断裂 (称之为 Binder 死亡),会导致我们远程调用失败。更为关键的时,如果我们不知道Binder的连接已经断裂,那么客户端的功能就会受到影响。为此我们可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,这个时候我们就可以给Binder设置一个死亡代理,这个时候就可以重新发起连接请求从而恢复连接。
声明一个IBinder.DeathRecipient对象,IBinder.DeathRecipient是一个接口,其内部只有一个binderDied,我们需要实现这个方法,当binder死亡的时候,系统就会回调binderDied方法,然后我们就可以移除之前绑定的binder代理并重新绑定远程服务:
//销毁代理类,重启服务
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
Log.w(TAG, "binder: deed");
mIBinderPool.asBinder().unlinkToDeath(mDeathRecipient, 0);
mIBinderPool = null;
connectBinderPoolService();
}
};
在客户端绑定远程服务成功后,给binder设置死亡代理
mIBinderPool = IBinderPool.Stub.asInterface(iBinder);
try {
mIBinderPool.asBinder().linkToDeath(mDeathRecipient,0);
} catch (RemoteException e) {
e.printStackTrace();
}
其中linkDeath的第二个参数是个标记位,我们直接设为0即可。经过上面的两个步骤就给我们的binder设置了死亡代理,当binder死亡的时候我们就可以收到通知了。另外Binder的方法isBinderAlive也可以判断Binder是否死亡。
private static final String TAG = "MessengerService";
private static class MessengerServiceHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constants.MSG_FROM_CLIENT:
Log.i(TAG, "server form client\t" + msg.getData().getString("msg"));
Messenger client = msg.replyTo;
Message replyMessage = Message.obtain(null, Constants.MSG_FROM_SERVICE);
Bundle bundle = new Bundle();
bundle.putString("reply", "收到消息,我是服务端!");
replyMessage.setData(bundle);
try {
client.send(replyMessage);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
default:
super.handleMessage(msg);
}
}
}
private final Messenger mMessenger = new Messenger(new MessengerServiceHandler());
@Override
public IBinder onBind(Intent intent) {
return mMessenger.getBinder();
}
在AndroidManifest 配置服务 android:process=”:remote”
- 客户端
private Messenger mMessenger;
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, MessengerService.class);
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mMessenger = new Messenger(service);
Message message = Message.obtain(null, Constants.MSG_FROM_CLIENT);
Bundle bundle = new Bundle();
bundle.putString("msg", "我是客户端");
message.setData(bundle);
//当客户端发送消息的时候,需要把接受服务端回复的Messenger通过Message的replytTo参数传递给服务端。
message.replyTo = mGetReplyMessenger;
try {
mMessenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
private Messenger mGetReplyMessenger = new Messenger(new MessengerHandler());
private static class MessengerHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case Constants.MSG_FROM_SERVICE:
Log.i(TAG, "receive msg from Service\t" + msg.getData().getString("reply"));
break;
default:
super.handleMessage(msg);
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(mServiceConnection);
}
```
在Messenger中进行数据传递必须将数据放入Message中,而Messenger和Message都是实现了Parcelable接口,因此可以跨进程传输。实际上:通过Messenger来传输Message,Message中能使用的载体只有what、arg1、arg2、Bundle已经replyTo。Message的另一个字段object在同一进程中是很实用的,但是在进程间通信时候,非系统的Parcelable对象无法通过object字段来传输。但可以实用Bundle,Bundle可以支持大量的数据类型。
package com.example.xiahao.myapplication;
// Declare any non-default types here with import statements
import com.example.xiahao.myapplication.Book;
import com.example.xiahao.myapplication.IOnNewBookArrivedListener;
interface IBookManager {
List getBookList();
void addBook(in Book book);
}
在AIDL文件中,并不是所有的类型都支持的,支持的类型如下:
- 基本数据类型(int 、long 、char、 boolean 、double等);
- string 和CharSequence
- List 只支持ArrayList,里面每个元素必须能够被AIDL支持
- Map 只支持HashMap ,里面每个元素必须能够被AIDL支持,包括key 和value
- Parcelable:所有实现了Parcelable接口的对象
- AIDL:所有AIDL接口本身也可以在AIDL文件中使用。
以上6中类型,其中自定义Parcelable对象和AIDL文件必须显示的import进来,不管是否和当前的AIDL位于同一文件中。另外,如果AIDL用到了自定义的Parcelable对象必须新建一个和它同名的的AIDL文件,上面我们用到了Book,所以必须创建Book.aidl.
// Book.aidl
package com.example.xiahao.myapplication;
parcelable Book;
为了方便开发,建议把所以AIDL相关类和文件全部放入同一包中,当客户端是另外一个应用时,我们可以直接把整个包复制到客户端工程中。后面会给出一个书上的例子:具体包含,基本的AIDL调用,注册解注册,权限验证,断开重连,binder连接池一个服务处理多个AIDL的调用。
- 使用ContentProvide
ContentProvide专门用来应用之间的通讯,和Messenger一样,ContentProvide底层也是Binder,虽然底层Binder但使用要比AIDL简单多,因为系统帮我们做了封装。
- 使用Socket
通过Socket进行进程间的通讯,它分为流式套接字和用户数据套接字两种,分别对应网络协议层中的TCP和UDP协议。TCP是面向连接的协议,提供稳定的双向的通讯功能,TCP的建立需要经过 “三次握手”才能完成,为提供稳定的的数据传输功能,其本身提供了超时重连机制,因此具有很高的稳定性。而UDP是无连接的,提供不稳定的单向的通讯功能,当然UDP也可以实现双向通讯功能。在性能上,UDP具有更好的效率,其缺点就是不保证数据一定能够正确传输,尤其是在网络拥塞的情况下。
- 选用合适的IPC方式
给出书中的一张表格《Android开发艺术探讨》
名称 优点 缺点 适用场景
Bundle 简单易用 只能传输Bundle支持的数据类型 四大组件的进程间通信
文件共享 简单易用 不适合高并发场景,并且无法做到进程间的即时通讯 无并发访问情形,交换简单的数据实时性不高的场景
AIDL 功能强大,支持一对多并发通信,支持实时通信 使用稍复杂,需要处理好线程同步 一对多通信且RPC需要
Messenger 功能一般,支持一对多串行通信,支持实时通信 不能很好的处理高并发情形,不支持RPC,数据通过Message进行传输,因此只能传输Bundle支持的数据类型 低并发的一对多即时通讯,无RPC需要,或者无需返回结果的RPC需求
ContentProvider 在数据访问功能很强大,支持一对多并发数据共享,可通过call方法扩展其他操作 可以理解为受约束的AIDL,主要提供数据的CRUD操作 一对多的进程间的数据共享
Socket 功能强大,可以通过网络传输字节流,并支持一对多并发实时通信 实现细节稍微有点繁琐,不支持直接的RPC 网络数据交换
代码:基本的AIDL调用,注册解注册,权限验证,断开重连,binder连接池一个服务处理多个AIDL的调用
点击下载