Android之Binder学习(IPC跨进程通讯)

Binder是什么?

“粘合剂”,可以理解为:粘合两个进程。它在不同的应用情景中,代表的意思不一样:

  • 从通讯机制的角度,它是 binder 通讯机制,一种实现跨进程通讯的方式。
    作用:在 Andorid 中实现跨进程通讯
     
  • 从模型结构的角度,它是binder 驱动,一种虚拟的物理设备驱动( binder.c )。
    作用:连接三大进程(Client 端进程、Server 端进程、ServiceManager 进程)
     
  • 从安卓代码的角度,它是 binder 类,一个java类( android.os.binder.java )。
    作用:在 Android 层用代码方式去实现 binder 通讯机制

 

一些基础背景

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。下面分别对每个模块进行分析:

Android之Binder学习(IPC跨进程通讯)_第1张图片

1、Binder驱动:

  1. 是所有IPC通讯的基础。Binder 不属于 Linux 系统内核的部分,但利用了 Linux 的动态内核可加载模块的机制,已经把它集成在 LinuxKernal 层。
  2. 所有进程访问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 

 

一个不恰当的比喻:程序猿找媳妇

1)程序猿(Client),想要找媳妇(Server),要通过婚姻介绍所(ServiceManager)介绍,而婚姻介绍所(ServiceManager)要到政府机关(Binder驱动)注册企业信息、接受行政管理。
2)程序猿(Client)想找一个称心如意不跑路的媳妇(Server),需要按照政府部门(Binder驱动)指引,到正规婚姻介绍所(ServiceManager)提出要求即可:年龄、身高、三围、工作、长相等等。
3)婚姻介绍所(ServiceManager)按照要求,找到了凤姐(指定的Server),告诉程序员(Client)凤姐的电话号码,让他们两个私下联系。
4)程序猿(Client)跟凤姐(Server)进行约会(传输通讯)
5)上述流程环节都要通过政府部门(Binder驱动)的监督、指引,在合法范围内办事。

 

借用张图:

Android之Binder学习(IPC跨进程通讯)_第2张图片

 

 

 

先来理清几个类:

  • 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




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中却无法读取修改后的值
Android之Binder学习(IPC跨进程通讯)_第3张图片
通过以上的实验,大家应该明白了一点:
在不同的进程之间访问同一个静态变量是行不通的。其原因是:
每一个进程都分配有一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这就导致在不同的虚拟机上访问同一个对象会产生多个副本。

例如我们在MainActivity中访问的name的值只会影响当前进程,而对其他进程不会造成影响,所以在SecondActivity中访问name时依旧只能访问自己进程中的副本。

 

 

 

未完,待续.....

 

 

 

 

你可能感兴趣的:(Android)