android中的ipc机制剖析

IPC是Inter-Process Communication的缩写,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程

Android多进程模式

在Android中使用多进程只有一种方法,就是在AndroidManifest.xml中指定组件的配置信息中添加android:process

两种指定方式:

1、android:process=”:remote”(属于当前应用的私有进程)

2、android:process=”包名.remote”(属于全局进程)

当应用启动时会创建多个进程,系统为每个进程分配一个独立的虚拟机,不同的虚拟机在内存的分配上有不同的地址空间,这就导致在不同的虚拟机中访问一个类的对象会产生多分副本。

所以运行在不同进程中的四大组件,只要他们之间需要通过内存来共享数据,都会共享失败,这也是多进程所带来的主要影响。正常情况下,四大组件不可能不通过一些中间层来共享数据

一般来说,使用多进程会造成如下几个方面的问题:

1、静态成员和单例模式完全失效

2、线程同步机制完全失效

3、SharedPreferences的可靠性下降

4、Application会创建多次

这里说一下SharedPreferences可靠性下降的原因:

SharedPreferences不支持两个进程同时去执行写操作,否则会导致一定几率的数据丢失,因为SharedPreferences底层是通过读写xml文件夹实现的,并发写显然是可能出现问题的,甚至并发读写都有可能出问题

Application会创建多次的原因:

由于系统要在创建新的进程同时分配独立的虚拟机,所以这个过程其实就是启动一个应用的过程,因此,相当于系统又把这个应用重新启动了一遍,既然重新启动了,那么自然会创建新的Application

虽然我们不能直接共享内存,但是通过跨进程通信我们还是可以实现数据交互。实现跨进程通信的方式很多,比如通过Intent来传递数据,共享文件和SharedPreferences,基于Binder的Messager和AIDL以及Socket等。

IPC的基础概念

Serializable接口:

Serializable是Java所提供的一个序列号的接口,它是一个空接口,为对象提供标准的序列化和反序列化操作

实现Serializable接口时,一般来说,我们应该手动指定serialVersionUID的值,比如1L,也可以让eclipse根据当前类的结构自动去生成他的hash值,效果完成一样的,如果不手动指定serialVersionUID的值,反序列化时当前类有所改变,比如增加或者删除了某个成员变量,那么系统会重新计算当前类的hash值并把它赋值给serialVersionUID,这个时候当前类的serialVersionUID就和序列化的数据中serialVersionUID不一致,于是反序列化失败,程序就会出现crash。不过当我们删除了某个变量或增加了一些新的成员变量,这个时候我们的反序列化过程仍然能够成功,程序仍然能够最大限度的恢复数据,相反,如果我们不设置serialVersionUID的话,程序就会挂掉。当然我们还要考虑另外一种情况,如果类的结构发生了非常规性改变,比如修改了类名,修改了成员变量的类型,这个时候尽管serialVersionUID验证通过了,但是反序列化过程还是会失败,因为类的结构发生了毁灭性的改变,根本无法从老版本的数据中还原出一个新的类结构对象。

Parcelable接口:

Android提供的一种新的序列化方式,代码示例:

public class User implements Parcelable{

public int userId;

public String userName;

public boolean isMale;

public Book book;

public User(int userId,String userName,boolean isMale){

this.userId = userId;

this.userName = userName;

this.isMale = isMale;

}

@Override

public int describeContents() {

return 0;

}

@Override

public void writeToParcel(Parcel out, int flags) {

out.writeInt(userId);

out.writeString(userName);

out.writeInt(isMale?1:0);

out.writeParcelable(book, 0);

}

public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {

@Override

public User createFromParcel(Parcel in) {

return new User(in);

}

@Override

public User[] newArray(int size) {

return new User[size];

}

};

private User(Parcel in){

userId = in.readInt();

userName = in.readString();

isMale = in.readInt() == 1;

book=in.readParcelable(Thread.currentThread().getContextClassLoader());

}

}

这里说一下Parcel,Parcel内部包装了可序列化的数据,可以在Binder中自由传输,从上述的代码中可以看出,在序列化的过程中需要实现的功能有序列化、反序列化和内容描述。序列化功能由writeToParcel方法来完成,最终是通过Parcel中的一系列write方法来完成的。反序列化功能由CREATOR来完成,其内部标明了如何创建序列化对象和数组,并通过Parcel的一系列read方法来完成反序列化的过程。内容描述功能由describeContnets方法来完成,几乎所有情况下这个方法都应该返回0,仅当当前对象中存在文件描述符时,此方法返回1,。需要注意的是,在User(Parce in)方法中,由于book是另一个可序列化的对象,所以他的反序列化过程需要传递当前线程的上下文类加载器,否则会报无法找到类的错误。

Serializable是java中的序列化接口,其使用起来简单但是开销很大,序列化和反序列化过程需要大量的I/O操作。而Parcelable是Android中序列化方式,因此更适合用在Android平台上,它的缺点就是使用起来稍微有点麻烦,但是它的效率很高,因此我们首先Parcelable。Parcelable主要用在内存序列化上,通过Parcelable将对象序列化到存储设备中或将对象序列化后通过网络传输也都可以,但这个过程会稍显复杂,因此这两种情况下建议大家使用Serializable。

Android中的IPC方法

Bundle

使用Bundle:四大组件中的三大组件(Activity,Service,Receiver)都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable接口,所以它可以方便的在不同进程间传输。

文件共享

使用文件共享:这是一种不错的进程间通信的方式,两个进程通过读写同一个文件来交换数据,除了可以交换一些文本信息外,我们还可以序列化一个对象到文件系统中的同时从另一个进程中恢复这个对象。文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读写的问题。当然,SharedPreferences是个特例,众所周知,SharedPreferences是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,从本质上来说,SharedPreferences也属于文件的一种,但是系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发的读写访问,SharedPreferences与很大的几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。

Messenger

使用Messenger:通过它可以在不同的进程中传递Messgae对象,在Message中放入我们需要传递的数据,就可以轻松的实现数据的进程间传递了。Messenger是一种轻量级的IPC方案,它的底层实现是AIDL。实现一个Messenger有如下几个步骤:

1、服务端进程

我们需要在服务端创建一个Service,来出来客户端的连接请求,同时创建一个Handler并通过它来创建一个Messenger对象,然后在Service的onBind中返回这个Messenger对象底层的Binder即可。

2、客户端进程

首先要绑定服务端的Service,绑定成功后服务端返回的IBinder对象创建一个Messenger,通过这个Messenger就可以向服务端发送消息了,发送消息类型为Message对象。如果需要服务端能够回应客户端,我们还需创建一个Handler并创建一个新的Messenger,并把这个Messenger对象通过Message的replyTo参数传递给服务端,服务端通过这个replyTo参数就可以回应客服端。

Messenger的工作原料图:

AIDL

使用AIDLMessenger是以串行的方式处理客户端发来的消息,如果有大量的消息同时发送到服务端,服务端只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,它就无法实现,但是我们可以使用AIDL来实现跨进程的方法调用。

服务端:创建一个Service用来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这AIDL文件中声明,最后在Service中实现这个AIDL接口即可。

客户端:首先需要绑定服务端的Service,将服务端返回的Binder对象转成AIDL接口所属的类型,调用AIDL中的方法。

ADIL文件中,支持的数据类型:

l 基本数据类型(int ,long ,char,boolean)

l StringCharSequence

l List:只支持ArrayList,里面的每个元素都需被AIDL支持

l Map:只支持HashMap,里面的每个元素都需被AIDL支持

l Parcelable:所有实现了Parcelable接口的对象

l AIDL:所有AIDL接口本身也可以在AIDL接口中使用

IBookManager.aidl中的代码

package com.jay.aidl;

Import com.jay.aidl.Book;

Interface IBookManager{

List<Book> getBookList();

void add(in Book book);

}

Book.aidl中的代码

package com.jay.aidl;

parcelable Book;

AIDL使用的注意事项:

A、在IBookManager.aidl文件中使用到了Book对象,该对象必须实现Parcelable接口,即使是在同一个包下,也要通过import语句导入到aidl文件中,同时还需定义Book.aidl文件,声明Book的类型,AIDL中除了基本数据类型,其他数据类型的参数要标上方向:

Inout或者inout,in表示输入型参数,out表示输出型参数,inout表示输入输出型参数,不能一概的使用out或者inout,因为这在底层实现是有开销的。最后,AIDL接口中只支持方法,不支持声明静态变量。

B、为了方便AIDL的开发,把AIDL文件和相关的类放在同一包下,开发客户端时可以直接拷贝过去,这些文件在客户端中的包名要和服务端中的包名相同,因为客户端需要反序列化服务端中何AIDL接口相关的类,如果类的完整路径不一致,就无法成功反序列化。

C、我们在服务端使用List集合操作数据时,使用CopyWriteArrayList,首先该类支持并发读/写,其次AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接时,会存在多线程访问的情形。AIDL中只支持ArrayList,之所以可以使用CopyWriteArrayList,是因为在Binder中会按照List的规范去访问数据并最终形成一个ArrayList传递给客户端。

D、有一种情况,就是当我们需要服务端给我们返回信息时,我们需要定义一个新的aidl文件,通过观察者模式实现,然后在客户端创建Stub类,把该类传输到服务端保存,并在客户端结束时,删除服务端中的对象,当客户端向服务端传递对象时,Binder会把客户端传递过来的对象重新转化为一个新的对象,因为对象时不能跨进程直接传输的,对象的跨进程传输的本质上都是反序列化的过程。解决办法就是使用RemoteCallbackList,它是系统专门提供的用于删除跨进程listener的接口,它是个泛型,支持管理任意类型的AIDL接口,定义如下:

Public class RemoteCallbackList<E extends IInterface>

它的工作原理很简单,内部有一个Map结构专门用来保存所有的AIDL回调,MapkeyIBinder类型,valueCallback类型,如下:

ArrayMap<IBinder,Callback> mCallbacks = new ArrayMap<IBinder,Callback>();

虽然跨进程传输客户端的同一个对象会在服务端生成不用的对象,但这些生成的对象有一个共同点,那就是它们底层的Binder对象是同一个,所以可以遍历删除该listener,找到该listener对于的Binder对象,当客户端进程终止后,它还能够自动移除客户端所注册的listener。另外,RemoteCallbackList内部自动实现了线程同步的功能。不过RemoteCallbackList并不是一个List,它的遍历方式也比较特别,如下:

final int N = RemoteCallbackList对象名.beginBroadcast();

for(int i =0; i < N ;i++){

每个接口对象=RemoteCallbackList对象名.getBroadcastItem(i);

}

RemoteCallbackList对象名.finishBroadcast();

E、我们知道,客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间阻塞,如果这个客户端线程是UI线程的话,就会导致客户端ANR,因此要开启线程去异步执行任务

F、我们还需要做一件事,就是Binder是可能意外死亡的,往往由于服务端进程意外停止了,这时我们需要重新连接服务。有两种方法,第一种方法就是给Binder设置DeathReipient监听,当Binder死亡时,我们可以收到binderDied方法的回调,在binderDied方法中我们可以重新连接远程服务,另一种方法是在onServiceDisconnected中重新连接远程服务。这两种方法的区别就在于,onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。

到此为止,我们已经对AIDL有了一个系统的认识,但是还差最后一步,如何在AIDL中使用权限验证功能

第一种方法,我们可以在onBind方法中验证,验证不通过就直接返回null,验证失败的客户端就无法绑定服务端,验证的方式很多,我们可以在AndroidMenifest.xml文件中声明permission验证:如下:

AndroidMenifest.xml文件中:

<permission 

    android:name="自己定义"

    android:protectionLevel="normal"

    />

如果想要在自己的应用中绑定,在AndroidMenifest.xml文件中,如下声明:

 <uses-permission 

          android:name="zheshizhijidingyidebaoming"

          />

在服务端的代码中:

@Override

public IBinder onBind(Intent intent) {

int check = checkCallingOrSelfPermission("在xml中文件中声明的permission");

if(check == PackageManager.PERMISSION_DENIED){

return null;

}

return mBinder;

}

第二中方法就是在服务端的onTransact方法中进程验证,如果验证失败就返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果,验证的方式和第一种一样。

ContentProvider

ContentProviderAndroid中提供的专门用于不同应用间的进行数据共享的方式,和Messneger一样,ContentProvider的底层实现同样也是Binder,可以通过ContentProvider提供的queryupdateinsertdelete方法进行数据共享,但是得结合SQLite数据库进行使用。创建一个SQLiteDatabase的连接,其内部对数据库的操作是有同步处理的,但是如果如果通过多个SQLiteDatabse对象来操作数据库就无法保证线程同步,因为SQLiteDatabase对象之间无法进行线程同步。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值