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
相关推荐
兰琛2 小时前
Compose组件转换XML布局
android·xml·kotlin
水w4 小时前
【Android Studio】解决报错问题Algorithm HmacPBESHA256 not available
android·开发语言·android studio
隐-梵6 小时前
Android studio进阶教程之(二)--如何导入高德地图
android·ide·android studio
Kika写代码6 小时前
【Android】界面布局-线性布局LinearLayout-例子
android·gitee
wangz766 小时前
kotlin,jetpack compose,使用DataStore保存数据,让程序下次启动时自动获取
android·kotlin·datastore·jetpack compose
mortimer7 小时前
半开源语音克隆神器 MegaTTS3:安装难、用起来更难?手把手从安装到使用
开源·github·音视频开发
Thread.sleep(0)8 小时前
WebRTC源码解析:Android如何渲染画面
android·webrtc
Android 小码峰啊9 小时前
Android Dagger 2 框架的注解模块深入剖析 (一)
android·adb·android studio·android-studio·androidx·android runtime
Android 小码峰啊9 小时前
Android Fresco 框架缓存模块源码深度剖析(二)
android
大胃粥11 小时前
Android V app 冷启动(8) 动画结束
android