<<Android 开发艺术探索>> Chapter 2

Android IPC机制

  1. Android中使用多进程的方法只有一种给四大组件在AndroidMenifest中指定android:process属性, 除此之外没有别的方法, 也就是说我们无法给一个线程或者一个实体类指定其运行所在的进程

  2. 我们可以通过设置两个App的ShareUserID和签名都相同的方式, 使两个App不管是否跑在同一个进程中都可以共享Data文件夹下的数据, 当他们跑在同一个进程中时他们还可以共享内存
    ShareUID示例

  3. android:process属性分为 android:process:":remote"android:process:"com.shishuo.example.remote"两种, 以:开头的属于私有进程, 其他应用组件不可以和它跑在同一个进程中, 不以:开头的属于全局进程, 应用可以通过指定shareUserID来和它跑在同一个进程中.

  4. android里每一个进程都是运行在一个单独的虚拟机里, 因此进程间是不能共享内存的, 因此如果使用多进程会导致以下问题:

    • 静态成员和单例模式完全失效
    • 线程同步锁完全失效
    • SharePrefrence可靠性降低
    • Application会多次创建, 有多少个进程就会创建几次
  5. 多进程分为两种

    • 一个应用因为某些原因自身需要采用多线程模式来实现。
    • 当前应用需要向其他应用获取数据
  6. Serializable
    Java所提供的一个序列化接口,它是一个空接口,为对象提供标准的序列化和反序列化操作。使用Serializable来实现序列化相当简单,只需要在类的声明中指定一个类似下面的标识即可自动实现默认的序列化过程。

    private static final long serialVersionUID = 8711368828010083044L
    

    通过Serializable方来实现对象的序列化,如下代码:

    //序列化过程
    User user = new User(0, "jake", true);
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cache.txt"));
    out.writeObject(user);
    out.close();
    
    //反序列化过程
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("cache.txt"));
    User newUser = (User)in.readObject();
    in.close();
    

    原则上序列化后的数据中的serialVersionUID只有和当前类的serialVersionUID相同时才能够正常的被反序列化。

    serialVersionUID的详细工作机制是这样的:序列化的时候系统会把当前类的serialVersionUID写入序列化的文件中(也可能是其他的中介),当反序列化的时候系统会去检测文件中的serialVersionUID,它是否和当前类的serialVersionUID一致,如果一致就说明序列化的类的版本和当前类的版本是相同的,这个时候可以成功反序列化,否则就说明当前类和序列化的类相比发生了某些变换.

    serialVersionUID制定为1L或者采用Eclipse根据当前类结构去生成的hash值,这两者并没有本质区别。

    • 静态成员变量属于类不属于对象,所以不会参与序列化过程
    • 其次用transient关键字标记的成员变量不参与序列化过程

    Serializable详解
    Serializable代理

  7. Parcelable
    Parcelable也是一个接口,只要实现这个接口,一个类的对象就可以实现序列化并可以通过IntentBinder传递.
    Parcelable通过parcel将序列化之后的数据写入到一个共享内存中,其他进程通过Parcel可以从这块共享内存中读出字节流,并反序列化成对象.

    Parcelable详解

  8. Serializable和Parcelable优劣:

    1. 当我们序列化后的数据只在内存操作时, 首选Parcelable. 例如Activity间传递数据.
    2. 当我们序列化后的数据需要持久化的保存到存储设备或者进行网络传输时优先选用Serializable.

    是因为Serializable序列化时需要大量的调用反射而且还会产生很多临时变量, 会导致它的性能比Parcelable慢10倍.因此情况1我们优先选用Parcelable
    Parcelable在序列化时和Serializable一样也会把数据转化成字节流, 但是它向流中写入描述信息时只写入了一个类名, 而Serializable还会写入serialVersionUID等信息来作为反序列化的验证条件, 因此在2中我们选择性能更低的Serializabl, 因为它的可靠性高.

  1. Binder
    Binder详解1
    Binder详解2
    我对Binder的理解

  2. 各种IPC方式对比

    1. 使用Bundle
      Bundle实现了Parcelable接口,ActivityServiceReceiver都支持在Intent中传递Bundle数据, 传递的数据必须可以被序列化比如 基本类型 实现了Parcelable接口的对象 实现了Serializable接口的对象, 以及一些Android支持的特殊对象
      具体可以去Bundle中查看

      • 优点: 简单易用
      • 缺点: 只能传输Bundle支持的数据类型
      • 适用场景: 四大组件之间的进程通信
    2. 使用共享文件

      1. 共享文件的方式适合在对数据同步性要求不高的进程之间通信, 并且要妥善的处理并发读/写的问题.
      2. SharePrefrence虽然也是以文件的形式储存的, 但是由于系统对它在内存中做了缓存, 因此并不能把它作为共享文件来进行进程间通信.
      • 优点: 简单易用
      • 缺点: 不适合高并发的场景, 并且无法做到进程间的及时通信
      • 适用场景: 无并发访问情形, 交换简单数据, 实时性不高的场景
    3. 使用Messenger

      1. Messenger是一种轻量级的IPC方式, 它的底层也是通过AIDL实现的. Messenger只能串行的处理请求, 即服务端只能一个一个处理, 不存在并发的情况.
      2. Messenger中传递数据必须将数据放入Message中, Message中可以使用的载体只有what, arg1, arg2, BundlereplyTo, Message中的另一个字段Object在2.2以前不支持跨进程传输对象, 2.2以后也只支持系统提供的实现了Parcelable接口的对象才可以. 因此在使用Message时尽量不要将数据放到Object中.
      • 优点: 支持一对多串行通信, 支持实时通信
      • 缺点: 不能很好的处理高并发场景, 只能一个一个执行, 数据需要通过Message传输, 因此只能传输Bundle支持的数据类型, 不支持RPC(客户端调用服务器端的方法)
      • 适用场景: 低并发的一对多通信, 只需要跨进程传输数据的简单请求
    4. 使用ContentProvider

      1. ContentProvider以表格来存储数据, 并且可以包含多个表.
      2. ContentProvider对底层的数据存储方式没有要求, 可以是SQLite, 可以是文件, 甚至可以是内存中的一个对象.
      3. 要观察ContentProvider中的一个数据变化情况, 可以通过ContentResolver中的registerContentObserver方法来注册观察者.
      • 优点: 在数据源访问方面功能强大, 支持一对多并发共享数据, 可以通过call方法.
      • 缺点: 可以理解为受约束的AIDL, 主要提供数据源的CRUD操作.
      • 适用场景: 一对多进程间的数据共享
    5. 使用Socket
      套接字,分为流式套接字和用户数据报套接字两种,分别对应于网络的传输控制层中TCPUDP协议。

      1. TCP协议是面向连接的协议,提供稳定的双向通信功能,TCP连接的建立需要经过"三次握手"才能完成,为了提供稳定的数据传输功能,其本身提供了超时重传功能,因此具有很高的稳定性.
      2. UDP是无连接的,提供不稳定的单向通信功能,当然UDP也可以实现双向通信功能,在性能上,UDP具有更好的效率,其缺点是不保证数据能够正确传输,尤其是在网络拥塞的情况下。
      • 优点: 可以实现一对多并发实时通信, 可以通过网络传输字节流
      • 缺点: 实现细节有些麻烦, 不支持直接的RPC
      • 适用场景: 网络数据交换
    6. 使用AIDL
      大致流程: 首先建立一个ServiceAIDL接口, 然后在Service中实现这个AIDL接口并在ServiceonBind方法返回这个实现了AIDL接口的Binder对象, 然后客户端的ServiceConnection中接受这个Binder对象的代理BinderProxy, 之后客户端就可以通过这个BinderProxy来调取Service中的方法了.

      1. AIDL支持的数据类型:基本数据类型String和CharSequenceArrayListHashMapParcelable以及AIDL.
      2. 某些类即使和AIDL文件在同一个包中也要显式import进来.
      3. AIDL中除了基本数据类,其他类型的参数都要标上方向:inout'或者inout.
      4. AIDL接口支持方法, 不支持静态变量.
      5. 为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中.
      6. RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口.
      7. 不管是客户端还是服务端在调用另一方的方法时, 方法都会运行在Binder线程池中, 其自身线程都会处于挂起状态, 特别是客户端, 如果这个客户端线程是UI线程的话, 就会导致客户端ANR. 因此, 如果我们明确知道调用的远程方法是耗时的时候, 我们就要避免在主线程中访问远程方法.
        ServiceConnectiononServiceConnectedonServiceDisconnected方法都运行在UI线程中, 因此也要避免在这两个方法中访问远程方法.
      8. 为了避免客户端一直等待远程方法执行完成,我们就可以将AIDL接口声明为oneway,声明为onewayAIDL接口中的所有方法在调用时都不会阻塞,具体来说,调用了远程方法后,不用等着远程方法执行完毕,会立即返回继续执行后面的代码,所以正因为此特性,oneway接口下面的方法都必须是返回void类型,不能返回其他类型的数据。大部分情况下,我们一般将客户端的回调接口AIDL定义为oneway的,这样远程服务调用回调接口中的方法时不会阻塞远程服务后面代码的执行。
      9. AIDL的权限验证方法:
        • onBind方法中进行验证用户绑定验证,例子:使用Permission验证.
          步骤1: 在AndroidManifest中声明所需的权限,例如:

          <permission android:name="com.ryg.chapter_2.permission.AXXESS_BOOK_SERVICE"
                      android:protectionLevel="Normal"/>
          

          步骤2:在OnBind方法中进行权限验证。例如:

          public IBinder onBind(Intent intent){
              int check=checkCallingOrSelfpermission("com.ryg.chapter_2.permission.AXXESS_BOOK_SERVICE");
              if(check == PackageManager.PERMISSION_DEBIND){
                  return null;
              }
              return mBinder;
          }
          
        • 在服务端的onTransact方法中进行权限认证。例子:使用Permission验证。(也可以使用UidPid来做认证)

          public boolean onTransact(int Code. Parcel data, Parcel reply, int flag) throws RemoteException{
                int check=checkCallingOrSelfpermission("com.ryg.chapter_2.permission.AXXESS_BOOK_SERVICE");
                if(check == PackageManager.PERMISSION_DEBIND){
                    return false;
                }
                String packageName = null;
                String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
                if(packages != null && package.length > 0){
                   packageName = packages[0];
                }
                if(!packageName.startWith("com.ryg")){
                    return false;
                }
                return super.onTransact(code,data,reply,flags);
          }
          
  3. Binder连接池

    • 当项目规模很大的时候, 每一个AIDL创建一个Service的方法是不对的, 因为Service是系统资源, 太多的Service会使应用看起来很繁重, 所以我们最好将所有的AIDL放到一个Service中去管理.
    • 整体的工作机制是: 每个业务创建自己的AIDL接口并实现它, 这个时候业务模块间必须是解耦的, 然后每个AIDL向服务端提供它的唯一标识和实现后的Binder对象, 服务端提供一个queryBinder接口, 这个接口可以根据不同模块的唯一标识返回给我们相应的BinderProxy对象, 然后我们通过拿到的BinderProxy就可以进行远程方法调用了.
    • Binder连接池主要作用就是将每个业务模块的Binder放到一个Service中去统一管理, 从而避免了重复创建Service的过程.

推荐阅读更多精彩内容