Android开发艺术探索2

Android开发艺术探索2

该系列文章为《Android开发艺术探索》读书笔记,仅作为学记录,勿喷。

Android IPC简介

  1. IPC是Inter-Process Communication 的缩写,含义为进程间通信,是指两个进程之间进行数据交换的过程。
  2. 任何一个操作系统都需要有相应的IPC机制,Linux上可以通过命名通道、共享内存、信号量等来进行进程间通信。Android系统使用了Binder机制来实现IPC,Socket也可以实现
  3. 多进程的使用场景:
    一个应用因为某些原因自身需要采用多进程的模式来实现
    当前应用需要向其他应用获取数据,由于是两个应用,必须采取跨进程方式获取所需数据,ContentProvider也是一种进程间通信。

Android中的多进程模式

  1. 在Android中使用多进程只有一种方法,就是给四大组件在AndroidMenifest中制定 android:process 属性,无法给一个线程或者一个实体类指定其运行时所在的进程(有一种非常规的方法,通过JNI在native层fork一个新的进程,该方法属于特殊情况,也不是常用的创建多进程的方式)
  2. 默认进程的进程名是包名packageName,进程名以 : 开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以 : 开头的进程属于全局进程,其他应用通过 ShareUID 方法可以和它跑在同一个进程中。
android:process=":xyz" //进程名是 packageName:xyz
android:process="aaa.bbb.ccc" //进程名是 aaa.bbb.ccc
  1. Android系统会为每个应用分配一个唯一的UID,具有相同UID的应用才能共享数据。两个应用通过ShareUID跑在同一个进程中是有要求的,需要这两个应用有相同的ShareUID并且签名相同才可以。在这种情况下,它们可以相互访问对方的私有数据,比如data目录、组件信息等,不管它们是否跑在同一个进程中。如果它们跑在同一个进程中,还可以共享内存数据,它们看起来就像是一个应用的两个部分。
  2. Android为每个进程都分配一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,导致在不同虚拟机中访问同一个类的对象会产生多份副本(在当进程修改类的值只会影响当前进程,对其它进程不会造成影响)
  3. 所有运行在不同进程的四大组件,只要它们之间通过内存的来共享数据,都会共享失败。
  4. 使用多进程会造成一下问题:
    静态成员和单例模式完全失效。//不同内存
    线程同步机制完全失效 //锁不是同一个对象
    SharePreference的可靠性下降。//不支持两个进程同时读写,不支持并发读写
    Application会多次创建 //组件在新的进程分配独立虚拟机,应用重启,新建Application,不同进程中的组件属于两个不同的虚拟机和Application

IPC基础概念介绍

  1. Serializable是java提供的一个序列化接口,踏实一个空接口,为对象提供标准的序列化和反序列化操作
  2. serialVersionUID是一串long型数字,主要是用来辅助序列化和反序列化的,原则上序列化后的数据中的serialVersionUId只有和当前类的serialVersionUId相同才能够正常地被反序列化。(serialVersionUID不是必须的,不声明也能序列化,但是会对序列化过程产生影响)

    serialVersionUID详细工作机制:
    序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其它中介),当反序列化的时候系统回去检测文件中的serialVersionUID,看它是否和当前类的serialVersionUID一致,这时候可以成功反序列化;否则说明当前类和序列化的类相比发生了某些变换,无法正常反序列化

  3. 给serialVersionUID指定为1L或采用自动使用当前类结构生成hash值,两者没有本质区别。我们应该手动指定serialVersionUId的值。

  4. 静态成员变量属于类不属于对象,不会参与序列化;用transient关键字标记的成员变量不参与序列化
  5. Parcel内部包装了可序列化的数据,可以在Binder中自由的传输。可以直接序列化的有Intent、Bundle、Bitmap以及List和Map等等
  6. Serializable是java中的序列化接口,使用开销很大,序列化反序列化需要大量I/O操作,建议用在对象序列化到存储设备和序列化后通过网络传输的情况;Parcelable是android中的序列化方式,效率很高,主要用在内存序列化。
  7. Binder是Android中的一个类,实现了IBinder接口,从IPC角度来说,Binder是Android中的一种跨进程通信方式,还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式linux没有;从framework角度说,Binder是Servicemanager连接各种Manager(ActivityManager、WindowManager等)和ManagerService的桥梁;从应用层来说,Binder是客户端和服务端进行通信的媒介,当bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务
  8. Android开发中,binder主要用在service中,包括AIDL和messenger,其中普通service中的Binder不涉及进程间通信,而messenger底层其实是aidl
  9. 我们所写的aidl文件会在gen目录下自动生成对应的java文件,这个类中继承了IInterface这个接口,同时它自己也还是个接口。里面的有个内部类Stub,这个Stub就是Binder类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的transact过程,当两者位于不同进程,方法调用走transact过程,这个逻辑由Stub的内部代理类Proxy来完成。

    DESCRIPTOR
    Binder的唯一标识,一般用当前Binder的类名表示。
    asInterface(android.os.IBinder obj)
    用于将服务端的Binder对象转换成客户端所需要的AIDL接口类型对象,这种转换过程是区分进程的,如果客户端和服务端用于同一进程,那么此对象返回就是服务端的Stub对象本身,否则返回的就是系统封装后的Stub.proxy
    asBinder
    此方法用于返回当前Binder对象
    onTransact
    这个方法运行在服务端中的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。这个方法的原型是public Boolean onTransact(int code, Parcelable data, Parcelable reply, int flags)服务端通过code可以知道客户端请求的目标方法,接着从data中取出所需的参数,然后执行目标方法,执行完毕之后,将结果写入到reply中。如果此方法返回false,说明客户端的请求失败,利用这个特性可以做权限验证(即验证是否有权限调用该服务)。
    Proxy#[Method]
    代理类中的接口方法,这些方法运行在客户端,当客户端远程调用此方法时,它的内部实现是:首先创建该方法所需要的输入型Parcel对象_data、输入型Parcel对象_reply和返回值对象List,然后把方法的参数信息写入到_data中,接着调用transact方法来发起RPC(远程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply中取出RPC过程的返回结果,最后返回_reply中的数据。

  10. 当客户端发起远程请求,当前线程观其知道服务端进程返回数据,如果一个远程方法很好使,那么不能再UI线程中发起此远程请求;由于服务端Binder方法运行在Binder的线程池中,所以Binder方法不管是否耗时都应该采用同步的方式去实现,因为已经运行在一个线程池中了。下图是Binder工作机制
    Android开发艺术探索2_第1张图片

  11. AIDL文件并不是实现Binder的必需品。手写Binder必须在服务端建立一个xxmanagerImpl的对象并在Service的onBind方法中返回。AIDL的本质是系统提供的一种快捷实现Binder的工具。
  12. Binder的两个重要方法linkToDeathunlinkToDeath
    Binder运行在服务端,如果由于某种原因服务端异常终止,这时候服务端Binder连接断裂(称之为Binder死亡)会导致客户端的远程调用失败。所以Binder提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath方法可以给Binder设置一个死亡代理,当Binder死亡的时候客户端就会收到通知,然后就可以重新发起连接请求从而恢复连接。(isBinderAlive可以判断Binder是否死亡)

    给Binder设置死亡代理

    • 1.声明一个DeathRecipient对象,DeathRecipient是一个接口,其内部只有一个方法bindeDied,实现这个方法就可以在Binder死亡的时候收到通知了。
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        if (mRemoteBookManager == null) return;
        mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mRemoteBookManager = null;
        // TODO:这里重新绑定远程Service
    }
};
  • 2.在客户端绑定远程服务成功之后,给binder设置死亡代理
mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);

Android中的IPC方式

  1. 使用Bundle:Bundle实现了Parcelable接口,方便在不同进程传输。除了数据直接传递的使用场景,还有一种特殊场景。a进程计算完的结果给b进程去启动组件,但结果不支持放入Bundle,这时可以吧a进程的计算转到b进程的后台Service去执行,这样就避免了进程间通信问题
  2. 使用文件共享:可以成功恢复之前存储的对象内容,但本质是两个对象。没有具体的格式要求,读写约定数据格式。适合在对数据同步要求不高的进程之间通信,需要妥善处理并发读写问题。

    sharepreference是一个特例,虽然它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。

  3. 使用Messenger:是一种轻量级的IPC方案,它的底层实现是AIDL。Messenger是以串行的方式处理请求的,即服务端只能一个个处理,不存在并发执行的情形。Messenger中进行数据传递必须将数据放入Message中,它们都实现了Parcelable接口,载体只有what,arg1.arg2,Bundle及replyto。message的另一个字段object在同一进程很实用,但在进程间通信,2.2以前object不支持跨进程传输,2.2以后系统提供的实现了Parcelable接口的对象才能传输。意味着自定义的Parcelable对象是无法传输的。还好有Bundle。下图是Messenger工作原理图
    Android开发艺术探索2_第2张图片

    • 客户端发送消息给服务端(单向通信)
      服务端:
        (1)创建一个handler对象,并实现handlemessage方法,用于接收来自客户端的消息,并作处理 .
        (2)创建一个messenger(送信人),封装handler .
        (3)messenger创建一个IBinder对象,通过onBind返回给客户端
      客户端:
        (1)在activity中绑定服务
        (2)创建ServiceConnection并在其中使用 IBinder 将 Messenger实例化
        (3)使用Messenger向服务端发送消息
        (4)解绑服务
        (5)服务端中在 handleMessage() 方法中接收每个 Message
      这样,客户端并没有调用服务的”方法”。而客户端传递的”消息”(Message 对象)是服务在其 Handler 中接收的。
      上面实现的仅仅是单向通信,即客户端给服务端发送消息。
    • 客户端与服务端双向通信
      如果我需要服务端给客户端发送消息又该怎样做呢?
      其实,这也是很容易实现的,下面就让我们接着上面的步骤来实现双向通信吧:
        (1)在客户端中创建一个Handler对象,用于处理服务端发过来的消息。
        (2)创建一个客户端自己的messenger对象,并封装handler。
        (3)将客户端的Messenger对象赋给待发送的Message对象的replyTo字段。
        (4)在服务端的Handler处理Message时将客户端的Messenger解析出来,并使用客户端的Messenger对象给客户端发送消息。
      这样就实现了客户端和服务端的双向通信了。
      注意:
        注:Service在声明时必须对外开放,即android:exported=”true”
  4. 使用AIDL:如果有大量消息同时发送到服务器,我们需要AIDL来实现跨进程的方法调用。

    • 服务端:创建Service来监听客户端的连接请求,然后创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明,最后在service中实现这个AIDL接口。
    • 客户端:绑定服务端的Service,将服务端返回的Binder对象装换成AIDL接口所属的类型,接着就可以调用AIDL中的方法了
    • AIDL支持的数据类型:基本数据类型、String和CharSequence、ArrayList、HashMap、Parcelable以及AIDL
    • 某些类即使和AIDL文件在同一个包中也要显式import进来
    • AIDL文件中用到了自定义的Parcelable对象,那么必须新建一个和它同名的AIDL文件,并在其中声明它为Parcelable类型
    • AIDL中除了基本数据类,其他类型的参数都要标上方向:in、out或者inout
    • AIDL接口中支持方法,不支持声明静态变量
    • 为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中
    • 当客户端要解除注册监听时,发现无法解除,由于进程间通信本质是序列化和反序列化,虽然listner内容相同,但本质是两个对象,这时就出现了RemoteCallbackList
    • RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。
      public class RemoteCallbackList
      它的工作原理很简单,内部有一个map来保存所有的aidl回调,如下
      ArrayMap mCallbacks = new ArrayMap();
      mCallabck中存的是真正的客户端注册时的listener。key,value获得方式
      IBinder key = listener.asBinder();
      Callback value = new Callback(listener,cookie);

    虽说多次跨进程传输客户端的同一个对象会在服务端生成不同对象,但是这些生成的对象它们底层有个相同的Binder对象。解注册时遍历对象找相同Binder的服务端listener并删除。RemoteCallbackList可以在客户端进程终止后自动移除客户端注册的listener。它内部实现了线程同步的功能,所以注册和解注册不用做额外操作
    RemoteCallbackList并不是List,遍历时beginBroadcast和finishBroadcast必须成对使用

    final int N = mListenerList.beginBroadcast();
     for(int i=0;iif(l!=null){
           //TODO handle l
     }
    }
    mListenerList.finishBroadcast();
    • 当客户端调用远程服务方法时,被调用的方法运行在服务端的Binder线程池中,同时客户端挂起,这时如果客户端的方法执行耗时会ANR,解决方式避免在UI线程中操作。
    • Binder死亡时会收到binderDied,也可以调用onServiceDisconnected。区别在于:onServiceDiconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程池中被回调。也就是在binderDied中不能访问UI
    • 如何在AIDL中使用权限验证?
      (1)在onBind中验证,验证不通过就直接返回null,失败的客户端无法绑定服务,方式比如permission验证
      (2)可以在服务端的onTransact方法中验证,如果失败返回false
  5. 使用ContentProvider

    • ContentProvider是Android中提供专门进行不同应用进行数据共享的方式,和Messenger一样底层实现也是Binder
    • ContentProvider主要以表格的形式来组织数据,并且可以包含多个表
    • ContentProvider还支持文件数据,比如图片、视频等,系统提供的MediaStore就是文件类型的ContentProvider
    • ContentProvider对底层的数据存储方式没有任何要求,可以是SQLite、文件,甚至是内存中的一个对象都行;
    • 要观察ContentProvider中的数据变化情况,可以通过ContentResolver的registerContentObserver方法来注册观察者;
  6. 使用Socket
    Socket是网络通信中“套接字”的概念,分为流式套接字和用户数据包套接字两种,分别对应网络的传输控制层的TCP和UDP协议。

Binder连接池

  1. 当项目规模很大的时候,创建很多个Service是不对的做法,因为service是系统资源,太多的service会使得应用看起来很重,所以最好是将所有的AIDL放在同一个Service中去管理。整个工作机制是:每个业务模块创建自己的AIDL接口并实现此接口,这个时候不同业务模块之间是不能有耦合的,所有实现细节我们要单独开来,然后向服务端提供自己的唯一标识和其对应的Binder对象;对于服务端来说,只需要一个Service,服务端提供一个queryBinder接口,这个接口能够根据业务模块的特征来返回相应的Binder对象给它们,不同的业务模块拿到所需的Binder对象后就可以进行远程方法调用了。
    Binder连接池的主要作用就是将每个业务模块的Binder请求统一转发到远程Service去执行,从而避免了重复创建Service的过程。
  2. 作者实现的Binder连接池BinderPool的实现源码,BinderPool是一个单里实现,同一个进程只会初始化一次,提前对BinderPool可以优化程序体验;BinderPool有短线重连机制,当远程服务以外终止,BinderPool会重新连接,这时如果业务模块中的Bidner调用出现异常需要手动重新获取Binder对象;BinderPool能够极大提高AIDL的开发效率,避免大量service创建,建议在AIDL开发工作中引入BinderPool机制。
    Android开发艺术探索2_第3张图片

选用合适的IPC方式

Android开发艺术探索2_第4张图片

你可能感兴趣的:(读书笔记)