Android开发艺术探索 - 第2章 IPC机制

46 篇文章 0 订阅
39 篇文章 0 订阅
1.多进程模式

给四大组件指定android:process。命名方式:“:”开头,该process将是application的私有进程,最终的process名会加上package前缀;小写字母开头,该process将是全局进程,多个application可以共享该process。
多进程造成的问题:

  • 静态成员和单例模式失效
  • 线程同步机制失效
  • SharedPreferences可靠性下降
  • Application多次创建:进程->虚拟机->application->四大组件
2.IPC基础
  • Serializable
    实现了该接口即可被序列化和反序列化。通过指定serialVersionUID,保证被反序列化的数据和预期的对象类型相符:
ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;

如果不指定serialVersionUID,Java运行时会根据类的各个方面属性计算出该值。但是因为不同的运行时环境的差异,计算结果可能会不同,导致抛出InvalidClassException异常,所以建议直接指定该值。并且设置为private,因为该值对子类来说没有使用价值。
static成员属于类,不属于对象,不会被序列化;被transient修饰的成员不会序列化。

  • Parcelable
    实现了该接口,可以被Intent和Binder传递。Intent、Bundle、Bitmap实现了该接口,List、Map维护的类型实现了该接口也可以被序列化。
  • Serializable & Parcelable对比
    前者的序列化和反序列化需要大量的IO操作,开销大;后者效率高。前者多用于本地存储或者网络传输,后者主要用于内存的序列化。
  • Binder
    Binder学习指南
    • Binder通信模型

      1. ServiceManager建立(建立通信录);首先有一个进程向驱动提出申请为SM;驱动同意之后,SM进程负责管理Service(注意这里是Service而不是Server,因为如果通信过程反过来的话,那么原来的客户端Client也会成为服务端Server)不过这时候通信录还是空的,一个号码都没有。
      2. 各个Server向SM注册(完善通信录);每个Server端进程启动之后,向SM报告,我是zhangsan, 要找我请返回0x1234(这个地址没有实际意义,类比);其他Server进程依次如此;这样SM就建立了一张表,对应着各个Server的名字和地址;就好比B与A见面了,说存个我的号码吧,以后找我拨打10086;
      3. Client想要与Server通信,首先询问SM;请告诉我如何联系zhangsan,SM收到后给他一个号码0x1234;Client收到之后,开心滴用这个号码拨通了Server的电话,于是就开始通信了。
    • Binder通信过程

      1. 首先,Server进程要向SM注册;告诉自己是谁,自己有什么能力;在这个场景就是Server告诉SM,它叫zhangsan,它有一个object对象,可以执行add 操作;于是SM建立了一张表:zhangsan这个名字对应进程Server;
      2. 然后Client向SM查询:我需要联系一个名字叫做zhangsan的进程里面的object对象;这时候关键来了:进程之间通信的数据都会经过运行在内核空间里面的驱动,驱动在数据流过的时候做了一点手脚,它并不会给Client进程返回一个真正的object对象,而是返回一个看起来跟object一模一样的代理对象objectProxy,这个objectProxy也有一个add方法,但是这个add方法没有Server进程里面object对象的add方法那个能力;objectProxy的add只是一个傀儡,它唯一做的事情就是把参数包装然后交给驱动。(这里我们简化了SM的流程,见下文)
      3. 但是Client进程并不知道驱动返回给它的对象动过手脚,毕竟伪装的太像了,如假包换。Client开开心心地拿着objectProxy对象然后调用add方法;我们说过,这个add什么也不做,直接把参数做一些包装然后直接转发给Binder驱动
      4. 驱动收到这个消息,发现是这个objectProxy;一查表就明白了:我之前用objectProxy替换了object发送给Client了,它真正应该要访问的是object对象的add方法;于是Binder驱动通知Server进程,调用你的object对象的add方法,然后把结果发给我,Sever进程收到这个消息,照做之后将结果返回驱动,驱动然后把结果返回给Client进程;于是整个过程就完成了。

      另外,Android系统实现这种机制使用的是代理模式, 对于Binder的访问,如果是在同一个进程(不需要跨进程),那么直接返回原始的Binder实体;如果在不同进程,那么就给他一个代理对象(影子);我们在系统源码以及AIDL的生成代码里面可以看到很多这种实现。
      我们为了简化整个流程,隐藏了SM这一部分驱动进行的操作;实际上,由于SM与Server通常不在一个进程,Server进程向SM注册的过程也是跨进程通信,驱动也会对这个过程进行暗箱操作:SM中存在的Server端的对象实际上也是代理对象,后面Client向SM查询的时候,驱动会给Client返回另外一个代理对象。Sever进程的本地对象仅有一个,其他进程所拥有的全部都是它的代理。
      一句话总结就是:Client进程只不过是持有了Server端的代理;代理对象协助驱动完成了跨进程通信。

    • Binder的含义
      Server进程里面的Binder对象指的是Binder本地对象,Client里面的对象值得是Binder代理对象;在Binder对象进行跨进程传递的时候,Binder驱动会自动完成这两种类型的转换;因此Binder驱动必然保存了每一个跨越进程的Binder对象的相关信息;在驱动中,Binder本地对象的代表是一个叫做binder_node的数据结构,Binder代理对象是用binder_ref代表的;有的地方把Binder本地对象直接称作Binder实体,把Binder代理对象直接称作Binder引用(句柄),其实指的是Binder对象在驱动里面的表现形式。

    • AIDL封装下的Binder通信流程:
      0. 首先声明一个AIDL接口,以此编译出一个IInterface接口类型的java类。继承该类中的Stub子类,实现相应的方法,然后在server端的Service的onBind方法中返回该类的实例。该实例即为Binder实体。

      1. 当client成功绑定到了server的Service之后,在onServiceConnected方法中,根据参数中的Binder对象(对于远程server,这里一定是Binder.BinderProxy类型,即Binder引用),调用YourInterfaceName.Stub.asInterface((IBinder)service)方法,得到我们可以去执行远程方法的IInterface类型的实例。
      2. asInterface方法中,针对传入的同一进程的Binder实体,返回的实则为该Binder实体强转类型后的IInterface实例,之后的方法调用则为简单的进程内直接调用;针对传入的是不同进程的Binder引用(BinderProxy类型),则会通过代理模式,将其包裹后返回一个Stub.Proxy类型的实例。
      3. 当client通过这个Proxy去执行远程方法时,会创建两个Parcel对象,分别用于传递参数和返回值,然后执行BinderProxy#transact方法,进而通过JNI执行本地方法。该函数通过ioctl系统调用,client进程陷入内核态,调用远程方法的线程挂起,Binder驱动完成一系列操作后唤醒server进程,调用其Binder实体的onTranscat方法。
      4. 在client的BinderProxy#transact方法中,会传递一个调用号,用来指定需要调用的方法。Binder#onTranscat中则根据这个调用号,确定需要执行的本地方法,取出参数,进行计算,将参数写回。最终返回结果会传递给Binder驱动。
      5. Binder驱动唤醒client的调用线程,BinderProxy#transact继续执行,取出其中的执行结果。
      6. 一次Binder RPC完成了。
3.IPC方式
  • Bundle
    通过在Intent中添加Bundle实现不同进程间组件的数据传输。
  • 文件共享
    数据同步,并发读写问题。
  • Messenger 基于AIDL
    Messenger每次处理一个client的请求,适用于service不需要处理并发的情况。使用步骤:
    1. The service implements a Handler that receives a callback for each call from a client.
    2. The service uses the Handler to create a Messenger object (which is a reference to the Handler).
    3. The Messenger creates an IBinder that the service returns to clients from onBind().
    4. Clients use the IBinder to instantiate the Messenger (that references the service’s Handler), which the client uses to send Message objects to the service.
    5. The service receives each Message in its Handler—specifically, in the handleMessage() method.
    6. If you want the service to respond, you need to also create a Messenger in the client. When the client receives the onServiceConnected() callback, it sends a Message to the service that includes the client’s Messenger in the replyTo parameter of the send() method.
  • AIDL 基于Binder
    1. 定义接口(server)
      1. 创建.aidl文件
        AIDL支持的数据类型:Java原始数据类型、String、CharSequeue、List、Map。List和Map中必须是受支持的数据类型、其他AIDL接口或者是可序列化类型。同时另一端收到的实际类型对应的是ArrayList和HashMap。
        • 以上未列出的类型,需要使用import导入;
        • 非原始类型的参数需要指定方向标记(in/out/inout),以减少开销。原始类型为in,不能更改。
        • aidl文件中不在import和package间的注释,会包含在生成的IBinder接口中。
        • 可以在其中定义String和int常量。如const int VERSION = 1;.
        • 可以定义方法的索引值,transact在调用时即通过该值定位到具体方法。void method() = 10;
        • 使用@nullable指定可为空的参数或返回值。
      2. 实现接口
        sdk工具根据aidl文件生成一个同名的java文件。实现该文件中的Stub子类的方法,该Stub类中声明了需要实现的接口。
        • 该RPC调用是同步调用。执行该调用时,主调方会挂起。保证主调方从子线程调用,避免因此可能造成的ANR问题。
        • 引发的异常不会回传给调用方。
      3. 向客户端公开该接口
        扩展Service,实现onBind(),返回上一步创建的Binder实例。
    2. 调用IPC方法(client)
      1. 在项目 src/ 目录中加入 .aidl 文件。
      2. 声明一个 IBinder 接口实例(基于 AIDL 生成)。
      3. 实现 ServiceConnection。
      4. 调用 Context.bindService(),以传入您的 ServiceConnection 实现。
      5. 在您的 onServiceConnected() 实现中,您将收到一个 IBinder 实例(名为 service)。调用 YourInterfaceName.Stub.asInterface((IBinder)service),以将返回的参数转换为 YourInterface 类型。
      6. 调用您在接口上定义的方法。您应该始终捕获 DeadObjectException 异常,它们是在连接中断时引发的;这将是远程方法引发的唯一异常。
      7. 如需断开连接,请使用您的接口实例调用 Context.unbindService()。
    3. 一些说明
      • AIDL中的方法如果接受Bundle类型的参数,在读取其中数据之前,要设置其classloader,否则会抛出ClassNotFoundException:
        bundle.setClassLoader(getClass().getClassLoader());
      • AIDL可以实现远程回调,即server端调用client端实现的在server端定义的listener。方法步骤:
        • server端创建一个AIDL接口,类似一般的listener。
        • server端声明并实现register和unregister方法,用于client向server端注册listener。
        • client在bind到server的时候,即onServiceConnection中,register一个本地listener到server。
        • server在需要回调的时刻,直接调用远端client的listener的方法。
      • 远程回调的原理:
        重点在于server端的register方法。
        在Stub.Proxy#register中:
        _data.writeStrongBinder((((listener != null)) ? (listener.asBinder()) : (null)));
        mRemote.transact(Stub.TRANSACTION_register, _data, _reply, 0);
        
        client将本地的listener传递给register,该listener因为本身即是Binder,所以同样可以传递给远端的server使其可以访问本地的方法。
        在Stub.register#onTransact中:
        _arg0 = com.wdjhzw.androidts.aidl.IListener.Stub.asInterface(data.readStrongBinder());
        this.register(_arg0);
        
        server端注册的listener即是远端client的listener实体的引用,即BindProxy。在需要执行远程回调的时候,通过该BindProxy最终由Binder驱动执行远程方法。
      • 远程回调解除:因为server端register的是Binder引用,而client要unregister的是Binder实体,所以unregister被调用时,server不能直接解除client的listener。所以要有一种机制,保存Binder实体到Binder引用的映射,在需要unregister的时候,根据传递过来的Binder引用的Binder实体去解除之前注册的Binder引用。RemoteCallbackList就做了上面这件事:
        • register和unregister很简单,传递进listener即可;
        • server进行回调时,按照固定模板:
        int i = callbacks.beginBroadcast();
        while (i > 0) {
            i--;
            try {
                callbacks.getBroadcastItem(i).somethingHappened();
            } catch (RemoteException e) {
                // The RemoteCallbackList will take care of removing
                // the dead object for us.
            }
        }
        callbacks.finishBroadcast();
        
      • Binder线程池
        Binder实体中的方法,运行在其进程的Binder线程池中。client调用server的方法时,会被挂起。所以:
        • 因为不是主线程,其中不能直接进行UI操作。
        • 因为不是主线程,其中可以直接执行耗时操作。
        • 因为主调方会被挂起,所以无论client还是server,执行远程方法时,尽量不要在主线程中直接调用。
      • 权限验证
        Binder中提供了getCallingPid()getCallingUid()方法,在service中可以检查对端进程是否具有权限。具体来说,可以在Binder的onTransact方法中,先验证权限,验证不通过则直接返回。
  • ContentProvider 基于Binder
    ContentProvider用于应用间的复杂数据共享,如果是在应用内部,使用SQLite数据库即可。
    开发步骤:
    1. 设计原始数据。ContentProvider以两种方式提供数据:文件数据和结构化数据。前者如图片、视频,文件存储在私有空间,通过提供文件句柄供访问者使用。后者的数据一般存储在SQLite数据库中。
    2. 实现ContentProvider类。
      实现六个方法:query()/insert()/delete()/update()/getType()/onCreate()
      注意事项:
      • ContentResolver在尝试访问ContentProvider时,才会被创建。
      • 除了onCreate(),其他5个方法都运行在Binder线程池中。如果ContentProvider用于同一个进程(应用),5个方法运行在主线程。
      • CRUT操作可由多个线程同时调用,因此必须是线程安全的。如果内部维护了一个SQLite数据库,则因为其本身实现了线程同步,所以是安全的。如果有多个数据库,则需要另外处理。
      • onCreate中要避免执行长时间的操作。您只应在此方法中执行运行快速的初始化任务,并将数据库创建和数据加载推迟到提供程序实际收到数据请求时进行。
    3. 设计内容URI。
      UriMatcher
  • Socket
4.Binder连接池
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值