02 IPC机制

2019/02/28 posted in  Android开发艺术探索

2.1 Android IPC简介

  1. IPC Inter-Process Communication,含义为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。
    • 进程:一个执行单元,PC和移动设备上指一个程序或应用
    • 线程:CPU调度的最小单元,一种有限的系统资源
  2. 为什么需要多进程?
    1. 一个应用因为某些原因需要采用多进程模式来实现。比如某些模块运行在单独进程中,或者为了更多的内存空间
    2. 向其他应用获取数据

2.2 Android中的多进程模式

2.2.1 开启多进程模式

指定android:process属性开启多进程模式:

  • ":":当前应用的私有进程,其他应用的组件不可以和它跑在一个进程中;
  • ".":全局进程,其他应用通过ShareUID方式可以和他跑在同一个进程中。

Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以互相访问对方的私有数据,比如data目录,组件信息等,不管他们是否跑在同一个进程中。如果在同一个进程中,还可以共享内存数据。

2.2.2 多进程模式的运行机制

Android为每一个应用分配了独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间。

使用多进程会导致的问题:

  1. 静态成员和单例模式完全失效
  2. 线程同步机制完全失效
  3. SharedPreferences的可靠性下降
  4. Application会创建多次

2.3 IPC基础概念介绍

序列化:将对象的状态信息转换为可以存储或传输的形式的过程。

2.3.1 Serializable接口

  • serialVersionUID
    • 辅助序列化和反序列化
    • serialVersionUID相同才能反序列化成功
  • 两种变量不参与序列化:
    • 静态成员属于类,不属于对象
    • transient关键字标记的成员变量
  • 使用ObjectOutputStreamObjectInputStream进行对象的序列化和反序列化。
  • 重写writeObject()readObject()方法可以重写序列化和反序列化过程。
  • 反序列化失败的几种情况:未指定serialVersionUID;成员变量的数量、类型结构等变化时;非常规性改版:类名变化。

2.3.2 Parcelable接口

Parcelable主页方法:

  • writeToParcel:序列化
  • CREATOR:read方法,反序列化
  • describeContents:内容描述

Serializable和Parcelable:

  • 两者都可以实现序列化并可用于intent间的数据传递
  • Serializable使用简单,但是开销很大,推荐使用在存储设备或者网络传输;
  • Parcelable效率很高,主要用在内存序列化上。

2.3.3 Binder

Binder的理解:

  • 直观来说,Binder 是Android中的一个类,它实现了IBinder接口。
  • 从IPC角度来说,Binder是Android中的一种跨进程通信方式:
    • 从Android Framework角度来说,Binder是ServiceManager连接各种Manager (ActivityManager、WindowManager, 等等)和相应ManagerService的桥梁;
    • 从Android应用层来说,Binder 是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder 对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务。

Android开发中, Binder主要用在Service中,包括AIDL和Messenger。其中普通的Service的Binder不涉及进程间通讯;而Messenger的底层其实就是AIDL。

  1. 新建一个AIDL示例,SDK会自动为我们生产AIDL所对应的Binder类。创建Book.javaBook.aidlIBookManager.aidl,Build后生成IBookManager.java

  • Book.java

    public class Book implements Parcelable {
        public int bookId;
    public String bookName;
    //...
    }
  • Book.aidl

    package com.wz.testaidl;
    import com.wz.testaidl.Book;
    parcelable Book ;
  • IBookManager.aidl

    package com.wz.testaidl;
    import com.wz.testaidl.Book;
    interface IBookManager{
    List<Book> getBookList();
    void addBook(in Book book);
    }

AIDL自动生成的java文件方法说明:

  • DESCRIPTOR:Binder的唯一标识,一般用当前Binder的类名表示。
  • asInterface:将服务端的Binder对象转换成客户端所需要的AIDL接口类型的对象;
    • 客户端和服务端位于相同进程,那么此方法返回的就是服务端Stub对象本身;
    • 否则返回系统封装后的Stub.proxy对象。
  • asBinder:用于返回当前的Binder对象
  • onTransact:运行在服务端的Binder线程池中,当客户端发起跨进程通讯时,远程请求会通过系统底层封装交由此方法处理。
  • Proxy#[Method]:代理类中的接口方法。内部实现:
    1. 首先创建该方法所需要的输入型参数Parcel对象_data和输出型参数Parcel对象_reply;
    2. 然后把参数写入_data中(如果有参数的话);
    3. 接着调用transact方法来发起RPC(远程过程调用)请求,同时当前线程挂起;
    4. 然后服务端的onTransace方法会被调用直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果;
    5. 最后返回_reply中的数据。

首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在UI线程中发起此远程请求;其它,由于服务端的Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。

Binder死亡通知
Binder的两个重要方法linkToDeathunlinkToDeath

  • 通过linkToDeath可以给Binder设置一个死亡代理,当Binder死亡时,我们就会收到通知,然后就可以重新发起连接请求。
  • 声明一个DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法binderDied,实现这个方法后就可以在Binder死亡的时候收到通知了。

    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设置死亡代理:

    mService = IMessageBoxManager.Stub.asInterface(binder);
    binder.linkToDeath(mDeathRecipient,0);
    
  • 另外,通过Binder的isBinderAlive方法,也可以判断Binder是否死亡。

  • binderDiedonServiceDisconnected区别:

    • binderDied在客户端的Binder线程池中被回调
    • onServiceDisconnected在UI线程中被回调

2.4 Android中的IPC方式

2.4.1 使用Bundle

我们知道,四大组件中的三大组件( Activity、Service、 Receiver) 都是支持在Intent中传递Bundle数据的,由于Bundle实现了Parcelable 接口,所以它可以方便地在不同的进程间传输。基于这一点,当我们在一个进程中启动了另一个进程的Activity、 Service 和Receiver,我们就可以在Bundle 中附加我们需要传输给远程进程的信息并通过Intent 发送出去。当然,我们传输的数据必须能够被序列化,比如基本类型、实现了Parcellable 接口的对象、实现了Serializable 接口的对象以及一些Android支持的特殊对象,具体内容可以看Bundle这个类,就可以看到所有它支持的类型。Bundle不支持的类型我们无法通过它在进程间传递数据。

除了直接传递数据这种典型的使用场景,它还有一种特殊的使用场景。比如A进程正在进行一个计算,计算完成后它要启动B进程的一个组件并把计算结果传递给B进程,可是遗憾的是这个计算结果不支持放入Bundle中,因此无法通过Intent来传输,这个时候如果我们用其他IPC方式就会略显复杂。可以考虑如下方式:我们通过Intent启动进程B的一个Service组件(比如IntentService), 让Service在后台进行计算,计算完毕后再启动B进程中真正要启动的目标组件,由于Service也运行在B进程中,所以目标组件就可以直接获取计算结果,这样一来就轻松解决了跨进程的问题。这种方式的核心思想在于将原本需要在A进程的计算任务转移到B进程的后台Service中去执行,这样就成功地避免了进程间通信问题,而且只用了很小的代价。

2.4.2 使用文件共享

通过文件共享这种方式来共享数据对文件格式是没有具体要求的,比如可以是文本文件,也可以是XML文件,只要读/写双方约定数据格式即可。通过文件共享的方式也是有局限性的,比如并发读/写的问题,如果并发读/写,那么我们读出的内容就有可能不是最新的,如果是并发写的话那就更严重了。因此我们要尽量避免并发写这种情况的发生或者考虑使用线程同步来限制多个线程的写操作。文件共享方式适合在对数据同步要求不高的进程之间进行通信,并且要妥善处理并发读/写的问题。

当然,SharedPreferences 是个特例,SharedPreferences 是Android中提供的轻量级存储方案,它通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,每个应用的SharedPreferences文件都可以在当前包所在的data目录下查看到。一般来说, 它的目录位于/data/data/package name/shared_ prefs 目录下,其中package name表示的是当前应用的包名。从本质上来说,SharedPreferences 也属于文件的一种,但是由于系统对它的读/写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读/写就变得不可靠,当面对高并发的读/写访问,Sharedpreferences有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences

2.4.3 使用Messenger

  1. 构建服务端Service,运行在独立进程中:

    public class MessengerService extends Service {
        private static final String TAG = "MessengerService";
    @Override
    public IBinder onBind(Intent intent) {
    return mMessenger.getBinder();
    }
    private final Messenger mMessenger = new Messenger(new MessengerHandler());
    private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
    Log.e(TAG, "server receive msg: " + msg.what);
    final Messenger replyTo = msg.replyTo;
    final Message replyMsg = Message.obtain();
    replyMsg.what = 999;
    try {
    replyTo.send(replyMsg);
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    }
    }
    // AndroidManifext.xml
    <service android:name=".MessengerService"
    android:process=":remote"/>
  2. 客户端:

    1. 通过绑定服务端返回的binder创建Messenger对象,并通过这个Messenger对象向服务端发送消息。
    2. 服务端给客户端回复消息:使用Message的replyTo参数。
    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final Intent intent = new Intent(this, MessengerService.class);
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
    Log.e(TAG, "onServiceConnected: ");
    Messenger mServerMessenger = new Messenger(iBinder);
    final Message msg = Message.obtain();
    msg.what = 666;
    //赋值replyTo,服务端才可以回复消息
    msg.replyTo = mClientMessenger;
    try {
    mServerMessenger.send(msg);
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    @Override
    public void onServiceDisconnected(ComponentName componentName) {
    }
    };
    private Messenger mClientMessenger = new Messenger(new MessengerHandler());
    private static class MessengerHandler extends Handler {
    @Override
    public void handleMessage(@NonNull Message msg) {
    Log.e(TAG, "client receive msg: " + msg.what);
    }
    }
    @Override
    protected void onDestroy() {
    unbindService(serviceConnection);
    super.onDestroy();
    }
    }

    流程图如下:

总结:

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

2.4.4 使用AIDL

使用AIDL来进行进程间通信的流程,分为服务端和客户端两个方面。

  1. 服务端:

    1. 创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明(见2.3.3)
    2. 创建一个Service用来监听客户端的连接请求,在Service中实现AIDL接口。
    public class BookMangerService extends Service {
        private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    @Override
    public IBinder onBind(Intent intent) {
    return mBinder;
    }
    private IBinder mBinder = new IBookManager.Stub(){
    @Override
    public List<Book> getBookList() {
    return mBookList;
    }
    @Override
    public void addBook(Book book) {
    mBookList.add(book);
    }
    };
    }
    <service android:name=".BookMangerService"
        android:process=":remote"/>
    
  2. 客户端

    1. 首先需要绑定服务端的Service
    2. 绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就可以调用AIDL中的方法了。
    public class MainActivity extends AppCompatActivity {
        private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    final Intent intent = new Intent(this, BookMangerService.class);
    bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
    final IBookManager bookManager = IBookManager.Stub.asInterface(iBinder);
    try {
    Log.e(TAG, "bookList.size(): " + bookManager.getBookList().size());
    bookManager.addBook(new Book());
    bookManager.addBook(new Book());
    Log.e(TAG, "bookList.size(): " + bookManager.getBookList().size());
    } catch (RemoteException e) {
    e.printStackTrace();
    }
    }
    @Override
    public void onServiceDisconnected(ComponentName componentName) {
    }
    };
    @Override
    protected void onDestroy() {
    unbindService(serviceConnection);
    super.onDestroy();
    }
    }

RemoteCallbackList

在解注册的过程中,服务端无法找到之前注册的那个listener,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。

RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。Remote一CallbackList是一个泛型,支持管理任意的AIDL接口。内部有一个Map结构专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value 是Callback类型,如下所示。

public class RemoteCallbackList<E extends IInterface>{
    ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>() ;
}

其中Callback中封装了真正的远程listener。 当客户端注册listener 的时候,它会把这个listener的信息存入mCallbacks中,其中key和value分别通过下面的方式获得:

IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

当客户端解注册的时候,只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删除就可以了。当客户端进程终止后,RemoteCallbackList能够自动移除客户端所注册的listener。RemoteCallbackList内部自动实现了线程同步的功能,所以使用它来注册和解注册时,不需要做额外的线程同步工作。

使用RemoteCallbackList,有一点需要注意。我们无法像操作List一样去操作它,尽管它的名字中也带个List,但是它并不是一个List。遍历RemoteCallbackList,必须要按照下面的方式进行,其中beginBroadcastfinishBroadcast必须配对使用,哪怕我们仅仅是想要获取RemoteCallbackList的元素个数。

final int N = mListenerList.beginBroadcast();
for(int i=0;i<N;i++){   
    IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
    if(l!=null){
        //TODO
    }
}
mListenerList.finishBroadcast();

如何在AIDL中使用权限验证功能?

  1. 第一种方法:可以在onBind中进行验证,验证不通过就直接返回null。然后在AndroidMenifest中声明所需的权限。

    public IBinder onBind(Intent intent){
        int check = checkCallingOrSelfPermission("xx.xx.xx");
    if(check==PackageManager.PERMISSION_DENIED){
    return null;
    }
    return mBinder;
    }
    <uses-permission android:name="xx.xx.xx"/>
    
  2. 第二种方法:可以在服务端的onTransact中进行权限验证,如果验证失败,就直接返回false,这样服务端就不会终止执行AIDL中的方法从而达到保护服务端的效果。可以验证permission,也可以验证Uid和Pid。

    public boolean onTransact(int code,Parcel data,Parcel reply,int flags) throws RemoteException{
        int check = checkCallingOrSelfPermission("xx.xx.xx");
    if(check==PackageManager.PERMISSION_DENIED){
    return false;
    }
    String packageName = null;
    String[] packages = getPackageManager().getPackgesForUid(getCallingUid());
    if(packages!=null&&packages.length>0){
    packageName = packages[0];
    }
    if(!packageName.startWith("xx.xx")){
    return false;
    }
    return super.onTransact(code,data,reply,flags);
    }
  3. 还可以为Service指定android:permission属性等。

2.4.5 使用ContentProvider

2.4.6 使用Socket

2.5 Binder连接池

随着AIDL数量的增加,我们不能无限制地增加Service,Service 是四大组件之一一,本身就是一种系统资源。针对上述问题,我们需要减少Service的数量,将所有的AIDL放在同一个Service中去管理。

每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的 Binder 对象;对于服务端来说,只需要一个 Service就可以了,服务端提供一个queryBinder 接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。由此可见,Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service中去执行,从而避免了重复创建Service的过程。

2.6 选用合适的IPC方式