0%

Android IPC 学习笔记

IPC: Inter-Process Communication

不同系统下的不同实现方式:

  • Windows: 剪贴板/管道/油槽
  • Linux: 命名管道/共享内容/信号量
  • Android: Binder/Socket

Android中的多进程模式

使用场景:保活/推送/扩大内存空间/ContentProvider

如何开启?

  • 在Manifest中设定四大组件的android:process属性
        _ `:$remote_name`私有进程
        _ `$package_name.$remote_name`共有进程,可以通过ShareUID的方式与其他应用共享进程
            \* 需要两个应用拥有相同的ShareUID以及应用签名一致
  • 通过JNI的方式在Native层去fork一个新的进程

多进程模式下的运行机制

Android对于每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,通常来讲,使用多进程会造成如下几方面问题:

  • 静态成员和单例模式失效——在不同的虚拟机中访问同一个类对象会产生多份副本。
  • 线程同步机制完全失效
  • SharedPreference可靠性下降——SP底层是XML实现,并发读写都有可能出现问题。
  • Application会多次创建——新组建运行在新的进程中时,由于系统在创建新进程的时候会同时分配独立的虚拟机,相当于也是将应用重新启动了一次。

IPC基础概念简介

Serializable接口

Java提供的序列化方法,通过ObjectInputStreamObjectOutputStream来对对象进行序列化及反序列化。

Serializable接口是一个空接口,只需要声明一个serialVersionUID即可,事实上这个字段不声明也是能够完成序列化/反序列化的。这个字段的作用在于_标识类的版本变化_,在序列化的过程中,会将这个值写入序列化的结果中,在反序列化时,会将写入值与反序列化对象的当前serialVersionUID值进行对比,如果相同那么能够成功反序列化,如果不一致则抛出异常。
如果该值不经手工指定,那么将按照该类的当前_成员变量_的构成计算Hash作为serialVersionUID值,在这种情况下,如果序列化/反序列化中间该类成员发生了改变,将导致无法完成序列化。因此为了保证序列化的成功率,最好手动指定serialVersionUID

Parcelable接口

Android系统中特有的序列化接口。需要实现的接口包括:

  • 一个CREATOR常量作为反序列化方法,在该方法中定义通过序列化数据生成对象的构造方法。
  • 一个describeContents内容描述符,通常情况下都为0,当且仅当当前内容含有文件描述符返回1
  • 一个writeToParcel方法作为序列化方法

系统已经定义好了许多可以直接序列化的方法。如BitmapBundle以及Intnet等。

那么,如何选择使用何种序列化方法?

  • Serializable使用简单方便,但是需要大量的IO操作,开销较大,适合在进行数据本地化或数据序列化之后进行网络传输时使用。
  • Parcelable定义较为复杂,但性能优秀,效率较高,是Android推荐的序列化方式,主要使用在内存序列化上。

Binder

Binder既可以理解为Android中的一个实现了IBinder接口的类,也可以理解为一种虚拟的物理设备,他的驱动是/dev/binder。Binder是连接ServiceManager连接各个ManagerService之间的桥梁。最常用到的地方在Service的使用中,在bindService的时候会返回一个包含服务端业务调用的Binder对象,通过Binder对象来完成客户端对于服务端业务的调用。

不同进程内的Binder很简单,稍微复杂一些的Binder通常使用AIDL或者Messenger完成,而Messenger的底层也是由AIDL实现的,因此了解AIDL的使用方法、原理实际上对于理解Binder的上层原理是有帮助的。

定义AIDL文件并不难,没有太多额外的语法需要注意,只需要按照自己的业务需求,像通常定义接口一样定义AIDL接口即可,注意所有自有的类都必须显式声明即可。在AS中的AIDL文件会被统一收拢到同一个目录下,但是这里有一个不知道是bug还是怎么回事的问题……当你已经定义好一个自定义类之后,IDE不允许再次声明同样名字的.aidl文件。这里没发现有特别好的解决方案,暂时只能先声明一个不同名字的文件,然后再改回去。

AIDL文件没什么好关心的,但是通过AIDL文件生成的.class文件则可以好好研究下:

AIDL生成的实际上是一个接口文件,在这个接口类中首先是定义了AIDL文件中规定的方法,然后生成了一个接口内部的抽象类Stub,这个抽象类实现了外层的接口,并继承自Binder,这个内部抽象类就是在之后使用时在Service内部初始化并实现接口方法的Binder对象,听过bindService返回给调用者,调用者使用Binder调用远端方法。

在这里需要注意的是,与日常使用的进程内利用Binder调用远端代码不同的是,这里在获取Binder对象时调用的方法是:

public void onServiceConnected(ComponentName name, IBinder service) {
    mITaskConsumer = ITaskConsumer.Stub.asInterface(service);
    mIsRemoteBond = true;
}

这里asInterfase(service)是完成整个跨进程调用的关键。我们继续看看这个方法里面做了什么:

public static com.kyangc.ipcdemo.ITaskConsumer asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.kyangc.ipcdemo.ITaskConsumer))) {
        return ((com.kyangc.ipcdemo.ITaskConsumer) iin);
    }
    return new com.kyangc.ipcdemo.ITaskConsumer.Stub.Proxy(obj);
}

这里obj是通过bindService返回的Binder对象,这时会在本地按照DESCRIPTOR定义的特征值去查找进程内的服务,如果查到了,那么说明这个Binder是个本地服务,那么直接将这个Binder作为处理业务的对象返回给调用者;反之如果没有查到,说明这个是一个远程的服务,这个时候会用Proxy对这个Binder进行包装,然后返回给用户。

下面继续看这个Proxy做了什么事:

首先是这个Proxy是继承于我们定义好的接口的:

private static class Proxy implements com.kyangc.ipcdemo.ITaskConsumer

然后在执行接口方法时,调用的是这一段代码:

@Override
public void consume(com.kyangc.ipcdemo.Task task) 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 ((task != null)) {
            _data.writeInt(1);
            task.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }
        mRemote.transact(Stub.TRANSACTION_consume, _data, _reply, 0);
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

这里看上去多,但实际上只是一堆模板代码。核心步骤实际上只有三步:

  • 将传入参数写入_data
  • 调用mRemote.transact(方法序号,传入序列化数据,传出序列化数据,标志位)
  • 写入返回值,处理异常,回收序列化变量

transact方法则是调用了/dev/binder提供的方法,将调用什么方法、使用什么参数、返回什么参数等内容传到了远程服务端。

那么远程服务端收到/dev/binder的提醒之后会做什么呢?我们继续看Stub中的一段代码:

@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_consume: {
            data.enforceInterface(DESCRIPTOR);
            com.kyangc.ipcdemo.Task _arg0;
            if ((0 != data.readInt())) {
                _arg0 = com.kyangc.ipcdemo.Task.CREATOR.createFromParcel(data);
            } else {
                _arg0 = null;
            }
            this.consume(_arg0);
            reply.writeNoException();
            return true;
        }
    }
    return super.onTransact(code, data, reply, flags);
}

/dev/binder把信息发到远端时,会调用到Stub中的onTransact方法,这个方法会接下来继续分析客户端调用了什么方法,从_data中取出参数,计算后将结果放到_reply中。注意这里有一个返回值,如果返回为false,那么代表该次请求失败,反之成功,这个特性可以被利用于权限验证。

这里需要注意的一点是,客户端的线程会在请求后被挂起,之后所有的操作均发生在各自Binder的线程池中,直到请求返回才停止主线程的阻塞。因此尽量不要再主线程发起远端的业务请求,尽可能在新的线程中发起业务需求通知。

下图简单描述了一下这个过程:

Android中其他IPC方式

Bundle

非常常用的跨进程传输数据的方式(Intent),只支持Parcelable数据的传输。

文件共享

必须手动控制并发,很容易出问题,效率较低。例如Sharedpreference,系统对于SP是有一定的缓存策略的,在多进程模式下会变得非常不可靠。

Messenger

一种基于AIDL的轻量级IPC手段。底层由Handler+AIDL实现,服务端存在一个固定宽度为1的消息队列,因此没有并发的问题。通过指定Message中的replyTo字段来完成服务端的消息转发。

ContentProvider

底层实现依然依赖于Binder。实现自定义的ContentProvider只需要实现对应的CRUD方法以及一个生命周期方法和一个返回MIME类型的getType方法即可。ContentProvider的储存方式是任意的,可以使用数据库、文件甚至是内存中的一个对象。

这里需要注意的是,ContentProvider的OnCreate方法是调用在UI线程中的,因此需要注意不要放入太多耗时操作。而其他CRUD方法则是运行在Binder线程池中,因此是存在多线程并发访问的可能性的,ContentProvider本身没有对这些方法做处理,需要用户自己管理并发。

Socket

Socket运行在TCP/UDP层上,本身支持传输任意字节流,因此也可以被用于IPC。