主要参考文章:
补充:一份Binder机制深度面试问题
Binder 是 Android 系统独有的进程间通信(IPC)方式之一。不同于 Linux 原有的 IPC 实现方式(Pipe、Signal、Socket、Share Memory)。
(1)性能:使用内存映射机制实现数据的传输只 copy 一次。
(详细讲解: Android Bander设计与实现 - 设计篇 - 6 Binder 内存映射和接收缓存区管理)
Binder 借助了内存映射的方法,在内核空间和接收方用户空间的数据缓存区之间做了一层内存映射,就相当于直接拷贝到了接收方用户空间的数据缓存区,从而减少了一次数据拷贝。
参考自:Binder在通信时,为什么只需要一次拷贝?
结合设计与实现一文的第 6 点,简单的说就是由 Binder 驱动负责管理数据接收缓存,首先会基于内存映射,接收方的用户空间会对应着驱动内一块用于缓存接收方数据的内存空间(实际上就是内核空间),这块内存是由驱动来维护管理的,且不用接收方来提供,当驱动将发送方的数据拷贝到该内存之后,因为接收方基于内存映射机制,其空间就对应着内核中数据存储的空间,因此整体上来说是一次拷贝。
IPC 方式性能对比:
(1) 共享内存:虽然无需拷贝,但控制复杂,难以使用
(2) 消息队列、管道:采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程
(3) socket:作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信
进程空间是 Linux 基础知识,每一个进程的内存空间分为 User Space 和 Kernel Space,
User Space 存放应用程序的代码和数据,是进程隔离的,但是 Kernel Space 存放内核代码和
数据,是内核和所有进程共享的——这才是能真正做到 IPC 交互的根本原因
正常情况下通过系统调用 copyFromUser 将 Server 的数据从 Server 进程的 User Space copy
到 Kernel Space, 处理后再由 Kernel Space copyToUser 到 Client 进程的 User Space,
从而进行 IPC 数据交互。
IPC 的本质是进程 User Space 和 Kernel Space 的数据交互或转换。
(2)安全:在内核空间添加身份标识(传统IPC没有,依赖上层协议控制),从而提供 API 可以区分 Caller uid, pid 进行包名、签名校验等;(校验方法除包名、签名外还可以加简单的 custom permission)
(3)易用:是封装的很好的 C/S 架构,是面向对象的思维和调用方式。AIDL 自动生成,屏蔽细节,整个 AIDL 目录放到 Client 下,然后再有 Proxy,相当于直接有一个 Server 的引用 Object,十分易于理解和方便调用,Client 调用 Proxy, Server 调用 Stub。比 Socket/Signal/Share Memory都简单。
该框架定义了四个角色:Server,Client,ServiceManager(以后简称 SMgr)以及 Binder 驱动。其中 Server,Client,SMgr 运行于用户空间,驱动运行于内核空间。
Binder 驱动:Binder 驱动是一个名词,和传统的 “驱动” 不同,与实际硬件设备无关,只不过其实现方式类似于驱动。本质上是基于 C++ 实现的代码,即 Linux 底层驱动,是 Server/Client/ServiceManager 通信的桥梁,通过内存映射机制在内核实现 IPC,持有 Server 在 Kernel Space 的 Binder 实体,负责进程之间 Binder 通信的建立,Binder 在进程之间的传递(给 Client 提供 Binder 实体的引用,即我们说的 Remote Object),Binder 引用计数管理,数据包在进程之间的传递和交互等一系列底层支持。且驱动和应用程序之间定义了一套接口协议。
ServiceManager:将字符形式的 Binder 名字转化成 Client 中对该 Binder 的引用,使得 Client 能够通过 Binder 名字获得对 Server 中 Binder 实体的引用,是 Binder Driver 和 Server/Client 之间的上下文管理者,负责 Service 的注册、查询。
一个 Binder 实体可以发送给其它进程从而建立许多跨进程的引用;另外这些引用也可以在进程之间传递,就象 java 里将一个引用赋给另一个引用一样。为 Binder 在不同进程中建立引用必须有驱动的参与,由驱动在内核创建并注册相关的数据结构(该数据结构即为内核中的 Binder 实体)后接收方才能使用该引用。
简单的说,Binder 跨进程传输并不是真的把一个对象传输到了另外一个进程,只不过是 Client 端通过 SMgr 获得在 Server 中目标 Binder 的“引用”—— Binder 的代理对象(这是一个新的对象),然后进行操作(即调用代理对象的方法),然后这一“操作”会通过以 指令+数据
的形式通过驱动传递给 Server 端真正的 Binder 实体,使 Binder 解析并实现这一“操作”,对应的从真正意义上调用目标方法,之后将结果再利用驱动再返回给 Client 端。
即 BInder 在 Client、Server、驱动以及数据传输时的存在形式。
请参考 Android Bander设计与实现 - 设计篇 第 5 点。
请参考 Android Bander设计与实现 - 设计篇 第 7 点。
基于 Android:IPC之AIDL的学习和总结 这篇文章进行 AIDL 的实践,并在部分示例代码中添加了一些打印日志内容。
public class BookManagerService extends Service {
//支持并发读写
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
//服务端定义Binder类(IBookManager.Stub)
private Binder mBinder = new IBookManager.Stub() {
int count = 0;
// 在 zzq_process 进程的主线程中调用
@Override
public List<Book> getBookList() {
Log.d("test", "-------- start --------");
Log.d("test", "currentThread =" + Thread.currentThread());
Log.d("test", "-------- end --------");
return mBookList;
}
@Override
public void addBook(Book book) {
count++;
mBookList.add(book);
}
};
@Nullable
@Override
public IBinder onBind(Intent intent) {
// 每次都是返回的同一个 binder 实例,即 mBinder,
// 此时如果有多个客户端同时引用 mBinder 的方法,那么就会引起冲突,
// 因此就有 Binder 线程池的存在,通过线程池,就可以实现并发处理
// 但需要注意的是,在线程池的不同线程中调用的都是同一个 mBinder,
// 其内部的状态共享,如 count 的值,
// 只不过是同时调用 mBinder 的方法(方法的本质就是一段指令,达到复用的目的),
// 因为该方法里面每次都是返回的同一个 binder 实例;
// 当然,也可以每次都返回一个新的实例,此时线程池依旧存在,因为这是一种通用设计
// 并不会因为客户端引用的 binder 实例不同而有所区别
return mBinder;
}
}
之后在客户端调用的时候,每次调用,日志打印的线程的内容可能不一样:
//第一次打印内容:
currentThread =Thread[Binder:16647_2,5,main]
//第二次打印内容
currentThread =Thread[Binder:16647_3,5,main]
这就间接证明了 Binder 线程池的存在,因为对于 Server 进程 S,可能会有许多 Client 同时发起请求调用同一个 Server 中的 Binder 实体,为了提高效率往往开辟线程池并发处理收到的请求。
另外,对于服务端的进程来说,(在 Android 中)也会有主线程以及 ANR 的概念,因此在主线程中同样不能做耗时的操作,如远程 Service 的 onCreate 就是运行在主线程的。 其实,可以看作是一个新的 App 进程。
另外,补充一个有关于 Binder 的点,在 Android中Parcel的解读 中有介绍到:
Parcel 还包含了一个活动的 IBinder 对象的引用,这个引用导致另一端接收到一个指向这个 IBinder 的代理 IBinder
以及:
Parcel 的一个非同寻常的特性是读写活对象的能力。对于活动对象,它们的内容实际上并没有写入,而是仅写入了一个令牌来引用这个对象。当从 Parcel 中读取这个对象时,你不会获取一个新的对象实例,而是直接得到那个写入的对象。有两种活动对象可操作:Binder对象、FileDescriptor对象。
这里需要注意,即 Parcel 在传递 IBinder 对象的时候,本质上是传递一个 IBinder 对象的引用(仅写入了一个令牌来引用这个对象),而不是得到一个新的对象实例。这样在 Binder 机制中,就可以实现在本地进程中利用本地 Binder 对象来间接引用远程服务中的 Binder 本体。