Android音视频框架探索(二):Binder——系统服务的通信基础

0. 前言

Android中的应用进程及系统服务,都是独立的进程,所以服务的调用必然涉及到进程之间的通信,而Binder正是其中沟通的桥梁。

Binder是Android中的一种IPC(进程间通信方式)机制,以AIDL,Binder为基础,Android实现了一套系统内的RPC(远程过程调用)调用机制,作为应用与系统服务之间的通信方式。

本文主要介绍Binder机制的原理及其在系统音视频服务中的使用。

p.s.需要说明的是,通过一篇文章,就把Binder机制介绍清楚也不大可能,所以本文的目的更多是让读者在后续多媒体框架中,对于Binder机制不会感到困惑,对于Binder的实现细节可以不必了解太多,能理解客户端和服务端的调用关系则可。

1. Binder介绍

1.1 Binder通信原理

Binder通信的架构如上图示,Binder通信中有四类主体角色:Binder驱动ServiceManager,客户端Client和服务端Server。其中ServiceMangerClientServer,均只能和Binder驱动进行直接通信,而在Binder驱动的传递下,三者可以进行间接通信。

Client和Server之间使用由AIDL(Android 接口定义语言)生成的具体的服务功能接口,Client在调用服务前,可以拿到服务代理对象(Proxy),继而调用具体的方法。而Server端,在启动后便会创建具体的服务实体对象(Stub)(服务功能具体的实现者),在收到Client发送的调用信息后,便会调用对应的方法。

所以说这一套Binder的调用过程更是一套RPC机制,因为被调用的是服务端进程中具体的服务对象的方法。

1.1.1 Binder驱动

Binder驱动是通信的基础,它在系统的设备目录中添加了一个虚拟设备/dev/binder,用户空间的进程只能通过Binder的调用库libbinder来访问和使用Binder通信,其底层实现上具体都是通过ioctl()来访问这个虚拟设备。

在一次通信的过程中,Binder驱动会使用copy_from_user()将发送端进程的数据拷贝至内核空间,然后使用mmap(),将内核的缓存区映射到接收端的虚拟地址空间。所以从数据操作上来讲,使用Binder通信会发生一次IPC数据的拷贝。相较于其他传统的IPC通信方式还比较高效(管道、消息队列、socket:2次,共享内存:0次)。

p.s. 这里说的拷贝的高效性还是针对一次IPC通信时的数据,万不可简单理解成"使用Binder的方式通信"就是高效的这种推之四海皆准的想法。事实上使用Binder时的高效性和以下的一些内容相关:

(1)IPC数据类型、大小

一般情况下,Binder通信时会发生拷贝,所以通信的数据最好只是控制信号、命令、协议信息等。而大型的数据信息,如多媒体数据,图片等,都不适合直接通过Binder来进行传输,Android系统中对于多媒体数据,也是使用的零拷贝的方式来实现,比如GraphicBuffer,在进程间使用Binder来传递索引,宽高等属性信息,而具体的的底层图形显存(内存)是进程间共享的。

(2)Binder的调用链路

从上面可以得 知,Android的具体功能服务,被拆分到不同的服务对象中,这些服务对象之间的通信需要Binder来传递。在模块拆分解耦的同时,Binder的调用链路也被拉长,比如笔者曾经研究过cameraserver的代码,在打开一次相机的过程中,从App层的调用到Hal层的响应,这期间需要进行6次Binder的通信。如果仅从次数上来看,这样的Binder调用次数"又不怎么高效"。但事实上,这类命令控制如(1)中所说,通信量其实无伤大雅,也并不都是瓶颈。

所以,当我们需要确认某种功能调用性能是否高效时,还是需要依靠具体情况来进行分析。

1.1.2 ServiceManager

之前的文章提及过,ServiceManager是一个系统服务,在init进程初始化时被创建,它是Binder通信的基础。服务端进程需要将自身服务向ServiceManager进行注册,而客户端进程在使用具体的Binder对象前,都需要向其请求获得具体的服务对象。

如果把Binder的通信过程理解成一次网络请求,那么ServiceManager就相当于请求过程中的DNS服务器。在网络请求前,客户端需要通过DNS服务器将服务器的域名解析成具体的IP地址,类似地,在Binder通信中,客户端需要向ServiceManager请求,获得服务的代理对象,继而去调用具体的服务功能。

值得一提的是,ServiceManager本身也是一种服务,与客户端和服务端进程之间的通信也仍然使用Binder。但是它作为一种"先验知识",事先被Binder驱动、客户端、服务端所知晓。ServiceManager进程启动后,会向Binder驱动使用BINDER_SET_CONTEXT_MGR_EXT命令,声明本进程为服务的管理者ServiceManager。同时Binder驱动也只能设置唯一的ServiceManager,在收到命令后便会为其分配上下文。而客户端和服务端,也通过固定的索引方式向Binder驱动获取到ServiceManager的代理对象(在libbinder中,具体通过getStrongProxyForHandle(0),来获得Handle为0的Serivce的代理对象,即ServiceManger)。

1.1.3 Server

Server是通信过程中的服务端进程,它是一个守护进程。一般而言,Server进程在启动后会进行如下的操作:

(1)创建Binder服务对象(继承自libbinderBBinder类)

(2)获取serviceManager的binder代理,并调用IServiceManager::addService()将服务对象向serviceManager进行注册。

(3)进程会启动Binder线程池,进程的线程(主线程或其他子线程)可以加入线程池监听Binder驱动的消息。

(4)待到接收到Binder消息时,会唤醒线程池中监听的线程,并将消息传递给对应的Binder服务对象,调用对应的方法。

1.1.4 Client

Client则是通信的客户端进程,它和服务端可以是不同的进程,也可以是在同一个进程。Client调用一个具体的Serivce过程如下:

(1)获取serviceManager的binder代理,并调用IServiceManager::getService()方法,获得指定name的服务代理对象。

(2)调用服务代理对象的指定方法,获取返回值。(底层会自动通过Binder驱动,将消息传递给服务端进程的服务对象,完成调用,并将返回值反序列化返回)

1.2 Binder的划分

在Android 8.0之前,系统的所有Binder通信都使用唯一的驱动设备"/dev/binder",和单独的ServiceManager进程"/system/bin/servicemanager",所有的服务(包括Google原生的,和供应商扩展的服务)都依赖他们。

在Android 8.0之后,为方便系统的移植和升级,Android引入Treble机制,原先的system分区被拆分成system分区和vendor分区,soc及供应商的功能实现都需要放入vendor分区,这样Hal层和Framework层进行了解耦。系统也对Binder通信的进程范围做划分,引入了多个IPC域,即增加了多个binder设备,并且不同binder域通信的进程的权限控制也通过selinux进行了进一步划分。当然,随之而来的Binder的调用链路也拉长了。

IPC域 说明
/dev/binder Framework/App进程之间的IPC, 使用AIDL接口
/dev/hwbinder Framework/Vendor进程之间的IPC,Vendor进程之间的IPC,使用HIDL接口
/dev/vndbinder Vender/Vendor进程之间的IPC,使用AIDL接口

以下摘自Android10.0 Binder通信原理的一张图可以很直观地说明这种划分后的变化。 事实上,这些设备的适用范围和实现细节有所区别,但Binder通信的原理还是一样的。 专栏后续主要讨论的还是Framework层的音视频框架,因此以介绍传统的Binder机制为主。

2. Binder的代码结构

2.1 libbinder的结构

在上文我们介绍了Binder通信的原理和通信的大概过程,下面我们更深入了解一下使用Binder的方法,使用Binder通信主要依赖libbinder库,其位于Android源码的frameworks/native/libs/binder/include/binder/下。

我们可以根据下列的脑图了解下Binder机制下的一些主要的类。

2.2 AIDL的使用

1.2 AIDL

AIDL全称Android Interface Definition Language,也就是Android接口定义语言。使用AIDL,我们可以快速地生成Binder通信的接口代码。AIDL的使用步骤如下:

  1. 创建.aidl文件,这些文件定义了Binder通信过程中的方法接口。
  2. 调用AIDL后端工具,生成java或c++的binder接口代码。(Proxy类已经可以使用,Stub类还需要补充实现)
  3. 填充和实现Stub类的功能代码。
  4. 完成好的Service向ServiceMananger注册,后续客户端正常使用。

例如有以下的aidl接口:

aidl 复制代码
package com.example;

interface IMyService {
    int add(int a, int b);
}

生成的C++文件有:

shell 复制代码
com/
└── example/
    ├── IMyService.h        // 接口定义
    ├── BnMyService.h       // 服务端基类
    ├── BpMyService.h       // 客户端代理类
    └── IMyService.cpp      // 实现(Binder 通信逻辑)

服务端需要继承并实现BnMyService

C++ 复制代码
class MyService : public BnMyService {
public:
    // 主要是实现接口定义的方法
    binder::Status add(int32_t a, int32_t b, int32_t* _aidl_return) override {
        *_aidl_return = a + b;
        return binder::Status::ok();
    }
};

服务端服务对象的创建和注册:

c++ 复制代码
int main() {
    sp<ProcessState> proc(ProcessState::self());
    sp<IServiceManager> sm = defaultServiceManager();
    
    sm->addService(String16("MyService"), new MyService());
    
    ProcessState::self()->startThreadPool();
    IPCThreadState::self()->joinThreadPool();
    
    return 0;
}

客户端进程调用:

c++ 复制代码
int main() {
    sp<IServiceManager> sm = defaultServiceManager();
    sp<IBinder> binder = sm->getService(String16("MyService"));
    
    sp<IMyService> service = interface_cast<IMyService>(binder);
    
    int32_t result;
    service->add(3, 5, &result);
    
    return 0;
}

3. 以MediaPlayerService为例

我们以上一篇文章中的MediaServer服务进程中创建的MediaPlayerService。需要注意的是,这里的代码流程我们还是以Android7.1的为例,来说明主要流程(8.0之后由于兼容多种binder,代码调用路径在源码中不是很清晰)。

3.1 MediaPlayerService的继承体系

上文中我们提及了libbinder中的一些基类。现在我们以MediaPlayerService为例,来了解一下一个具体的服务类的继承体系。

首先是MediaPlayerService的实体服务对象,它继承自上面提及的libbinder中的BBinder(IBinder)IMediaPlayerService(IInterface)两条接口脉络。

而客户端代理对象,会直接使用到BpMediaPlayerService,它的结构继承自BpRefBaseIMediaPlayerService,其中BpRefBase中组合了BpBinder对象,而间接获得IBinder的能力。

3.2 获取ServiceManager

这里我们主要了解一下Binder通信中,一定会使用到的"获取ServiceManager"的过程。我们以MediaPlayerService在创建过程中通过defaultServiceManager()获取到ServiceManager代理对象的过程为例。

主要过程是ProcessState获取handle为0的代理对象,这里会向驱动发送一个PING_TRANSACTION命令,确认ServiceManager是否已经注册,然后创建一个BpBinder对象返回。

3.3 请求注册

这里在3.2中,MediaPlayerService已经获取到了一个ServiceManager的代理对象BpServiceManager,请求注册的过程则是通过addService()方法向其注册服务。 这个是客户端的调用部分,我们具体展开了一下,主要是通过BpBindertransact方法发送请求,其最终会在IPCThreadState中,通过talkWithDriver()方法来读写驱动,输入和输出。

而另一方面,servicemanager的代码在service_manager.c中,其在启动后,就已经调用binder_loop()进入到循环监听。在ioctl收到驱动返回数据后便会调用svcmgr_handler进行响应。ServiceManager会将请求注册的服务添加至保存的服务列表中。需要注意的是,这部分处理过程,也是发生在MediaPlayerService调用ioctl的过程中,此时是一个同步的过程。

3.4 服务端调用过程

在3.3节中我们探究了一下MediaPlayerServiceServiceManager注册的过程,ServiceManager并没有直接使用libbinder的接口通信,我们现在来看看当客户端调用MediaPlayerService的过程。

客户端部分通过BpBinder调用MediaPlayerService的某一接口的过程和3.3中调用ServiceManager的流程类似,这里也不再具体展开了,我们主要看看MediaPlayerService的服务实体,是如何响应请求的。

服务端进程会像之前文章所说的,在向ServiceManager注册服务后,就会创建监听Binder驱动的线程池,之后线程加入到线程池中进行监听。

在客户端发起调用请求,服务端线程会收到驱动返回的消息,然后从消息中提取出之前注册的服务实体对象的指针,最终调用onTransact()方法来调用对象的具体方法实现。

参考资料

  1. Android10.0 Binder通信原理
  2. Android系统RPC与Binder
  3. Android MultiMedia框架完全解析 - MediaPlayer的C/S架构与Binder机制实现
  4. Android Treble架构及HIDL添加示例
  5. Google文档:AIDL
相关推荐
青春给了狗7 分钟前
Android 14 修改侧滑手势动画效果
android
CYRUS STUDIO14 分钟前
Android APP 热修复原理
android·app·frida·hotfix·热修复
邪恶的贝利亚42 分钟前
一些有关ffmpeg 使用(1)
ffmpeg
火柴就是我1 小时前
首次使用Android Studio时,http proxy,gradle问题解决
android
limingade1 小时前
手机打电话时电脑坐席同时收听对方说话并插入IVR预录声音片段
android·智能手机·电脑·蓝牙电话·电脑打电话
浩浩测试一下2 小时前
计算机网络中的DHCP是什么呀? 详情解答
android·网络·计算机网络·安全·web安全·网络安全·安全架构
青春给了狗3 小时前
Android 14 系统统一修改app启动时图标大小和圆角
android
RenderNow3 小时前
深耕ffmpeg系列之AVFrame
ffmpeg
pengyu4 小时前
【Flutter 状态管理 - 柒】 | InheritedWidget:藏在组件树里的"魔法"✨
android·flutter·dart
居然是阿宋5 小时前
Kotlin高阶函数 vs Lambda表达式:关键区别与协作关系
android·开发语言·kotlin