Android 开发艺术探索 - 读书笔记之第二章 IPC 机制

2.1 简介

  • 全称 Inter-Process Communication,跨进程通信。Android 下可以通过 Binder,Socket 等方式实现
  • 进程与线程的概念
    • 进程,一个程序或应用,可以包含多个线程
    • 线程,CPU 调度的最小单元
  • Android 下的多进程场景
    • 一个应用有些模块需要运行在单独进程
    • 通过多进程获取多份内存空间
    • 当前应用要向其他应用获取数据,例如 ContentProvider

2.2 Android 中的多进程模式

2.2.1 开启多进程模式

  • 开启多进程模式的两种方式:

    • 为四大组件指定 android:process 属性
    • 通过 JNI 在 native 层 fork 一个新的进程
  • android:process 的属性值

    • 默认为包名,运行在单进程上
    • 指定为“processName”,完整命名方式; 在 ADM 上可以看到一条名为 processName 的进程; 属于全局进程; 其他应用通过 ShareUID 方式(在 manifest 中配置)可以和它跑在同一个进程中 (详情请谷歌 "Android ShareUID" 关键字)
    • 指定为“:processName”,在 ADM 上可以看到一条名为 "packageName:processName" 的进程; 属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中
  • adb 命令查看进程信息:

    • 查看所有进程: adb shell ps
    • 过滤查询:

    linux 下:adb shell ps|grep packageName
    windows 下,包名要加双引号:adb shell ps|find "packageName"

  • Android 系统的共享机制:

    • 这里讨论的共享分两个层面,磁盘共享和内存共享,内存共享无疑应具有更高要求
    • Android 系统为每个应用分配一个唯一的 UID (继承自 Linux 的 userID),不同应用可以使用相同的 ShareUID 让两个应用使用相同的 UID,从而共享私有数据比如 data 目录,组件信息,SharePreference,资源文件等
    • 若两个应用使用了相同的 ShareUID 且签名相同, 跑在同一个进程,就满足了共享内存数据的条件,看起来就像一个应用的两个部分

2.2.2 多进程模式的运行机制

举个栗子:

问题:现有 MainActiviy,SecondActivity,令 SecondActivity 运行在另一个进程,当 MainActivity 在其 onCreate 方法中将 UserManager 类的静态成员 sUserId 赋值并打印; 再启动 SecondActivity,onCreate 方法下打印 sUserId 的值,出现两次打印值不一致!

解释:由于 Android 为每一个进程分配独立的虚拟机,因此不同的虚拟机访问同一个类会产生各自的副本。

也就是说,运行在不同进程的组件无法通过内存共享数据,也就有需要引入一个中间介质,这就是大名鼎鼎的 Binder。

使用多进程出现的几个问题

  • 静态成员和单例模式完全失效
  • 线程同步机制完全失效
  • SharedPreferences 可靠性下降
  • Application 多次创建(可以在 onCreate 下打印验证)

跨进程通信的方式

  • Intent
  • 共享文件
  • SharePreferences
  • 基于 Binder 的 Message 和 AIDL
  • Socket

2.3 IPC 基础概念

此节首先介绍 Serializable,Parcelable 两个接口,然后再解释 Binder 类的运行流程

2.3.1 Serializable 接口

  • 通过实现 Serializable 接口,可以让类支持 ObjectOutputStream,ObjectInputStream
  • 当系统反序列化时会计算当前类的 serialVersionUID 和序列号文件中的 serialVersionUID 值是否相同,不同则反序列化失败
    • 若不指定它的值,serialVersionUID 的值就会随着类文件的改动也会跟着变化,导致序列化失败
    • 若指定它的值,即便版本发生变化,也可以强制令系统认为版本一致,避免反序列化失败
  • 静态成员变量属于类不属于对象,所以不参与序列化过程
  • 声明为 transient 的成员变量不参与序列化过程
  • 也可以重写类的 writeObject 和 readObject 方法重写系统的默认序列化过程

2.3.2 Parcelable 接口

通过实现该接口,该类对象就可以实现序列化并可以通过 Intent 和 Binder 传递

观察 Parcelable接口,这里就不贴代码了 (可以自己写一个简单的 bean 类,在 Android Studio 下选择 Code-Generate-Parcelable,自动实现 Parcelable 接口,你需要安装“Android Parcelable code generator”这款插件)

  • public void writeToParcel(Parcel dest,int flags),向 Parcel 写入,属于序列化操作
  • CREATOR 类的 createFromParcel(Parcel source) 方法,从 Parcel 中恢复,属于反序列化操作

2.3.3 Binder 类

  • 实现 IBinder 接口,主要应用在 Service 中,是客户端和服务端进行通信的媒介,服务端向客户端返回实现了业务接口的 Binder 对象(Service 的 onBind 方法)
  • 是 Android 中一种跨进程通信的方式
  • 可以理解成一个虚拟物理设备,设备驱动为 /dev/binder ?? 此处不理解...
  • 是连接各种 Manager(ActivityManager,WindowManager 等)和相应的 ManagerService 的桥梁

请求响应流程

  1. 客户端请求连接
bindService(intent,servConn,FLAG);
  1. 服务端响应,回调 onBind 方法,返回 Binder
public IBinder onBind(Intent intent) {
    return binder;
}
  1. 客户端接到响应,回调 ServiceConnection 的方法
/**
 * 连接成功
 * 单进程下,这个 binder 参数与 Service 端 onBind 方法的返回值是同一个实例
 * 但如果是跨进程要稍微复杂一点,下文会作讲解
 */
servConn.onServiceConnected(ComponentName name,IBinder binder) { 
  // 客户端保存服务端返回的 binder 的引用
  mServBinder = IAddOp.Stub.asInterface(binder); 
}
  1. 客户端通过 binder 调用远程方法
mServBinder.someFunction();

看到这里有没有感觉像网络请求的 HttpRequest/HttpResponse,完全可以用类比的思维来理解 Binder 的流程。

编写 AIDL (Android Interface Definition Language) 文件

AIDL 作为接口定义文件,与普通的接口定义方法有些许不同,然而大致是类似的。

  1. AIDL 支持的类型
  • 基本数据类型
  • String 和 CharSequence
  • 实现了 Parcelable 接口的对象
  • AIDL 接口本身的类型
  • 对于集合,AIDL 仅支持两种类型
    • ArrayList,且里面的每个元素必须被 AIDL 支持
    • HashMap,且里面的每个 key和 value 必须被 AIDL 支持
  1. 如果在 AIDL 文件中出现了定义的 Parcelable 对象,需要新建一个与之同名的 AIDL 文件,并声明为 parcelable 类型。
package *.*.*;
parcelable Something;
  1. 定义业务接口,AIDL 中除了基本类型都要标识方向:in/out/inout,只支持方法,不支持静态常量
package *.*;
import *.Something; // 即便处于同一个包下,也要 import 语句

interface ISomeManager {

   List<Something> getSomethingList();
   void doSomething(in Something sth); 
}

构建工具对 AIDL 的处理

很明显,AIDL 虽然类似一个接口类,但虚拟机肯定是不认识它的,我们都知道 Android 代码在编译前有个 Build 过程,在这个过程中,构建工具(build-tools 文中的 aidl.exe 程序)对 AIDL 文件做了什么解释呢?
当我们使用 Android Studio 时,在构建完成后,在 build/generated/source/aidl/debug/ 的路径下,我们找到了 AIDL 文件生成的类,下面我们就来阅读这些源码,来一窥究竟。

理解 AIDL 的默认实现

这里我使用原书提供的源码进行解释。

下图是 AIDL 文件的生成类的结构:

AIDL 的实现类结构

这里的 Stub 类就是我们要关注的地方,因为它就是 AIDL 的实现类:
首先看它的 asInterface 方法,因为我们要在 ServiceConnection 连接成功后,根据此方法将服务器返回的 Binder 转换为远程业务接口:

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

语意很清晰,首先拿一个字符串(接口类全名)到本地进程查询(queryLocalInterface),查询不到则说明这是一个跨进程调用;这里运用到了代理模式(Proxy),因为是跨进程,就需要额外的步骤,就是将请求参数和返回值序列化的过程,而单进程就不需要这个步骤了:

@Override
public void addBook(com.ryg.chapter_2.aidl.Book book) 
        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 ((book != null)) {
            _data.writeInt(1);
            book.writeToParcel(_data, 0);
        } else {
            _data.writeInt(0);
        }
         /**
           * 客户端发起请求,当前线程阻塞等待返回
           * 所以请求远程服务尽量不要在 UI 线程上发起
           */
        mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
        /** 
          * 客户端请求发送出去后
          * 远程请求通过底层封装后回调服务端的 Binder 的 onTransact 方法进行处理
          */
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

在 public Boolean onTransact(int code,Parcelable data,Parcelable reply,int flags) 方法中,服务端通过 code 知道客户端请求的目标方法,从data中取出所需的参数,调用对应的业务方法,执行完毕后将结果写入到 reply 中。返回 true 意味着请求成功。利用这个特性可以做验证是否有权限调用该服务,见 BookManagerService

public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
        throws RemoteException {
    // 检测清单是否声明了以下权限
    int check = checkCallingOrSelfPermission(
        "com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
    Log.d(TAG, "check=" + check);
    if (check == PackageManager.PERMISSION_DENIED) {
        return false;
    }
    // 检查应用包名是否以 "com.ryg" 开头
    String packageName = null;
    String[] packages = getPackageManager().getPackagesForUid(
            getCallingUid());
    if (packages != null && packages.length > 0) {
        packageName = packages[0];
    }
    Log.d(TAG, "onTransact: " + packageName);
    if (!packageName.startsWith("com.ryg")) {
        return false;
    }

    return super.onTransact(code, data, reply, flags);
}

在清单文件中定义权限:

<permission
    android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE"
    android:protectionLevel="normal" />

在清单文件中使用权限:

<uses-permission android:name="com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE" />

还有一种权限校验的方法就是在 onBind 中验证:

public IBinder onBind(Intent intent) {
    int check = checkCallingOrSelfPermission(
        "com.ryg.chapter_2.permission.ACCESS_BOOK_SERVICE");
    Log.d(TAG, "onbind check=" + check);
    if (check == PackageManager.PERMISSION_DENIED) {
        return null;
    }
    return mBinder;
}

感受跨进程通信的全过程

这里可以采用 Android Studio 的 debug 工具来直观感受跨进程通信的流程

attach 按钮.png

必须注意的是因为存在两个进程,所以需要 attach 两次


两个进程.png

分别在 Stub#onTransact(...) 后面,Proxy#getBookList 方法中的多处


getBookList 的断点.png

加断点,然后点击按钮进行 debug,触发断点。可以看到断点经过 getBookList 的105行、110 行时是在“com.ryg.chapter_2”进程中,接着跳转到 onTransact 中是在“com.ryg.chapter_2:remote”进程中运行的,过了一段时间断掉跳转到 getBookList 的111行了。

也就是说在 mRemote.transact 这一行时跳转到远程进程了,mRemote 的引用是 ServiceConnection#onServiceConnected 传递进去的,是客户端 Binder 驱动中的 Binder 对象,与服务端 onBind 返回的 Binder 对象是不同的。此时客户端线程进入 Binder 驱动,Binder 驱动就会挂起当前线程,并向远程服务发送一个消息包裹,消息中包含了调用的方法 id,请求参数,服务端接收到消息后调用 onTransact 进行拆包,也就是解析请求,接着调用业务方法,业务方法执行完毕后再把执行结果放入客户端提供的 reply 包裹中。然后服务端向 Binder 驱动发送一个 notify 的消息,从而使得客户端线程从 Binder 驱动代码返回到客户端代码区。

讲解至此,你是不是觉得可以不用 AIDL,自己也能写出 Stub 的实现呢。

Binder 的两个重要方法 linkToDeath 和 unlinkToDeath

由于 Binder 是可能意外死亡的,往往是服务端进程异常停止,会导致客户端的远程调用失败,有两种方法解决这个问题。一是在 onServiceDisconnected 中重连远程服务;二是给 Binder 设置一个死亡代理(DeathRecipient),当 Binder 死亡的时候客户端就会收到通知,客户端可以选择重新发起连接请求进而恢复连接了:

private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
    @Override
    public void binderDied() {
        Log.d(TAG, "binder died. tname:" + Thread.currentThread().getName());
        if (mRemoteBookManager == null)
            return;
        mRemoteBookManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
        mRemoteBookManager = null;
        // TODO:这里重新绑定远程Service
    }
};

然后在 ServiceConnection#onServiceConnected 中注册:

mRemoteBookManager.asBinder().linkToDeath(mDeathRecipient, 0);

2.4 Android 中的 IPC 方式

2.4.1 使用 Bundle

Bundle实现了Parcelable接口,在启动另一个进程的 Activity,Service和Receiver,都支持在Intent中传递 Bundle 数据

2.4.2 使用文件共享

这种方式简单,由于Linux 没有限制并发读写,适合在对数据同步要求不高的进程之间进行通信,但要尽量避免并发读写;通常会使用到对象序列化文件,文本文件,XML 等等,但 SharedPreferences 应该避免使用,虽然它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份 SharedPreferences 文件的缓存,因此在多进程模式下,系统对它的读写就变得不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据。

2.4.3 使用Messenger

Messenger 是一种轻量级的 IPC 方案,它的底层实现就是 AIDL。Messenger 是以串行的方式处理请求的,即服务端只能一个个处理,不存在并发执行的情形。必须将数据放入 Message 中,能使用的载体只有 what,arg1,arg2,Bundle 以及 replyTo 和 object 字段。

2.4.4 使用AIDL

源码
定义一个图书管理接口,客户端可以远程获取图书列表,也可以远程添加一册图书,甚至可以向服务端设置监听接口:

interface IBookManager {
     List<Book> getBookList();
     void addBook(in Book book);
     void registerListener(IOnNewBookArrivedListener listener);
     void unregisterListener(IOnNewBookArrivedListener listener);
}

服务端使用 RemoteCallbackList<T> 这个类来管理 AIDL 接口,否则服务端无法保证监听的正常连接,底层依旧是使用 Binder 实现的。调用 register/unregister 方法管理监听接口:

RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = 
        new RemoteCallbackList<IOnNewBookArrivedListener>();

boolean success = mListenerList.register(listener);
boolean success = mListenerList.unregister(listener);

回调接口的方法如下:

final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
    IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
    if (l != null) {
        try {
            l.onNewBookArrived(book);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
}
mListenerList.finishBroadcast();

2.4.5 使用ContentProvider

可以自行百度或谷歌,不是本文重点

2.4.6 使用Socket

可以自行百度或谷歌,不是本文重点

2.5 Binder连接池

简单讲就是使用一个 Service,一个 Binder 连接池来替代多个 Service 的实现。毕竟多个服务进程会让应用看起来繁重,用户有想卸载的欲望。
源码点这里

  1. 服务端 onBind 返回 BinderPool 的 IBinder 实例,它实现了 queryBinder 方法,这个接口能够根据业务模块的特征来返回相应的 Binder 对象给它们,不同的业务模块拿到所需的 Binder 对象后就可以进行远程方法调用了。
@Override
public IBinder onBind(Intent intent) {
    Log.d(TAG, "onBind");
    return new BinderPool.BinderPoolImpl();;
}
  1. Binder 连接池的主要作用就是将每个业务模块的 Binder 请求统一转发到远程Service去执行,从而避免了创建多个 Service。
@Override
public IBinder queryBinder(int binderCode) throws RemoteException {
    IBinder binder = null;
    switch (binderCode) {
        case BINDER_SECURITY_CENTER: {
            binder = new SecurityCenterImpl();
            break;
        }
        case BINDER_COMPUTE: {
            binder = new ComputeImpl();
            break;
        }
        default:
            break;
    }
    return binder;
}

2.6 选用合适的 IPC 方式

名称 优点 缺点 适用场景
Bundle 简单易用 只能传输 Bundle 支持的数据类型 四大组件间的进程通信
文件共享 简单易用 不适合高并发场景,并且无法做到进程间的及时通信 无并发访问情形,交换简单的数据,实时性不高的场景
AIDL 功能强大,支持一对多并发通信,支持实时通信 使用稍复杂,需要处理好线程同步 一对多通信且有 RPC 需求
Messenger 功能一般,支持一对多串行通信,支持实时通信 不能很好处理高并发情形,不支持 RPC,数据通过 Message 进行传输,因此只能传输 Bundle 支持的数据类型 低并发的一对多即时通信,无 RPC 需求,或者无须要返回结果的 RPC 需求
ContentProvider 在数据源访问方面功能强大,支持一对多并发数据共享,可通过 Call 方法扩展其他操作 可以理解为受约束的 AIDL,主要提供数据源的 CRUD 操作 一对多的进程间的数据共享
Socket 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 实现细节稍微有点繁琐,不支持直接的 RPC 网络数据交换

参考阅读

Binder框架及运用详解

推荐阅读更多精彩内容