Binder通信机制初探

335 阅读14分钟

Binder 机制

  • Binder是什么

    • 概述:

      • Android中的血管,Android中四大组件与AMS进行通信时,需要借助Binder
    • 属于IPC机制一种:任何类继承自Binder之后就拥有IPC能力

      • Client与Server两端通信

        image-20220422142617977

    • 从驱动角度:Binder是一个虚拟物理设备驱动

      • Binder没有对应的实际物理设备(例如网卡,显卡)
      • android底层是Linux,一切皆文件
    • 从应用层角度:Binder是一个发起通信的Java类

      • 在使用的时候,一般是写一个AIDL,此时在build目录下会生成一个接口文件(用于客户端与服务端通信,相当于一个接口协议)
  • Binder应用层分析:

    • 整体来说,分为客户端与服务端

      • 两端的自动生成的文件中的类结构的三个东西是一样的
    • 首先写一个AIDL接口

      image-20220422143624861

    • 系统在Build目录下会使用AIDL工具生产一个同名接口文件,作为IPC协议

      • 生成同名文件结构

        • Default:默认实现

        • Stub:抽象类

          • 继承自Binder,从而拥有IPC能力
          • 开放给另一端进行完善,实现IPC通信
        • Proxy

        • 自定义的接口方法

多进程的使用及其优势

  • 为什么需要多进程:

    • 概述:成熟的应用一般是多进程

      • 虚拟机分配给各个进程的运行内存有限制,LMK优先回收对系统资源占用较多的进程
    • 突破进程内存限制

      • 因为单个进程的内存是有限,一个进程可以看成是一个盒子

      • 盒子大小由手机厂商决定的,例如 384MB

        • 查看命令:

          1. adb shell
          2. getprop dalvik.vm.heapsize
    • 功能稳定性:独立的通信进程保持长连接的稳定性

      • 比如推送功能一般开一个进程处理
    • 规避系统内存泄漏:单独的WebView进程阻隔其内存泄漏

      • WebView在正常使用的时候会导致内存泄漏

        • 内存泄漏的原因:

          • WebView 销毁的时候,其 destroy 和 onDetachedFromWindowInternal 方法最后会调用到 AwContents 中对应的方法,低版本的内存泄漏就发生在这里。
          • AwComponentCallbacks 类的实现,发现它是 AwContents 中的一个非静态内部类,因此它会持有外部 AwContents 实例的引用,而 AwContents 持有 WebView 的 Context 上下文,对于 xml 中的 WebView 布局而言,这个上下文就是其所在的 Activity,因此如果在 Activity 生命周期结束后没有调用 unregisterComponentCallbacks 方法解注册的话,便可能会发生内存泄漏
        • 解决手段:

          • 在 Activity.onDestroy 的时候将 WebView 从 View 树中移除,然后再调用 WebView.destroy 方法:
          • 一般单独开一个进程,用完就杀掉这个进程
    • 隔离风险:对于不稳定的功能放入独立的进程,避免导致主进程崩溃

    • 多进程导致的问题:

      • application会调用多次,一个进程调一次

Binder的优势

  • Linux内置了许多IPC机制

    • 管道、消息队列、共享内存、信号、信号量、Sockect
  • Binder与传统IPC的对比

    • 拷贝次数:

      • Binder一次拷贝:就是为了更好用
      • 一般认为除共享内存外(0次),其余IPC两次
      • 性能:共享内存>Binder>其余IPC
    • 功能特点:

      • Binder基于C/S架构,易用性高,结构清晰

        • Android中通过AIDL的调用甚至感知不到IPC

        • 具体调用

          • 客户端:

            • bindService:传intent

            • ServerConnection:

              • 链接成功

                • 拿到句柄(就是自定义的AIDL接口)
              • 链接失败

            • initView中通过句柄.addPerson(new Person())就行了

      • 共享内存:控制复杂

        • 线程本来就是共享进程内存的;

          • 线程安全:死锁
          • 信息不共享
        • 进程之间还是有共享导致的问题

      • Sokect:基于CS架构但其作为通用接口,传输效率低,开销大;

      • 从特点:Binder > 共享内存 > 传统IPC

    • 安全性:

      • Binder:

        • 为每个APP分配UID

          • Linux底层决定UID为APP唯一标识;
          • 做了坏事,就可以找到你;
        • 支持实名与匿名

          • 实名:谁都可以访问

            • 实名服务:系统开放给外界的

              • AMS,WMS
          • 匿名:就像滴滴打车,不能直接找到司机,系统给了一个虚拟号码,整体来说是一个代理;通过系统拿到服务的代理对象,通过这个代理对象找到服务

            • 匿名服务:自己的服务一般是匿名,当然自己的服务也可以变成实名服务
          • 实名与匿名的区别:有没有在ServerManager中注册

            • 注册了:就是实名
      • 共享内存等传统IPC的问题

        • 依赖上层协议

          • 就像手写录入信息(信息录入全靠程序),导致无法跟踪

          • 网络通信角度:

            • 根据协议传数据包,由应用层填写再发送到服务端

            • 应用层相当于Service,服务端相当于系统;

              • Service依赖协议,将信息拿给系统
              • 但是信息是Service自己写的,不安全
        • 访问接入点开放--->不安全

          • 类似于公交车站点,谁都可以上车,进行访问
          • 这样就会导致易于攻击

系统内存划分与寻址:

  • 进程内存划分示意图:

    image-20220422152146621

  • 进程寻址:受机器影响(地址总线长度)

    • 示意图:

      image-20220422170611738

  • 寻址细节:

    • 32位机器--->总体地址空间4G(有32条地址总线决定的)

      • 用户空间:3G,虚拟内存,用户态
      • 内核空间:1G,物理内存,内核态
    • 常规寻址过程:虚拟内存与物理内存两者通过MMU联系

      • 用户空间--->内核空间:copy_from_user

        • 用户申请系统调用
        • 调用由内存为主
      • 拷贝次数:指系统调用的次数

        • copy_from_user:用户--->内核
        • copy_to_user:内核--->用户
    • 寻址带来的问题

      • 用户态与内核态之间切换:

        • 上下文切换:耗资源

          • 保存当前运行的状态;
        • 涉及到中断并且耗时

    • 数据流动过程:

      • 数据在进程1的用户空间中--->进程1的内核空间:copy_from_user

      • 进程1的内核空间与进程2的内核空间是共享的

        • 所有进程中的内核空间(藏宝图)映射到同一块物理内存(终点)
      • 进程2的内核空间到进程1的用户空间:copy_to_user

    • Binder优化手段:共享内存性能好但不好用,传统IPC好用但性能不好

      • 将进程2的内核空间与进程2的用户空间通过mmap映射到同一块物理内存上;

      • 相当于进程1的内核,进程2的内核,进程2的用户指向同一块物理内存

      • 当然也可以再次优化成共享内存(mmap),虽然性能好,但是难用

      • 为什么是优化这一端?

        • 客户端实现太灵活了

          • Proxy,直接继承自定义的AIDL接口
        • 所以自己搞的AIDL接口,生成了同名接口文件中的Stub继承自Binder(拥有IPC能力),同时Binder是由Google工程师弄的

    • 64位机器:

      • 用户空间
      • 内核空间
      • 无用空间
    • 虚拟内存:

      • 软件工程师所说的都是虚拟内存
      • 变量的地址也是虚拟内存
      • 虚拟内存与物理内存之间的映射由MMU管理
    • 物理内存:

      • 内存条
      • 不同进程中的用户空间映射到不同物理内存上
      • 所有进程中的内核空间(藏宝图)映射到同一块物理内存(终点)

MMAP

  • MemoryMapping是什么?

    • 内存映射(MMAP):Linux通过将一块虚拟内存区域与一个磁盘上的对象关联起来,从而初始化这个虚拟内存区域的内容
  • MMAP好处:加快文件读写速度

    • 最开始的时候,用户空间是不能直接操作磁盘对象的,需要通过内核程序;
    • 引入MMAP,将用户空间(虚拟内存)与磁盘对象(物理内存)直接关联起来
  • 虚拟内存--->物理内存

    • MMU:内存管理单元处理

      • 硬件实现,用户透明
  • MMAP具体使用:实现虚拟内存--->物理内存,需要去创建文件

    1. 搞一个物理内存:创建指定大小(一页)的虚拟内存块

      • 打开文件:获取文件描述符(通过文件路径,Linux中一切皆文件)
      • 获得一页内存大小:Linux中内存分页,以页为单位(一般32位机器,4kb一页)
      • 将文件设置为一页内存这么大(大小限制问题无所谓)
    2. 调用MMAP:获得一个虚拟内存块句柄(指针)

      • 实现一块一页大小的虚拟内存指向了文件,并返回这个虚拟内存的引用
      • 虚拟内存指向物理内存(大小一致的)
      • 返回虚拟内存的指针,那么我们通过这个指针写入文件的时候,就直接干到具体的物理内存了;因为磁盘读写与内存读写速度是不一样的;
    3. 向指针中写入数据就到位了:就跟个快捷方式一样的效果

    • 实现细节:

      • 整体由C++实现的并且需要开放权限

      • 源码体现:

        • 内核中会去调用一个bind_mmap
      • 还有个MMKV的东西

    • 为什么要用文件描述符:

      • Linux下一切皆文件;
      • 需要去指定一块物理内存,那么就需要用文件来处理就行了;
    • 使用场景:需要提高文件读写性能

      • MMKV:

      • Binder

      • 使用这个MMAP就需要去创建文件,Linux底层

AIDL:Android IPC

  • AIDL是什么?

    • 帮助用户去代理一些规则(IPC规则):主要目的就是简化Binder的使用,

      • 实现Binder的使用,就像黄牛
    • 但是与Binder是不同的东西

  • Android Studio中的内置工具:.aidl

    • 通过内置工具根据我们自定义的AIDL接口生成对应的 Java文件

    • 在哪里:

      • sdk/build-tools/随便整个版本30.0.0/aidl就在这个里面
      • 通过 .aidl文件生成对应的java文件
    • APT也是这个样子

  • 手写AIDL自动生成的代码:

    • 结构:一个接口+两个类

      • 接口:实现IPC通信协议

      • 代理对象Proxy:

        • 由客户端使用,实现IPersonManager接口
      • Stub:

        • 由服务端使用
      • Default:这个不用管

    • Java代码生成细节:

      • Proxy:实现在客户端调用方法后服务端也会调用同名方法

        • asInterface:区分是否为跨进程通信(是,返回代理对象(return new Proxy(Binder));不是,返回服务端对象(return (IPersonManager)))

        1. 获取代理对象

        2. 客户端调用bindService

           bindService(intent, connection, Context.BIND_AUTO_CREATE);
          
        3. 系统回调onServiceConnected(iBinder对象)

           public void onServiceConnected(ComponentName name, IBinder service) {
          
          • 这个IBinder对象是服务端的IBinder对象的代理

            • 通过Service Manager与AMS拿到的
        4. 调用Stub.asInterfce(service),返回iPersonManage(服务端的代理对象)

           iPersonManager = Stub.asInterface(service);
          
        5. 在客户端中调用addPerson方法:跑到服务端中的addPerson 方法

           iPersonManager.addPerson(new Person("Leo", 3));
          
      • ClientActivity:通过代理对象调用Proxy中的方法:主要帮助进入Binder

        1. 打包:两个包

          • 客户端到服务端的数据:data

          • 服务端返回给数据的包(repl)

            • (C->S的时候是空的)
            • S->C的时候就有东西
        2. 安全检测:判空

        3. mRemote.transact(Stub):C->S,进入Binder,Binder在进入服务端

          • 处理Binder的过程:

            • 客户端到Binder
            • Binder到服务端
          • Binder通信机制实现:这个里面就牵涉到Service Manager

            • Service Manager:服务的大管家,所有服务都在这个里面注册,类似于一个电话簿

            • 例如,客户端需要拿到AMS服务

              1. 客户端先告诉管家,要找AMS
              2. 管家将AMS的代理对象返回给客户端
              3. 客户端通过AMS的代理对象与AMS进行通信
        4. 服务端:进入Stub调用onTransact方法,这个里面就会去调用对应的方法

          • 怎么找的?

            • 一般情况下,需要去调用另外一个类或者框架的方法,使用全类名,一个String但是占用的位置大,那么进行优化

            • 优化手段:因为CS两端生成的AIDL代码都是一样的(协议是一样的),那么我们就可以使用一个代号来标识方法

              • stub内部是使用static final int 来玩的
            • 通过一个switch-case,根据传入的code来玩

              • 如果有返回结果,那么写入reply包
              • 没有就不写
          • flag为0:同步,mRemote.transact(Stub)

            • 在客户端调用,客户端等待服务端的返回,如果说在主线程中会导致ANR

            • 一般都是同步调用,也可以设置成异步调用

              • 将flags设置为one way

        • 比如客户端想要与AMS通信

          1. AMS 在创建后会在Service Manager中去注册的
          2. 客户端首先找到Service Manager
          3. Service Manager找到AMS并将AMS的代理对象返回给客户端
        • 为什么要有Service Manager

          • 就像电话本一样,没有这个,那么我要去记录所有的服务
          • 就像一个中介,它还是一个中介
        • 那么客户端是怎么去找到Service Manager的?

          • 通过handle = 0的一个句柄,这个是固定的

            • 通过注册来的
          • 而WMS,AMS的句柄是不定的

            • 当AMS创建后向Service Manager中去注册;将自己的名字啊,handle 啊告诉它;
        • SystemManager与Service Manager

          • 前面那个就是进程:相当于app

          • 后面那个是进程里面的服务:相当于activity/service

四大组件底层通信机制:Binder

  • 概述:客户端调用bindService之后系统是如何回调到onServiceConnected的?

    • 怎么跨进程通信的?
  • 调用流程思路:

    • 示意图:

      image-20220429144125282

    • 详细流程:

      1. 客户端调用bindService,进入AMS中执行bindService

      2. Servicemanager.getService("activity")

        • 这个Activity就是AMS
      3. 客户端拿到AMS的代理对象IBinder,跨进程

        • 调用AMS中的bindService方法,进入AMS
        • 四大组件基于AMS
      4. AMS中干了什么?

        • 如果服务端APP进程未创建,那么AMS就会创建服务端进程
        • AMS调用scheduleBindService(启动服务)---》handleBindeService
    • handleBindService 中干了什么?跨进程

      1. 拿到map集合(mService)中的Service对象(s--->RemoteService)

        • 在handleCreateService中创建服务

          • 通过反射进行创建
          • 接着调用service.onCreate()
          • 最后将创建的服务放入map集合mService
      2. 调用s.onBind()拿到IBinder对象

      3. 通过publishService将IBinder对象返回回去

  • 示意图:bindService(intent)---》回调onServiceConnected(iBinder对象),就是这个箭头

    • 过程:

      1. 客户端调用bindService
      2. ServiceManager.getService(AMS),返回AMS的IBinder对象

    image-20220422174250406

    image-20220422174406777

image-20220422174433337

Intent为什么不能传递大数据

  • 现象:

    • Intent传递数据不能超过一MB,Intent底层是Binder通信

    • 其实不能超过1MB - 8k,等于也不行;

      • 需要预留出打包的空间;
  • 那么这个Binder在哪里限制的大小?

    • 在内存映射的哪里;
    • Binder中的驱动功能,对应的驱动文件
  • 源码分析:ProcessStart.cpp

    1. openDriver(driver) :打开驱动文件,得到文件描述符

      • 这个driver就是"/dev/binder"
    1. mmap:将这个文件描述符映射到"/dev/binder"上

      • 注意此时就会指定大小:BINDER_VM_SIZE;

        • 宏定义决定了1MB(1 * 1024 * 1024)-8k(2页的大小)

        • 这是同步的大小,异步是同步/2(因为内存不能一致给)

        • 为什么设置成这个样子?

          • 首先不能设置成无限大
          • 其次Google工程师决定的
      • Binder通信由共享内存做了局限;
      • Binder线程池大小:15
  • 源码分析Binder.c

    • mmap就会调用底层的内核代码(binder_map)

      • 将服务端的一块区域与内核的一块区域,同时指向同一块物理区域

      • 流程:

        1. 创建一块物理区域
        2. 将服务端区域(虚拟区域)指向物理区域
        3. 将内核区域指向物理区域
      • 给的大小:

        • 最开始不是1MB - 8K
        • 最开始注册服务的时候,只给一页(4kb),有需要再给你
        • 一页4kb就是最小单位
    • 比较重要的方法:

      • binder_ioctl:write,read;

        • 客户端写服务端读;或者换着来
      • binder_map:MMAP,之前在这个里面规定了异步就是同步的一半,之前是写的很长,后面去看的时候;这个减半操作封装在了一个新的方法里面了

    • 四大组件:一般都是同步,异步很少;

    • 关于1MB - 8k

      • 直接写这么大,其实是用不了的,log日志会提示尺寸太大
      • 因为数据会打一层包,在传输数据之前要预留一点空间出来;