Android开发必看!意外收获字节跳动内部资料——《Android-Binder机制实现原理》

Binder 是 Android 系统中非常重要的组成部分。Android 系统中的许多功能建立在 Binder 机制之上。在这篇文章中,我们会对 Android 中的 Binder 在系统架构中的作用进行分析;然后,我们会从底层的实现角度简要说明为什么 Android 要开发出一套独立的跨进程通信机制;最后,我们会给出一个 AIDL 的使用示例来说明如何使用 Binder 来进行通信。

1、什么是 Binder? 为什么说它对 Android 系统至关重要?

“什么是 Binder? 为什么说它对 Android 系统至关重要?” 在回答这个问题之前,我们先来说下其他的东西。

不知道你有没有思考过这么一个问题:为什么当我们在 Android 中启动一个页面的时候需要调用 startActivity() 方法,然后还要传入一个 Intent? 如果我们不使用这种传递值的方式,直接写成静态的变量有没有问题?这也是之前有人问过我的一个问题。

对上面的两个问题,我们先回答第二个。使用静态的变量传递值在大部分情况下是可以的,当然要注意在使用完了值之后要及时释放资源,不然会占用太多内存,甚至 OOM. 但是,在特殊的情况下它是无法适用的,即跨进程的情况下。这是因为,静态的变量的作用范围只是其所在的进程,在其他进程访问的时候属于跨进程访问,当然访问不到了。对于第一个问题,Android 中的一个 Activity 的启动过程远比我们想象的复杂,其中就涉及跨进程的通信过程。当我们调用 startActivity() 方法之后,我们的所有的 “意图” 会经过层层过滤,直到一个称之为 AMS 的地方被处理。处理完之后,再跨进程调用你启动页面时的进程进行后续处理,即回调 onCreate() 等生命周期方法。

一个 Activity 的启动过程涉及 Android 中两种重要的通信机制,Binder 和 Handler,我们会在以后的文章中对此进行分析。

下面我们通过一个简单的图来说明一下 Activity 的启动过程:

Android开发必看!意外收获字节跳动内部资料——《Android-Binder机制实现原理》

当我们调用 startActivity() 方法的时候,首先会从 ServiceManager 中获取到 ActivityManagerService (就是 AMS),然后将 ApplicationThread 作为参数传递给 AMS,然后执行 AMS 的方法来启动 Activity. (在我们的应用进程中执行另一个进程的方法。)

AMS 是全局的,在系统启动的时候被启动。当我们使用它的时候从 ServiceManager 中获取这个全局的变量即可。当我们调用它的方法的时候,方法具体的执行逻辑将在系统的进程中执行。我们传入的 ApplicationThread 就像一个信使一样。当 AMS 处理完毕,决定回调 Activity 的生命周期方法的时候,就直接调用 ApplicationThread 的方法(这是在另一个进程中调用我们的应用进程)。这样就实现了我们的 Activity 的生命周期的回调。

看了上面的过程,也许有的同学会觉得。Binder 对 Android 系统至关重要,但是我们并没有用到 Binder 啊。实际上,我们只是没有直接使用 Binder. 以下图为例,我们说下我们实际开发过程中是如何使用 Binder 的。

Android开发必看!意外收获字节跳动内部资料——《Android-Binder机制实现原理》

在大多数情况下,我们都在与各个 Manager 进行交互,而实际上这些 Manager 内部是使用 Binder 来进行跨进程通信的。如上所示,当我们调用 Manager 的时候,Manager 会通过代理类来从 Binder 驱动中得到另一个进程的 Stub 对象,然后我们使用该 Stub 对象,远程调用另一个进程的方法。只是这个过程被封装了,我们没有感知到而已,而这个跨进程通信 (IPC) 的机制就是 Binder 机制。

至于什么是 Stub 呢?Stub 是 AIDL 规范中的一部分。AIDL 为我们使用 Binder 提供了一套模板。在 Android 系统中大量使用了这种定义来完成跨进程通信。稍后我们介绍 AIDL 的时候,你将看到它是如何作用的。

2、为什么是 Binder 而不是其他通信机制?

Android 是基于 Linux 的,Linux 本身已经具有了许多的 IPC 机制,比如:管道(Pipe)、信号(Signal)和跟踪(Trace)、插口(Socket)、消息队列(Message)、共享内存(Share Memory)和信号量(Semaphore)。那么,为什么 Android 要特立独行地搞出一套 IPC 机制呢?这当然是有原因的:

  1. 效率上 :Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder 只需要一次数据拷贝,性能上仅次于共享内存

  2. 稳定性:Binder 基于 C|S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。 共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的。

  3. 安全性:Binder 通过在内核层为客户端添加身份标志 UID|PID,来作为身份校验的标志,保障了通信的安全性。 传统 IPC 访问接入点是开放的,无法建立私有通道。比如,命名管道的名称,SystemV 的键值,Socket 的 ip 地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接。

除了上面的原因之外,Binder 还拥有许多其他的特性,比如:1).采用引用计数,当某个 Binder 不再被任何客户端引用的时候,会通知它的持有者可以将其释放,这适用于 Android 这种常常因为资源不足而回收资源的应用场景。2).它内部维护了一个线程池;3).可以像触发本地方法一样触发远程的方法。4).支持同步和异步 (oneway) 的触发模型;5).可以使用 AIDL 模板进行描述和开发。

3、Binder 模型,Binder 中的 4 个主要角色

在 Binder 模型*有 4 个主要角色,它们分别是:Client、Server、Binder 驱动和 ServiceManager. Binder 的整体结构是基于 C|S 结构的,以我们启动 Activity 的过程为例,每个应用都会与 AMS 进行交互,当它们拿到了 AMS 的 Binder 之后就像是拿到了网络接口一样可以进行访问。如果我们将 Binder 和网络的访问过程进行类比,那么 Server 就是服务器,Client 是客户终端,ServiceManager 是域名服务器(DNS),驱动是路由器。其中 Server、Client 和 ServiceManager 运行于用户空间,驱动运行于内核空间

当我们的系统启动的时候,会在启动 SystemServer 进程的时候启动各个服务,也包括上面的 AMS. 它们会被放进一个哈希表中,并且哈希表的键是字符串。这样我们就可以通过服务的字符串名称来找到对应的服务。这些服务就是一个个的 Binder 实体,对于 AMS 而言,也就是 IActivityManager.Stub 实例。这些服务被启动的之后就像网络中的服务器一样一直等待用户的访问。

对于这里的 ServiceManager,它也是一种服务,但是它比较特殊,它会在所有其他的服务之前被注册,并且只被注册一次。它的作用是用来根据字符串的名称从哈希表中查找服务,以及在系统启动的时候向哈希表中注册服务。

Android开发必看!意外收获字节跳动内部资料——《Android-Binder机制实现原理》

所以,我们可以使用上面的这张图来描述整个 Binder 模型:首先,在系统会将应用程序所需的各种服务通过 Binder 驱动注册到系统中(ServiceManager 先被注册,之后其他服务再通过 ServiceManager 进行注册),然后当某个客户端需要使用某个服务的时候,也需要与 Binder 驱动进行交互,Binder 会通过服务的名称到 ServiceManager 中查找指定的服务,并将其返回给客户端程序进行使用。

4、Binder 的原理

上面我们梳理了 Binder 的模型,以及为什么系统设计一套通信机制的原因。那么你是否也好奇神乎其神的 Binder 究竟是怎么实现的呢?这里我们来梳理下 Binder 内部实现的原理。

首先,Binder 的实现过程是非常复杂的,在《Android 系统源码情景分析》一书中有 200 页的篇幅都在讲 Binder. 在这里我们不算详细地讲解它的具体的实现原理,我们只对其中部分内容做简单的分析,并且不希望涉及大量的代码。

4.1 inder 相关的系统源码的结构

然后,我们需要介绍下 Binder 相关的核心类在源码中的位置,

-framework
    |--base
        |--core
            |--java--android--os  
                              |--IInterface.java
                              |--IBinder.java
                              |--Parcel.java
                              |-- IServiceManager.java
                              |--ServiceManager.java
                              |--ServiceManagerNative.java
                              |--Binder.java  
            |--jni
                |--android_os_Parcel.cpp
                |--AndroidRuntime.cpp
                |--android_util_Binder.cpp
    |--native
        |--libs--binder         
                  |--IServiceManager.cpp
                  |--BpBinder.cpp
                  |--Binder.cpp             // Binder 的具体实现
                  |--IPCThreadState.cpp
                  |--ProcessState.cpp
        |--include--binder                  // 主要是一些头文件
                      |--IServiceManager.h
                      |--IInterface.h
        |--cmds--servicemanager
                    |--service_manager.c    // 用来注册服务的 ServiceManager
                    |--binder.c
-kernel-drivers-staging-android
                         |--binder.c        
                         |--uapi-binder.h

4.2 Binder 实现过程中至关重要的几个函数

当我们查看 binder.c 的源码的时候,或者查看与 Binder 相关的操作的时候,经常看到几个操作 ioctl, mmap 和 open. 那么这几个操作符是什么含义呢?

首先,open 函数用来打开文件的操作符,在使用的时候需要引入头文件,#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>,其函数定义如下,

int open(const char * pathname, int flags);
int open(const char * pathname, int flags, mode_t mode);

这里的 pathname 表示文件路径;flag 表示打开方式;mode 表示打开的模式和权限等;若所有欲核查的权限都通过了检查则返回 0, 表示成功, 只要有一个权限被禁止则返回-1.

然后是 ioctl 指令,使用的时候需要引入 #include <sys/ioctl.h> 头文件,ioctl 是设备驱动程序中对设备的 I/O 通道进行管理的函数,用于向设备发控制和配置命令。其函数定义如下:

int ioctl(int fd, ind cmd, …); 

其中 fd 是用户程序打开设备时使用 open 函数返回的文件标示符,cmd 是用户程序对设备的控制命令,至于后面的省略号,那是一些补充参数,一般最多一个,这个参数的有无和 cmd 的意义相关。

最后是 mmap 函数,它用来实现内存映射。使用的时候需要引入头文件 #include <sys/mman.h>. 与之对应的还有 munmap 函数。它们的函数定义如下,

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);

这里的参数的含义是:

  1. start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址;
  2. length:映射区的长度。长度单位是以字节为单位,不足一内存页按一内存页处理;
  3. prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过 o r运算合理地组合在一起;
  4. flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体;
  5. fd:有效的文件描述词。一般是由 open() 函数返回,其值也可以设置为-1,此时需要指定 flags 参数中的 MAP_ANON,表明进行的是匿名映射;
  6. off_toffset:被映射对象内容的起点。

成功执行时,mmap() 返回被映射区的指针,munmap() 返回0。失败时,mmap() 返回 MAP_FAILED[其值为(void *)-1],munmap() 返回 -1.

4.3 ServiceManger 启动

Binder 中的 ServiceManager 并非 Java 层的 ServiceManager,而是 Native 层的。启动 ServiceManager 由 init 进程通过解析 init.rc 文件而创建。启动的时候会找到上述源码目录中的 service_manager.c 文件中,并调用它的 main() 方法,

// platform/framework/native/cmds/servicemanager.c
int main(int argc, char** argv)
{
    struct binder_state *bs;
    char *driver;

    if (argc > 1) {
        driver = argv[1];
    } else {
        driver = "/dev/binder";
    }
    // 1\. 打开 binder 驱动
    bs = binder_open(driver, 128*1024);
    // ...
    // 2\. 将当前的 ServiceManger 设置成上下文
    if (binder_become_context_manager(bs)) {
        return -1;
    }
    // ...
    // 3\. 启动 binder 循环,进入不断监听状态
    binder_loop(bs, svcmgr_handler);
    return 0;
}

ServcieManager 启动的过程就是上面三个步骤,无需过多说明。下面我们给出这三个方法具体实现的。在下面的代码中你将看到我们之前介绍的三个函数的实际应用。相应有了前面的铺垫之后你理解起来不成问题

上一篇:Android组件化架构实践,完整PDF


下一篇:Android 进阶解密 - 根 Activity 的启动