binder机制原理

136 阅读11分钟

一、用户空间和内核空间

我们知道操作系统采用的是虚拟地址空间,以32位操作系统举例,它的寻址空间为4G(2的32次方)。

为什么2的32次方=4G呢?

计算机最小的存储单位是字节(byte),然后分别是:KB,MB,GB

1B(byte) = 8bit(位)

1KB = 1024B,以此类推

4GB = 4 * 1024 *1024 * 1024 = 4294967296

而2的32次方也 = 4294967296

即:2的32次方=4G

os分配给每个进程一个独立的、连续的、虚拟的地址内存空间,该大小一般是4G(32位操作系统,即2的32次方)。即进程寻址空间是0-4G。

其中将高地址值3G-4G的内存空间分配给os占用,每个进程虚拟空间的3G~4G部分是相同的。0-3G的内存地址空间分配给进程使用。

学习 Linux 时,经常可以看到两个词:User space(用户空间)和 Kernel space(内核空间)。

简单说,

  • Kernel space 是 Linux 内核的运行空间。

    • 当进程运行在内核空间时就处于内核态。
    • Kernel space 可以执行任意命令,调用系统的一切资源。
  • User space 是用户程序的运行空间。

    • 当进程运行在用户空间时就处于用户态。
    • User space 只能执行简单的运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。
  • 为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。

  • 通过系统调用(system call),进程可以从用户空间切换到内核空间。

示例:

str = "my string" // 用户空间
x = x + 2
file.write(str) // 切换到内核空间
y = x + 4 // 切换回用户空间

上面代码中,第一行和第二行都是简单的赋值运算,在 User space 执行。第三行需要写入文件,就要切换到 Kernel space,因为用户不能直接写文件,必须通过内核安排。第四行又是赋值运算,就切换回 User space。

二、Linux 下传统的进程间通信原理

linux下系统调用主要通过如下两个函数来实现:

  • copy_from_user
  • copy_to_user

通常的做法是:

  • 消息发送方通过系统调用copy_from_user将数据从用户空间copy到内核缓存区
  • 消息接收方通过copy_to_user将数据从内核缓存区copy到用户空间

a22970def8c0573558c51a1fa6da4e9a.jpg

这种传统的 IPC 通信方式有两个问题:

  • 性能低下,一次数据传递需要经历:内存缓存区 → 内核缓存区 → 内存缓存区,需要 2 次数据拷贝;

  • 接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。

三、Binder 跨进程通信原理

3.1、动态内核可加载模块 和 内存映射

正如前面所说,跨进程通信是需要内核空间做支持的。传统的 IPC 机制如管道、Socket 都是内核的一部分,因此通过内核支持来实现进程间通信自然是没问题的。但是 Binder 并不是 Linux 系统内核的一部分,那怎么办呢?

这就得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)机制

模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核,作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。

在 Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。

那么在 Android 系统中用户进程之间是如何通过这个内核模块(Binder 驱动)来实现通信的呢?难道是和前面说的传统 IPC 机制一样,先将数据从发送方进程拷贝到内核缓存区,然后再将数据从内核缓存区拷贝到接收方进程,通过两次拷贝来实现吗?显然不是,否则也不会有开篇所说的 Binder 在性能方面的优势了。

这就不得不提到 Linux 下的另一个概念:内存映射(mmap)

内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。

内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。

3.2、Binder IPC 实现原理

一次完整的 Binder IPC 通信过程通常是这样:

  1. 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
  2. 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
  3. 发送方进程通过系统调用 copyfromuser() 将数据 copy 到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。

f78fc3d513d474d1f2273051ad6d8346.jpg

四、Binder 通信模型

前面我们介绍过,Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。

Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。

通常我们访问一个网页的步骤是这样的:

  • 首先在浏览器输入一个地址,如 www.google.com 然后按下回车键。
  • 但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要先访问 DNS 域名服务器,域名服务器中保存了 www.google.com 对应的 ip 地址 10.249.23.13。
  • 然后通过这个 ip 地址才能访问到 www.google.com 对应的服务器。

64c8c23c1df866b5655a0dea661ccd28.jpg

Binder通信也一样:

  • Server 通过驱动向 ServiceManager 中注册 Binder(Server 中的 Binder 实体),表明可以对外提供服务。
  • Client 通过名字,在 Binder 驱动的帮助下从 ServiceManager 中获取到对 Binder 实体的引用,通过这个引用就能实现和 Server 进程的通信。

d316ada105ec7f6899fc42665bb94745.jpg

五、进程间通信时,对方进程挂了,报出DeadObjectException该如何解决

5.1、崩溃来源

首先,这个崩溃的意思是,多进程在进行跨进程Binder通信的时候,发现通信的Binder对端已经死亡了。

抛出异常的Java堆栈最后一行是BinderProxy.transactNative,所以我们从这个方法入手,看看崩溃是在哪里产生的。

很显现,transactNative对应的是一个native方法,我们找到对应的native方法,在android_util_Binder.cpp中。

static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj,
        jint code, jobject dataObj, jobject replyObj, jint flags) // throws RemoteException
{
    // 如果data数据为空,直接抛出空指针异常
    if (dataObj == NULL) {
        jniThrowNullPointerException(env, NULL);
        return JNI_FALSE;
    }
    // 将Java层传入的对象转换为C++层的指针,如果转换出错,中断执行,返回JNI_FALSE
    Parcel* data = parcelForJavaObject(env, dataObj);
    if (data == NULL) {
        return JNI_FALSE;
    }
    Parcel* reply = parcelForJavaObject(env, replyObj);
    if (reply == NULL && replyObj != NULL) {
        return JNI_FALSE;
    }
    // 获取C++层的Binder代理对象指针
    // 如果获取失败,会抛出IllegalStateException
    IBinder* target = getBPNativeData(env, obj)->mObject.get();
    if (target == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", "Binder has been finalized!");
        return JNI_FALSE;
    }
    // 调用BpBinder对象的transact方法
    status_t err = target->transact(code, *data, reply, flags);
    // 如果成功,返回JNI_TRUE,如果失败,返回JNI_FALSE
    if (err == NO_ERROR) {
        return JNI_TRUE;
    } else if (err == UNKNOWN_TRANSACTION) {
        return JNI_FALSE;
    }
    // 处理异常情况的抛出
    signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/, data->dataSize());
    return JNI_FALSE;
}

可以看到,这个方法主要做的事情是:

  • Java层传入的data,转换成C++层的指针
  • 获取C++层的Binder代理对象
  • 调用BpBinder对象的transact方法
  • 处理transact的结果,抛出异常

接下来我们看看,BpBindertransact方法。

status_t BpBinder::transact(uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags) {
    // 首先判断Binder对象是否还存活,如果不存活,直接返回DEAD_OBJECT
    if (mAlive) {
            ...
            status = IPCThreadState::self()->transact(binderHandle(), code, data, reply, flags);
            return status;
    }
    return DEAD_OBJECT;
}

transact的具体方法,我们这里先不讨论。我们可以看到,在这里会判断当前的Binder对象是否alive,如果不alive,会直接返回DEAD_OBJECT的状态。

返回的结果,在android_util_BindersignalExceptionForError中处理。

void signalExceptionForError(JNIEnv* env, jobject obj, status_t err,
        bool canThrowRemoteException, int parcelSize) {
       // 省略其他异常处理的代码
        ....
        case DEAD_OBJECT:
            // DeadObjectException is a checked exception, only throw from certain methods.
            jniThrowException(env, canThrowRemoteException
                    ? "android/os/DeadObjectException"
                            : "java/lang/RuntimeException", NULL);
            break;
}

这个方法,其实包含非常多异常情况的处理。为了看起来更清晰,这里我们省略了其他异常的处理逻辑,只保留了DEAD_OBJECT的处理。可以很明显的看到,在这里我们抛出了DeadObjectException异常。

5.2、解决方法

通过前面的源码分析,我们知道DeadObjectException是发生在,当我们调用transact接口发现Binder对象不再存活的情况。

解决方案也很简单,就是当这个Binder对象死亡之后,不再调用transact接口。

方法1、调用跨进程接口之前,先判断Binder是否存活

这个方案比较简单粗暴,就是在多有调用跨进程接口的地方,都加一个Binder是否存活的判断。

 if (mService != null && mService.asBinder().isBinderAlive()) {
    mService.test();
 }

我们来看下isBinderAlive的源码,就是判断mAlive标志位是否为0。

bool BpBinder::isBinderAlive() {
    return mAlive != 0;
}
方法2、监听Binder死亡通知

先初始化一个DeathRecipient,用来监听死亡通知。

    private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {

        @Override
        public void binderDied() {
            // 解绑当前监听,重新启动服务
            mService.asBinder().unlinkToDeath(mDeathRecipient, 0);
            // 再次尝试启动和绑定服务
            if (mService != null)
                bindService(new Intent("com.service.bind"), mService, BIND_AUTO_CREATE);
        }
    };

在这个死亡监听里,我们可以选择几种处理方式:

  • 什么都不做,直接将mService设置为空
  • 再次尝试启动和绑定服务

onServiceConnected方法中,注册死亡监听:

public void onServiceConnected(ComponentName name, IBinder service) {          
    mService = IServiceInterface.Stub.asInterface(service);     
    //获取服务端提供的接口
    try {
        // 注册死亡代理
        if(mService != null){
            service.linkToDeath(mDeathRecipient, 0); 
        }       
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

六、Intent传递大数据

6.1、TransactionTooLargeException

在Android开发过程中,我们常常通过Intent在各个组件之间传递数据。例如在使用startActivity(android.content.Intent)方法启动新的 Activity 时,我们就可以通过创建Intent对象然后调用putExtra() 方法传输参数。

val intent = Intent(this, TestActivity::class.java)
intent.putExtra("name","name")
startActivity(intent)

启动完新的Activity之后,我们可以在新的Activity获取传输的数据。

val name = getIntent().getStringExtra("name")

一般情况下,我们传递的数据都是很小的数据,但是有时候我们想传输一个大对象,比如bitmap,就有可能出现问题。

val intent = Intent(this, TestActivity::class.java)
val data= ByteArray( 1024 * 1024)
intent.putExtra("param",data)
startActivity(intent)

当调用该方法启动新的Activity的时候就会抛出异常。

android.os.TransactionTooLargeException: data parcel size 1048920 bytes

很明显,出错的原因是我们传输的数据量太大了。在官方文档中有这样的描述:

The Binder transaction buffer has a limited fixed size, currently 1Mb, which is shared by all transactions in progress for the process. Consequently this exception can be thrown when there are many transactions in progress even when most of the individual transactions are of moderate size。

即缓冲区最大1MB,并且这是该进程中所有正在进行中的传输对象所公用的。所以我们能传输的数据大小实际上应该比1M要小。

6.2、解决方案

使用bundle.putBinder()方法完成大数据传递。

为什么通过这种方式就可以绕过1M的缓冲区限制呢,这是因为直接通过Intent传递的时候,系统采用的是拷贝到缓冲区的方式,而通过putBinder()的方式则是利用共享内存,而共享内存的限制远远大于1M,所以不会出现异常。

由于我们要将数据存放在Binder里面,所以先创建一个类继承自Binder。data就是我们传递的数据对象。

class BigBinder(val data:ByteArray):Binder()

然后传递

val data= ByteArray( 1024 * 1024)
startActivity(Intent(this, TestActivity::class.java).apply{
    putExtra("bundle",Bundle().apply{
        putBinder("bigData",BigBinder(data))
    })
})

然后正常启动新界面,发现可以跳转过去,而且新界面也可以接收到我们传递的数据。