Binder是什么?
“粘合剂”,可以理解为:粘合两个进程。它在不同的应用情景中,代表的意思不一样:
- 从通讯机制的角度,它是 binder 通讯机制,一种实现跨进程通讯的方式。
作用:在 Andorid 中实现跨进程通讯
- 从模型结构的角度,它是binder 驱动,一种虚拟的物理设备驱动( binder.c )。
作用:连接三大进程(Client 端进程、Server 端进程、ServiceManager 进程)
- 从安卓代码的角度,它是 binder 类,一个java类( android.os.binder.java )。
作用:在 Android 层用代码方式去实现 binder 通讯机制
什么是Stub-Proxy(桩-代理)
Stub 跟 Proxy ,俗称“桩-代理”模式,常见于不同进程间通信。Stub是服务端(应答端);Proxy是客户端(请求端)
Binder是Android中一个java类。aidl生成的java代码中,Stub类是继承于Binder类的,也就是说Stub实例就是Binder实例。
- Proxy是Stub的内部类
- A(端)和B(端)都是一个Stub
- 对于A请求----------》B处理过程
A.Proxy与B.Stub通讯
B.Stub处理消息,发送结果给A.Proxy - 对于B请求----------》A处理过程
B.Proxy与A.Stub通讯
A.Stub处理消息,发送结果给B.Proxy
asInterface()返回的Stub或Stub.Proxy:如果客户端和服务端在同一个进程下,那么asInterface()将返回Stub对象本身,否则返回Stub.Proxy对象。
一些基础背景
1、进程间通讯的本质
- 1)进程之间的内存是相互独立的,不能直接共享内存地址,也就是数据不能共享,不能直接通讯
- 2)进程A访问进程B,无非是A访问B的变量或方法
2、什么是C/S架构
- 一个进程要访问另外一个进程,本进程叫client,被访问那个叫server
3、Linux进程间通信(IPC)方式有7种:
- 管道( pipe )、有名管道、信号量、消息队列、信号、共享内存、套接字(socket),其中效率最高的是共享内存。
4、Andorid 提供了4种跨进程通讯的方法
- 分别对应着4大组件:Activity、Content Provider、Broadcast和Service。感兴趣的朋友可以了解一下,而这四大组件的底层通讯实现,都依赖Binder。
5、数据拷贝次数
- 共享内存不需要进行数据拷贝,binder 通讯方式需要一次数据拷贝
6、Android 五层架构图,Binder驱动 位于最下层,由下往上结构分为:
- Linux Kernael:主要包括(10个):binder驱动、显示、相机、蓝牙、内存、USB、键盘、WIFI、声音等驱动程序、电源管理 等驱动程序
- HAL:硬件抽象层
- Libraries 和 Android Runtime: C/C++ 底层库(SSL、OpenGL、WebKit、SQLite等);Dalvik虚拟机
- Framework层
- 应用层
7、aidl 是什么?为什么采用 aidl ?
- 因为进程间不能直接通讯,所以需要相同的通讯规范,所以看到C/S两端的aidl文件都是一样的
- 客户端和服务端都认可的一个通讯方式规范。假设两个不同国家的人,一个只会讲中文(client端)、一个只会讲日语(server端),他们要实现沟通,会采用英语进行交流(相同的aidl)
- aidl 工具位于 Sdk/build-tools/版本xx.x.x/aidl.exe,可以编译aidl文件,自动生成Stub(extends Binder)和Proxy类
8、Binder 驱动、Binder 引用、Binder 接口 是什么?
- 什么是Binder 驱动?
尽管名叫‘驱动’,实际上和硬件设备没有任何关系,只是实现方式和设备驱动程序是一样的:它工作于内核态,提供open(),mmap(),poll(),ioctl()等标准文件操作,代码位于linux目录的drivers/misc/binder.c中。
- 什么是Binder 引用?
binder 引用不是 android.os.binder 类对象,而是:被访问服务端 在binder驱动中注册的AIDL的唯一信息标识。
如何注册的?在Stub 的无参构造函数中,通过它的父类构造函数注册的。
java 知识点:子类执行无参构造函数,必先执行父类的构造函数,如果没有调用super指定那个,默认执行无参构造函数
- 什么是Binder 接口?
就是IBinder 接口,Binder类因为在framework 层实现了这个接口,具有跨进程传输的能力,因为Andorid 系统会为所有实现了该接口的对象提供跨进程传输服务。
9、AIDL 和 binder的区别:
- AIDL是一个文件格式,Android Studio 会调用本地sdk目录的aidl.exe 程序,对这个文件生成固定格式的java 文件,这个java文件由binder 子类组成,Android 底层会利用 binder 类实现IPC.
-
Android提供了AIDL方式(实际是通过binder)实现应用进程间通信(示例代码)
10、Binder 使用 Pacelable 实现序列化
在Intent、Bundle 传输对象时,需要对它实现序列化,实现序列化有两种方法:Serrialzable 和 Parcelable,后者由于性能好效率高,因此被用于 Binder 底层数据传输。原因详见
11、句柄是什么?
英文"handle",能够从一个数值拎起一大堆数据的东西都可以叫做句柄。我们可以这样理解Windows句柄:
- 数值上,是一个32位无符号整型值(32位系统下);
- 逻辑上,相当于指针的指针;
- 形象理解上,是Windows中各个对象的一个唯一的、固定不变的ID;
- 作用上,Windows使用它来标记各种对象,诸如:窗口、位图、画笔......可以通过句柄找到这些对象。
系统规定:任何 IPC 都必须使用句柄0来访问,Binder驱动为 ServiceManager 分配了 0号句柄,其余进程句柄都是一个的大于0的值
通讯过程
主要由4部分组成:Binder驱动、ServiceManager、Server、Client。下面分别对每个模块进行分析:
1、Binder驱动:
- 是所有IPC通讯的基础。Binder 不属于 Linux 系统内核的部分,但利用了 Linux 的动态内核可加载模块的机制,已经把它集成在 LinuxKernal 层。
- 所有进程访问binder 驱动,要先调用binder_open 函数打开binder设备驱动
(这过程中,会创建一个binder_proc 结构体,它里面有一个成员task_struct 结构体,用来记录着进程的各种信息和状态,如:线程表、binder节点表、节点引用表)!!!!!这个很重要,下面提到的所有进程访问都会用到这个特性!!!!!
2、服务端 注册:即 Server 向 ServerManager 注册
对应上图 2_1:初始化时,服务端(Server)提供参数(进程的名称),请求Binder驱动(binder.c),要求注册到ServiceManager。binder驱动把binder_proc (见上面第2点说明,下同)的信息发送给ServiceManager
对应上图 2_2:在ServiceManager中有一个链表svc list,每当向ServiceManager注册一个服务端,就会往svc_list链表添加一个节点(node),节点名称就是Server的名称
3、客户端 查询 服务端
对应上图 3_1:客户端(Client)根据Server服务名称,向Binder驱动(binder.c)请求查询ServiceManager 的链表。
对应上图 3_2:ServiceManager 遍历svc list 链表,找到与服务名称相同的节点(node)
对应上图 3_3:ServiceManager 把响应结果handle(整型值)回复给Client
对应上图 3_4:Client 收到 handle 值
4、客户端 调用 服务端
客户端根据handle 值,经过binder 驱动内部的一系列转化,将handle 的值对应到binder_pro 里面,进而找到Server 进程。
下面简述一下每部分的工作流程:
ServiceManager:
- ServiceManager是由系统的 init 进程启动的,不依赖任何Android 服务进程,在Linux系统中 init 进程是一切用户空间进程的父进程。源码位置:/frameworks/native/cmds/servicemanager/service_manager.c
- ServiceManager 工作流程:从service_manager.c 的 main 函数作为入口:
第一步:调用函数binder_open打开设备文件/dev/binder
第二步:调用binder_become_context_manager将自己注册为Binder进程间通信机制的上下文管理者;
第三步:调用函数binder_loop开启循环,监听Client进程的通信要求。
1_1)通过open函数,打开binder驱动(/dev/binder)
使用mmap。在内核空间,创建一个内存空间,应用层需要通过mmap拿到内存首地址
1_2)成为管理者:告诉binder驱动,我是各种服务的管理者(handle值为0。查询到注册的服务的handle值都是 >0)
1_3)循环。一直处于工作状态
1)读取数据----读取binder驱动,获取请求进程数据(Server 端注册的数据 / Client 端查询的数据)
2)解析数据----判断数据类型(注册 or 查询),如果是注册服务,在svc_list 链表添加数据;如果是查询服务,找到链表中内容
3)回复数据----回复一个handle值
源码位置:service_manager.c
Server 流程:
2_1)open函数,打开binder驱动。/dev/dbinder
mmap函数 在内核空间,创建一个内存空间,应用层需要通过mmap拿到内存首地址
2_2)注册服务---向serviceManager注册服务
2_3)循环
1)读取数据---除了读取servicemanager返回的数据外,重点是读取client 端数据
2)解析数据---解析client数据,得知client 到底想要调用server的哪个函数,然后server内部执行这个函数
3)回复数据---把执行函数的结果,告知client
Client 流程:
3_1)open函数,打开binder驱动。/dev/dbinder
mmap函数 在内核空间,创建一个内存空间,应用层需要通过mmap拿到内存首地址
3_2)查询服务---向ServiceManager查询,得到handl整数值,
3_3)调用服务---根据handle值,通过驱动执行server的函数
命令码:
ServiceManager、Server、Client 三者之间的任意交互,要实现进程间的传输,都需要通过命令码来实现:
1、数据传输:
请求方 发送命令码 BC_Transaction---> 经过binder驱动后,该命令码会被转换成 BR_Transaction ---> 接收方得到BT_Trasaction
2、数据回复:
接收方 发送命令码 BC_Reply----> 经过binder驱动后,该命令码会被转换得到 BR_Reply ---> 请求方 得到Br_Reply
一个不恰当的比喻:单身汪找媳妇
背景说明:单身汪(Client)找媳妇(Server),要通过婚介所(ServiceManager)穿针引线,婚介所要到政府机关(Binder驱动)备案注册。
1)单身汪(Client)按政府部门(Binder驱动)指引,到正规婚介所(ServiceManager)发起请求:我要找媳妇(Server),年龄多少、身高多少、体重多少,等等
2)婚介所(ServiceManager)按照要求,找到符合条件的媳妇(指定的Server),把她的号码告诉单身汪(Client),让他俩私聊。
3)单身汪(Client)找到了媳妇(Server)进行约会(传输通讯)
4)上述流程环节都要通过政府部门(Binder驱动)的监督、指引,在合法范围内办事。
借用张图:
先来理清几个类:
- IBinder : IBinder 是一个接口,代表了一种跨进程通信的能力。只要实现了这个借口,这个对象就能跨进程传输。
- IInterface : IInterface 代表的就是 Server 进程对象具备什么样的能力(能提供哪些方法,其实对应的就是 AIDL 文件中定义的接口)
- Binder : Java 层的 Binder 类,代表的其实就是 Binder 本地对象。
BinderProxy 类是 Binder 类的一个内部类,它代表远程进程的 Binder 对象的本地代理;
这两个类都继承自 IBinder, 因而都具有跨进程传输的能力;
实际上,在跨越进程的时候,Binder 驱动会自动完成这两个对象的转换。 - Stub : AIDL 的时候,编译工具会给我们生成一个名为 Stub 的静态内部类;
这个类继承了 Binder, 说明它是一个 Binder 本地对象,它实现了 IInterface 接口,表明它具有 Server 承诺给 Client 的能力;
Stub 是一个抽象类,具体的 IInterface 的相关实现需要开发者自己实现。
每一个AIDL生成的文件,由两部分组成:Stub(存根)和Proxy(代理),以进程A、B两者通讯为例:
1、请求:进程A通过Proxy通过访问Binder驱动,Binder驱动找到进程B的Stub
进程A的Proxy会在callMyService中调用transact,通过JNI连接到IBinder驱动,IBinder驱动会通过native代码找到进程B的AIDL,找到后调用AIDL的Stub,回调onTransact方法
transact是连接IBinder引用后写数据,onTransact是IBinder引用连接Stub接受数据
2、响应:进程B通过自身的Proxy访问Binder驱动,Binder驱动把结果响应给进程A的Stub
进程A 访问 进程B,先是A 根据自己的aidl标识,通过JNI调用NDK,找到进程B 在binder驱动程序中的 binder引用。因为A、B进程的aidl标识都是类名、路径那些都是一样的,所以binder驱动可以很轻易找到进程B的信息,然后根据类名执行C/C++代码,访问B进程中的java层的变量和方法
为什么要用IPC
IPC在我们的应用开发过程中随处可见,下面我将举一个例子来说明他的重要性。
我们在MainActivity修改一个静态变量,接着在另一个进程的SecondActivity中去访问该变量,看看能否读取已经修改过的变量。
1、新建一个Student类,并声明一个静态变量
public class Student {
public static String name="BOB";
}
2、在MainActivity的onCreate方法中修改name的值,并打印log
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Student.name = "JACK";
Log.d("MainActivity:Sname=", Student.name);
}
3、将SecondActivity设置为新进程,并在其onCreate方法中访问name
<!-- 在清单文件中通过android:process属性为SecondActivity指定特定的进程:com.bob.aidltest:second -->
<activity
android:name=".SecondActivity"
android:process=":second">
</activity>
public class SecondActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.second_activity);
Log.d("SecondActivity:Sname=" , Student.name);
}
}
通过log,可以看到在MainActivity中修改了name的值,但是在SecondActivity中却无法读取修改后的值
通过以上的实验,大家应该明白了一点:
在不同的进程之间访问同一个静态变量是行不通的。其原因是:
每一个进程都分配有一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机上访问同一个对象会产生多个副本。
例如我们在MainActivity中访问的name的值只会影响当前进程,而对其他进程不会造成影响,所以在SecondActivity中访问name时依旧只能访问自己进程中的副本。