IPC的基础概念

134 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第34天,点击查看活动详情

1.Serializable接口

Serializable是Java提供的一个序列化接口,它是一个空接口,当我们需要使用他时只需要在类中实现这个接口并声明一个serialVersionUID即可,这个serialVersionUID不声明也可以实现序列化但是会遇到无法被反序列化的问题。原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID一致才可以被反序列化。当一个类被序列化后会把当前类的serialVersionUID写入到文件中,当需要反序列化的时候会从文件中查找serialVersionUID是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本一致,反序列化就可以成功,如果UID不一致则反序列化失败因为也许这个类有了一些改变。这个值的定义一般可以定义为1L即可。还需要注意两点:1.静态成员变量属于类不属于对象因此无法被序列化;2.用transient关键词标记的变量不参与序列化的过程

2.Pracelable接口

Pracelable是Android提供的接口,类可以直接实现这个接口即可,实现接口会增加几个新的方法:

  • writeToPracel:主要负责序列化过程通过Pracel的write来完成
  • CREATOR:主要负责饭序列化过程通过Pracel的read来完成
  • describeContents:内容描述,几乎在所有情况下返回值为0
  • Pracel:内部包装了可序列化的过程,可以在Binder中自由的传递数据

3.Serializable和Pracelable要怎么选择

  • Serializable:是Java提供的一个接口使用简单但是在序列化和反序列化的过程中需要大量的I/O操作,使用起来开销很大
  • Pracelable:是Android提供的一个接口,使用简单但是序列化和反序列化的过程消耗比较小效率高

4.Binder

1.为什么要采用Binder

  • 影响跨进程通信的传输有两点:传输效率和安全性
    • 传输效率,传统的管道队列模式采用的是内存缓冲区的方式,就是先从发送方的内存缓存区拷贝到内存开辟的缓存区中,然后再从内存开辟的缓存区中拷贝到接收方的缓存区中,这样就相当于拷贝了两次势必会影响传输效率,socket是开销大传输效率低,主要用于跨网络进程的交互
    • 安全性,Android是开放的系统因此确保终端安全是非常重要的,传统的IPC通信是没有任何措施的基本依靠上次协议。Android为每个应用分配了UID,通过UID可以鉴别身份,传统的IPC通信如果采用UID的方式鉴别终端那么也只能将UID放入数据包中,这并不安全因为会被拦截,而socket的通信方式是要暴露自己的IP和端口的,这样也不安全。
    • 根据安全性和传输效率的分析Binder的优点就展现出来了,Binder在传输效率上比较高效是因为只需要一次拷贝,性能仅次于内存共享并且采用的是C/S结构稳定性也很好,发送UID时安全性也高

2.Binder实现原理

  • Binder的实现首先申明定义的方法,然后根据方法数量定义ID,这个ID主要是为了标识在transact过程中客户端请求的具体方法,然后声明一个内部类Stub,Stub是一个Binder类,当客户端与服务端都位于同一个进程时方法调用不会经过transact进程,当位于不用进程时方法调用则需要经过transact过程,这个逻辑由Stub的内部代理类Proxy完成,这个接口的核心实现就是StubStub的内部代理类Proxy,这两个类中每个方法的作用如下:
    • DESCRIPTOR:这是Binder的唯一标识,一般就是Binder的类名
    • asInterface(android.os.IBinder obj):用于将服务端的Binder转换成客户端所需要的AIDL,如果服务端和客户端运行在同一个进程中那么返回的对象就是服务端的Stub,如果不在同一个进程那么返回的就是Stub封装后的对Stub.proxy
    • asBinder:返回当前Binder对象
    • onTransact:这个方法运行在服务端中,当客户端发起跨进程请求时,远程会通过系统底层封装后交由此方法处理,方法原型是
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)

这个方法中首先根据code判断调用的是哪个方法,然后通过_data取出目标方法所需要的参数开始执行目标方法,执行完毕后将返回值写入_reply,方法执行的流程大概就是这样,这里需要注意的是它的返回值,如果返回值是false那么就表示客户端的请求是失败的,可以利用这个特性进行权限验证。

  • Proxy#XXX(方法名):这个方法是在AIDL中定义的方法,运行在客户端,内部实现如下:
    • 创建输入型对象Parcel _data,输出型对象Parcel _reply,返回值对象result(如果有返回对象才会创建),然后把方法的参数写入_data中(如果有),调用transact方法执行远程调用并将线程挂起,然后服务器端的onTransact会被调用,直到远程调用返回,当前线程才会继续执行,并从_reply中取出返回结果,最后返回_reply数据。
  • 这里还需要注意两点:
    • 首先是当客户端的方法调用时线程会被挂起,那么就不能在UI线程发起远程请求,因为如果遇到耗时操作就会出现ANR;
    • 其次是由于服务端的Binder方法运行在Binder的线程池中,因此Binder方法不管是否耗时都要采用同步的方式实现,因为它已经运行在线程中了

3.Binder工作机制流程图

4.如何解决Binder断裂(Binder死亡)

可以通过linkToDeath给Binder配置一个死亡代理,当Binder死亡时系统就会通知客户端,客户端重新发起连接请求从而恢复连接。

5.如何配置一个死亡代理

  • 首先声明一个DeathRecipient对象,这个对象是一个接口内部只有一个方法binderDied,当Binder死亡的时候系统就会调用这个方法然后就可以移出之前绑定的binder代理并重新绑定远程服务
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mBookManager != null) {
            return;
        }
        mBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mBookManager = null;
        //TODO 这里重新绑定远程服务
    }
}
  • 其次在客户端绑定远程服务成功后,给binder设置死亡代理:
mService = IMessageBoxmanager.Stub.asInterface(binder);
//第二个参数为标志位,一般为0即可
binder.linkToDeath(mDeathRecipient, 0);
  • 上面两个步骤完成后就完成了死亡代理的设置,当Binder死亡的时候就可以收到通知了。
  • 还可以通过Binder的方法isBinderAlive也可以判断Binder是否死亡。