Android Binder机制

1:内存映射,一次拷贝

1)内存映射(mmap:memory mapping):

所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络读写数据等等。

用户控件通过系统调用让内核空间完成这些功能,所以比如写文件,需要进行两次拷贝,先从用户控件到内核空间,再从内核空间到磁盘文件:

写文件流程:

   1)调用write,告诉内核需要写入数据的开始地址和长度

   2)内核将数据拷贝到内核缓存

   3)由操作系统调用,将数据拷贝到磁盘,完成写入。

在传统的文件写入方式中,数据需要先从用户空间复制到内核空间,然后再由内核空间进行IO操作,将数据写入磁盘文件。这涉及到两次数据拷贝。

而使用mmap的方式,通过内存映射文件,将磁盘文件映射到用户空间的内存中。这样,用户空间对内存的写操作直接映射到了磁盘文件上,跳过了内核空间,从而避免了一次数据拷贝,提高了写入效率。

使用mmap可能会带来其他方面的考虑,比如内存占用、并发访问等,但它在某些场景下可以提供更高的性能和效率。

2)为什么使用虚拟内存:

  1. 虚拟内存连续性与物理内存不连续性

    • 虚拟内存需要在逻辑上是连续的,这样才能提供给进程一个统一的地址空间。但在物理上,物理内存的分配和释放是动态的,导致物理内存不一定是连续的。
    • 通过虚拟内存和物理内存的映射关系,虚拟内存页可以在物理内存上是不连续的,这样可以更高效地利用物理内存,提高内存的使用效率。
  2. 部分虚拟内存需求映射到物理内存

    • 当一个应用程序使用的虚拟内存空间非常大时,不是所有的虚拟内存都需要实时映射到物理内存上。部分虚拟内存可以暂时不需要在物理内存中。
    • 虚拟内存系统可以根据需要,将部分虚拟内存映射到物理内存中,而将其余部分暂时存储在硬盘上,只有在需要时才加载到物理内存中。这种方式提高了物理内存的使用效率,允许操作系统在有限的物理内存下运行更多的程序。

 

1、内存分页如何实现内存管理?

分页是把整个虚拟和物理内存空间切成⼀段段固定尺⼨的⼤⼩。这样⼀个连续并且尺⼨固定的内存空间,我们叫Page)。在 Linux 下,每⼀⻚的⼤⼩为 4KB

​ 虚拟地址与物理地址之间通过⻚表来映射。页表存储在内存中,通过MMU(内存管理单元)来实现虚拟内存到物理内存的转换。⽽当进程访问的虚拟地址在⻚表中查不到时,系统会产⽣⼀个缺⻚异常,进⼊系统内核空间分配物理内存、更新进程⻚表,最后再返回⽤户空间,恢复进程的运⾏。

2、怎么解决内存碎片和效率低?

​ 由于内存空间都是预先划分好的,也就不会像分段会产⽣间隙⾮常⼩的内存,这正是分段会产⽣内存碎⽚的原因。⽽采⽤了分⻚,那么释放的内存都是以⻚为单位释放的,也就不会产生无法给进程使⽤的小内存

​ 如果内存空间不够,操作系统会把其他正在运⾏的进程中的「最近没被使⽤」的内存⻚⾯给释放掉,也就是暂时写在硬盘上,称为换出(Swap Out)。⼀旦需要的时候,再加载进来,称为换⼊(Swap In。所以,⼀次性写⼊磁盘的也只有少数的⼀个⻚或者⼏个⻚,不会花太多时间,内存交换的效率就相对⽐较⾼。

 

​ 因此,分⻚的⽅式使得我们在加载程序的时候,不再需要⼀次性都把程序加载到物理内存中。我们完全可以在进⾏虚拟内存和物理内存的页之间的映射之后,并不真的把页加载到物理内存⾥,⽽是只有在程序运⾏中,需要用到对应虚拟内存页里面的指令和数据时,再加载到物理内存里面去。

  • VSS(virtual set size)虚拟耗用内存(包含共享库占用的内存),它是一个进程能访问的所有内存空间地址的大小。这个大小包含了
    一些没有驻留在RAM中的内存,就像mallocs已经被分配,但还没有写入。VSS很少用来测量程序的实际使
    用内存。
  • RSS(Resident set size)实际使用物理内存(包含共享库占用的内存)
    •   RSS是进程实际驻存在物理内存的部分的大小。因为一个进程执行不需要把整个进程都全部驻存到物理内存。RSS是最常用的内存指标,表示进程占用的物理内存大小。但是,将各进程的RSS值相加,通常会超出整个系统的内存消耗,这是因为RSS中每个进程都包含了各进程间共享的内存,因此存在重叠部分。
    •   VSS是一个进程的总的大小。只有当进程执行且整个进程都驻存到物理内存时才RSS=VSS。
  • PSS(Proportional set size)实际使用的物理内存(比例分配共享库占用的内存),例如,如果有三个进程共享一个占30页内存控件的共享库,每个进程在计算PSS的时候,只会计算10页。
    PSS是一个非常有用的数值,如果系统中所有的进程的PSS相加,所得和即为系统占用内存的总和。当一个
    进程被杀死后,它所占用的共享库内存将会被其他仍然使用该共享库的进程所分担。在这种方式下,PSS
    也会带来误导,因为当一个进程被杀后,PSS并不代表系统回收的内存大小。
  • USS(Unique set size ) 进程独自占用的物理内存(不包含共享库占用的内存)
    •   与RSS相比,PSS会更准确一些,它将共享内存的大小进行平均后,再分摊到各进程上去。
    •   USS则是PSS中自己的部分,它只计算了进程独自占用的内存大小,不包含任何共享的部分。·

  一般来说内存占用大小有如下规律:VSS >= RSS >= PSS >= USS

查看所有进程的内存信息:adb shell dumsys meminfo

 

在开发过程中,最实用的指标通常是 USS(Unique Set Size)。这是因为 USS 反映了一个进程独占使用的物理内存量,即进程专用的内存量。对于开发人员来说,了解他们的应用程序实际使用了多少内存是非常重要的,因为这有助于优化应用程序的性能和资源管理。

其他指标如 VSS(Virtual Set Size)、RSS(Resident Set Size)和 PSS(Proportional Set Size)也提供了关于内存使用情况的有用信息,但它们更多地用于系统性能调优和资源分配方面。例如,系统管理员可能更关心 RSS,因为它表示一个进程当前实际驻留在物理内存中的数量。而 PSS 则考虑了共享内存的情况,对于一些特定的场景可能更有用。

但对于大多数开发任务来说,最关注的是应用程序实际使用的内存量,因此 USS 是最实用的指标。

 

 

 什么是共享库(动态库):动态库使用的扩展名是 .so,它是 Shared Object 的缩写

共享库在程序编译时并不会被连接到目标代码中, 而是在程序运行是才被载入.

1)不同的应用程序如果调用相同的库, 那么在内存里只需要有一份该共享库的拷贝, 规避了空间浪费问题.

2)动态库在程序运行时才被载入, 也解决了静态库对程序的更新、部署和发布会带来麻烦. 用户只需要更新动态库即可, 增量更新.

为什么使用JNI:
JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。

这就引出第 1 个问题(为什么要这么做):Java 为什么要调用 C/C++ 代码,而不是直接用 Java 开发需求呢?我认为主要有 4 个原因:

• 原因 1 - Java 天然需要 JNI 技术:虽然 Java 是平台无关性语言,但运行 Java 语言的虚拟机是运行在具体平台上的,所以 Java 虚拟机是平台相关的。因此,对于调用平台 API 的功能(例如打开文件功能,在 Window 平台是 openFile 函数,而在 Linux 平台是 open 函数)时,虽然在 Java 语言层是平台无关的,但背后只能通过 JNI 技术在 Native 层分别调用不同平台 API。类似的,对于有操作硬件需求的程序,也只能通过 C/C++ 实现对硬件的操作,再通过 JNI 调用;

• 原因 2 - Java 运行效率不及 C/C++:Java 代码的运行效率相对于 C/C++ 要低一些,因此,对于有密集计算(例如实时渲染、音视频处理、游戏引擎等)需求的程序,会选择用 C/C++ 实现,再通过 JNI 调用;

• 原因 3 - Native 层代码安全性更高:反编译 so 文件的难度比反编译 Class 文件高,一些跟密码相关的功能会选择用 C/C++ 实现,再通过 JNI 调用;

• 原因 4 - 复用现有代码:当 C/C++ 存在程序需要的功能时,则可以直接复用。

 

一次拷贝

Client端的用户空间和Server短的用户空间,以及内核空间指的都是虚拟内存,实际上都对应着各自的物理内存。

按普通流程,就是Client端数据拷贝到内核,内核再将数据拷贝到Server端,两次拷贝。

Binder的做法:将Server端的虚拟内存和内核的虚拟内存进行映射,实际上就是这两个的虚拟内存映射到同一块的物理内存,这样Client端的数据拷贝到内核的时候,Server端也得到了该数据,实现了一次拷贝。

 

2:Android为什么使用binder进行IPC通信:

1)效率:数据拷贝次数:Binder数据拷贝只需要一次,管道、消息队列、Socket都需要2次,而共享内存方式一次内存拷贝都不需要。从性能角度看,Binder性能仅次于共享内存。

2)稳定性:Binder是基于C/S架构的,简单解释下C/S架构,是指客户端(Client)和服务端(Server)组成的架构,Client端有什么需求,直接发送给Server端去完成,架构清晰明朗,Server端与Client端相对独立,稳定性较好;而共享内存实现方式复杂,没有客户与服务端之别,需要充分考虑到访问临界资源的并发同步问题,否则可能会出现死锁等问题;从这稳定性角度看,Binder架构优越于共享内存。

 什么是共享内存:

共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。进程可以将同一段共享内存连接到它们自己的地址空间中,所有进程都可以访问共享内存中的地址,就好像它们是由用C语言函数malloc分配的内存一样。而如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。

特别提醒:共享内存并未提供同步机制,也就是说,在第一个进程结束对共享内存的写操作之前,并无自动机制可以阻止第二个进程开始对它进行读取。所以我们通常需要用其他的机制来同步对共享内存的访问,例如信号量机制

3)安全性:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份;而Android作为一个开放的开源体系,拥有非常多的开发平台,App来源甚广,因此手机的安全显得额外重要;对于普通用户,绝不希望从App商店下载偷窥隐射数据、后台造成手机耗电等等问题,传统Linux IPC无任何保护措施,完全由上层协议来确保。

Android为每个安装好的应用程序分配了自己的UID,故进程的UID是鉴别进程身份的重要标志,前面提到C/S架构,Android系统中对外只暴露Client端,Client端将任务发送给Server端,Server端会根据权限控制策略,判断UID/PID是否满足访问权限,目前权限控制很多时候是通过弹出权限询问对话框,让用户选择是否运行。

比如安装了一个播放音乐的客户端,UID为1,并且客户指定允许播放的permission。

这时候如果有个恶意应用,去假冒这个客户端,但是UID为2,并且他没有这个播放音乐的permisson,这时候系统为进行判断,一个是UID不是合法的应用程序,另外一个是,即使UID是合法的,但是他不拥有播放音乐的权限,导致他无法调用起系统的播放服务。

因为bidner是自动会带上UID,所以UID无法伪造。而其他通信方式并没有天然带上UID的操作,所以容易伪造。

  1. 防止恶意应用冒充合法应用进行通信:通过验证客户端的 UID,系统可以确定通信请求的来源是否是一个合法的、已安装的应用程序。这样可以防止恶意应用冒充合法应用程序进行通信,从而保护系统安全。

  2. 保护服务端资源不被未经授权的应用访问:服务端应用可能会包含一些敏感或私密的资源,只有特定的合法应用程序才能够访问。通过检查客户端的 UID,系统可以确保只有具有合法权限的应用程序才能连接到服务端应用,并访问其资源,从而保护了服务端资源的安全。


 

3:ServiceManager:

1:先了解几个概念:

1)Binder 实体

是各个 Server 以及 ServiceManager 在内核中的存在形式。例如,Binder 实体中保存了Server对象在用户空间的地址,Binder 实体是一个抽象的通信对象,代表了一个进程中的一块数据或服务。这个实体可以包含数据、状态和方法。

2)binder_node、binder_proc

驱动中的结构体binder_node用来描述一个Binder实体对象。每一个Service组件在Binder驱动程序中都对应有一个Binder实体对象,用来描述它在内核中的状态

binder_proc 是一个数据结构,代表一个进程。每个进程在系统中都有一个相应的 binder_proc 结构,用于管理该进程所拥有的 Binder 实体。这个数据结构包含了进程的信息以及它所拥有的 Binder 实体的列表。每个服务端进程在内核态都对应一个binder_proc进程对象

一个进程(binder_proc)可以拥有多个 Binder 实体(Binder Object),每个 Binder 实体在进程内有一个相应的 binder_node。

内核空间的binder驱动程序拥有一个全局列表binder_procs,该列表维护了所有使用binder驱动程序的用户进程,正因为binder驱动程序掌握了所有用户进程的名单,所以binder驱动程序就能够很方便的找到他们。

Binder实体和Binder引用都是内核(即,Binder驱动)中的数据结构。每一个 Server 在内核中就表现为一个Binder实体,而每一个Client则表现为一个Binder引用。这样,每个Binder引用都对应一个Binder实体,而每个Binder实体则可以多个 Binder 引用。

3)binder引用

binder引用是内核中binder_ref结构体的对象,包含了binder实体的标识符,所以可以通过引用找到bindr实体。

ServiceManager是比较特殊的服务,所有应用都能直接使用,因为ServiceManager对于Client端来说Handle句柄是固定的,都是0,所以ServiceManager服务并不需要查询,可以直接使用。

 

2:Binder通信过程涉及的四个角色:Server、Client、ServiceManager和Binder驱动,下面大概得流程:

(1)server传入一个flat_binder_object结构体给驱动,在内核态驱动里为每一个服务创建binder_node,biner_node.proc关联描述进程的结构体

(2)ServiceManager 在驱动中创建binder_ref结构体,引用binder_node服务节点

  • 在用户态创建服务链表(name,handle)

  • binder_ref.desc关联handle

(3)client向ServiceManager传入name查询对应服务

(4)servicemanager返回hanle给驱动程序

(5)驱动程序在servicemanager的binder_ref红黑树中根据handle找到binder_ref结构体,在根据binder_ref.node找到binder_node结构体,最后为client创建新的binder_ref,并关联找到的binder_node节点,他的结构体成员desc从1开始,驱动返回的desc给client,即为handle。

(6)总结client如何得到hanled:驱动根据handle找到binder_ref结构体->binder_node->进程server


3:注意点:

1)从本质上讲,binder用的binder驱动共享内存原理,但是和共享内存又有所不同。共享内存是两个进程之间共享,而binder是多个进程对多个进程,然后通过各种调度,形成关联,handle对应到具体的一个个服务;

2) handle对于每个不同的客户端进程A0、A1、A2,均指向服务端B的服务,但handle值不同。这类似于多个进程访问同一个文件,都会得到自己的文件描述符,但因为每个进程访问文件描述符的顺序不同而导致不同(虽然不同,但指向的是同一个文件)

4)客户端都持有一个handle,对应到具体的服务。而最开始的时候,客户端要和ServiceManager也是一个服务,此时默认的handle值为0,就是可以直接找到ServiceManager,然后client和Server才能通过ServiceManager进行通信。

 

以下为具体的步骤:

1. Binder驱动为Server进程创建binder_node

当一个Server进程注册其服务时,Binder驱动会为该服务创建一个binder_node对象。这个binder_node表示Server提供的服务接口,并存储在内核空间中。

  • binder_node:代表一个具体的Binder实体(即Server端的服务)。它包含了服务的相关信息,并且会被添加到Binder驱动的内部数据结构中。
c
深色版本
// 假设Server注册了一个名为"MyService"的服务
struct binder_node *node = binder_new_node(proc, ptr); // 创建一个新的binder_node

2. ServiceManager在Binder驱动中创建binder_ref

ServiceManager负责管理所有已注册的服务。当Server注册其服务时,ServiceManager会在Binder驱动中创建一个binder_ref对象,指向相应的binder_node

  • binder_ref:是一个引用对象,用于管理和查找binder_node。它通常包含一个唯一的标识符(如服务名称)和一个指向binder_node的指针。
c
深色版本
// ServiceManager在Binder驱动中创建一个binder_ref,指向刚刚创建的binder_node
struct binder_ref *ref = binder_get_ref_for_node(proc, node);

3. ServiceManager在用户空间创建binder_ref

在用户空间,ServiceManager维护一个服务列表,其中每个服务都有一个名称和一个句柄(handler)。这个句柄实际上是对应于Binder驱动中的binder_ref对象。

  • binder_ref在用户空间:这是一个简化版本的binder_ref,主要用于ServiceManager在用户空间管理和查找服务。
java
深色版本
// ServiceManager在用户空间创建一个服务条目
ServiceEntry entry = new ServiceEntry("MyService", handle);
serviceMap.put("MyService", entry);

4. Client查询服务

当Client想要访问某个服务时,它会通过Binder驱动查询ServiceManager,找到对应的服务句柄(handler),并通过该句柄获取到实际的binder_ref

  • 查询过程
    1. Client调用getService()方法向ServiceManager查询指定名称的服务。
    2. ServiceManager返回服务的句柄(handler)。
    3. Client使用该句柄从Binder驱动中获取对应的binder_ref
java
深色版本
// Client查询服务
IBinder binder = getService("MyService");

5. 驱动为Client创建binder_ref

当Client获得服务的句柄后,Binder驱动会为Client创建一个新的binder_ref对象,指向之前创建的binder_node。这样就形成了一个Client到Server的通信通道。

  • 新的binder_ref:这是Client端的引用对象,用于与Server端的服务进行通信。
c
深色版本
// 驱动为Client创建一个新的binder_ref,指向之前的binder_node
struct binder_ref *client_ref = binder_get_ref_for_node(client_proc, node);

ServiceManager的角色

ServiceManager在Binder机制中的主要职责包括:

  1. 服务注册:允许Server进程将它们的服务注册到系统中。
  2. 服务查找:允许Client进程通过服务名称查找并获取相应的服务句柄(handler)。
  3. 服务管理:维护一个全局的服务目录,确保服务的唯一性和可访问性。

详细流程

以下是完整的流程,展示了ServiceManager如何作为桥梁,实现Server注册和服务查找的过程。

1. Server注册服务

当一个Server进程启动并准备好提供服务时,它需要将服务注册到ServiceManager中。具体步骤如下:

  1. 创建Binder实体

    • Server进程创建一个Binder实体对象,这个对象实现了服务的具体功能。
  2. 调用addService()方法

    • Server进程通过Binder驱动调用addService()方法,将服务名称和服务句柄(handler)传递给ServiceManager。
java
深色版本
// Server端代码示例
IBinder binder = new MyService(); // 创建Binder实体
ServiceManager.addService("MyService", binder); // 注册服务
  1. Binder驱动处理注册请求
    • Binder驱动为该服务创建一个binder_node对象,并将其添加到内核中的数据结构中。
    • Binder驱动还会为ServiceManager创建一个指向该binder_nodebinder_ref对象。

2. ServiceManager记录服务

ServiceManager在用户空间维护一个服务目录,用于记录所有已注册的服务。具体步骤如下:

  1. 存储服务信息
    • ServiceManager接收服务名称和服务句柄(handler),并在其内部数据结构中记录这些信息。
java
深色版本
// ServiceManager内部数据结构示例
Map<String, IBinder> serviceMap = new HashMap<>();
serviceMap.put("MyService", binder); // 存储服务名称和对应的Binder实体
  1. 返回成功确认
    • ServiceManager向Server进程返回一个确认消息,表示服务注册成功。

3. Client查找服务

当Client进程需要使用某个服务时,它可以通过ServiceManager查找该服务。具体步骤如下:

  1. 调用getService()方法
    • Client进程通过Binder驱动调用getService()方法,传入所需服务的名称。
java
深色版本
// Client端代码示例
IBinder binder = ServiceManager.getService("MyService"); // 查找服务
  1. Binder驱动处理查找请求

    • Binder驱动根据服务名称查找对应的binder_ref对象。
    • 如果找到,则返回该binder_ref对象的服务句柄(handler)给Client进程。
  2. ServiceManager返回服务句柄

    • ServiceManager从其内部数据结构中查找服务名称对应的服务句柄(handler),并通过Binder驱动返回给Client进程。
java
深色版本
// ServiceManager内部查找服务示例
IBinder binder = serviceMap.get("MyService"); // 根据服务名称查找Binder实体
return binder; // 返回Binder实体
  1. Client获得服务引用
    • Client进程收到服务句柄(handler)后,可以使用该句柄与Server进行通信。
java
深色版本
// Client使用获得的Binder实体进行通信
MyServiceInterface myService = MyServiceInterface.Stub.asInterface(binder);
myService.someMethod(); // 调用服务的方法

具体的数据结构和映射关系

为了更好地理解整个过程中的数据结构和映射关系,以下是一些关键概念的详细说明:

  1. binder_node

    • 位于内核空间,代表一个具体的Binder实体(即Server提供的服务)。
    • 每个binder_node都有一个唯一的标识符,并且包含服务的相关信息。
  2. binder_ref

    • 位于内核空间或用户空间,用于管理和查找binder_node
    • 在内核空间,binder_ref包含对binder_node的引用;在用户空间,binder_ref通常简化为一个句柄(handler)。
  3. ServiceManager的内部数据结构

    • ServiceManager在用户空间维护一个服务目录(如Map<String, IBinder>),用于记录服务名称和对应的binder_ref(句柄)。

总结

ServiceManager在Binder机制中起到桥梁的作用,具体体现在以下几个方面:

  1. 服务注册:Server进程通过ServiceManager将服务注册到系统中,Binder驱动会为该服务创建binder_nodebinder_ref
  2. 服务查找:Client进程通过ServiceManager查找所需的服务,Binder驱动根据服务名称返回对应的binder_ref(句柄)。
  3. 服务管理:ServiceManager在用户空间维护一个服务目录,确保服务的唯一性和可访问性。

 

 

 

 

 


 

3:系统服务注册过程

ServiceManager作为Android进程间通信binder机制中的重要角色,运行在native层,由c++语言实现,任何Service被使用之前,例如播放音乐的MediaService,例如管理activity的ActivityManagerService,均要向SM注册,同时客户端使用某个service时,也需要向ServiceManager查询该Service是否注册过了。
注册系统服务AMS

 Service Manager在Binder机制中既充当守护进程的角色,同时它也充当着Server角色,然而它又与一般的Server不一样。对于普通的Server来说,Client如果想要获得Server的远程接口,那么必须通过Service Manager远程接口提供的getService接口来获得,这本身就是一个使用Binder机制来进行进程间通信的过程。而对于Service Manager这个Server来说,Client如果想要获得Service Manager远程接口,却不必通过进程间通信机制来获得,因为Service Manager远程接口是一个特殊的Binder引用,它的引用句柄一定是0

 

这里直接create了一个BpBinder,参数为0,然后将其直接作为IBinder返回,BpBinder是远端对象在当前进程的代理,当handle为0的时候,代表的正是servicemanager

 使用的时候getService:ServiceManager->getService:

 

 4:binder线程池:

Binder本身是C/S架构,就可能存在多个Client会同时访问Server的情况。 在这种情况下,如果Server只有一个线程处理响应,就会导致客户端的请求可能需要排队而导致响应过慢的现象发生。解决这个问题的方法就是引入多线程。【多个客户端不同线程去请求,服务端需要使用多线程机制,binder线程池,创建多个线程去回复多个客户端的请求】

创建binder 普通线程是由binder 驱动控制的,驱动通过 BR_SPAWN_LOOPER 命令告知进程需要创建一个新的线程,然后进程通过 BC_REGISTER_LOOPER 命令告知驱动其子线程(非主线程)已经ready

1)一个进程的Binder线程数默认最大是16(1个主线程和15个非主线程),超过的请求会被阻塞等待空闲的Binder线程,即Binder线程池耗尽会导致后面的调用被阻塞。

2)service 端创建线程的2种情况:

BC_TRANSACTION:client进程向binderDriver发送IPC调用请求的时候。

BC_REPLY:client进程收到了binderDriver的IPC调用请求,逻辑执行结束后发送返回值。

4:AIDL文件解析:

1)每个aidl文里面,stub负责发送数据,proxy负责接受数据

2):Descriptor的作用:

每个Stub对应一个Descriptor,当我们绑定一个service的时候,这个服务有可能是另外一个进程的,也有可能是同一个进程的,就是根据DESCRIPTOR来判断的。

而这决定了我们创建aidl实例的时候,使用的是Stub的还是Proxy.

复制代码
public static abstract class Stub extends android.os.Binder implements com.example.aidlvaslibrary.IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.example.aidlvaslibrary.IMyAidlInterface";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.example.aidlvaslibrary.IMyAidlInterface interface,
         * generating a proxy if needed.
         */
        public static com.example.aidlvaslibrary.IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.example.aidlvaslibrary.IMyAidlInterface))) {
                return ((com.example.aidlvaslibrary.IMyAidlInterface) iin);
            }
            return new com.example.aidlvaslibrary.IMyAidlInterface.Stub.Proxy(obj);
        }
复制代码
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
public @Nullable IInterface queryLocalInterface(@NonNull String descriptor) {
        if (mDescriptor != null && mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }

3):Proxy:

复制代码
private static class Proxy implements com.example.aidlvaslibrary.IMyAidlInterface {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            /**
             * Demonstrates some basic types that you can use as parameters
             * and return values in AIDL.
             */
            @Override
            public java.lang.String add(int a, int b) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
//Binder驱动根据DESCRIPTOR找到Server端的Stub,因为Server进程对应的Stub注册的时候,就绑定DESCRIPTOR了。 _data.writeInterfaceToken(DESCRIPTOR); _data.writeInt(a); _data.writeInt(b);
//将数据传到Binder驱动
boolean _status = mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0); if (!_status && getDefaultImpl() != null) { return getDefaultImpl().add(a, b); } _reply.readException(); _result = _reply.readString(); } finally { _reply.recycle(); _data.recycle(); } return _result; } public static com.example.aidlvaslibrary.IMyAidlInterface sDefaultImpl; }      //每个函数对应一个int值,用来区分不同的函数 static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
复制代码

 

Proxy里面的transact负责发送。

Stub里面的onTransact负责接收。

5:bindservice流程:

执行到contextImpl:

int res = ActivityManagerNative.getDefault().bindService(
                mMainThread.getApplicationThread(), getActivityToken(), service,
                service.resolveTypeIfNeeded(getContentResolver()),
                sd, flags, getOpPackageName(), user.getIdentifier());

可以看到这里的IActivityManager就是一个AIDL类

 但是这里的类没有我们前面讲的对应的Stub和Proxy那种结构,

其实AIDL那种固定的结构是为了方便开发者开发而定义的,对于系统来说,他有自己的写法,具体在ActivityManagerNative(这个就是相当于我们应用层生成代码里面的stub)里面:

 所以我们的bindService发送数据的proxy就是在这个类里面的:ActivityManagerProxy的bindservice

 这里的Proxy就是发送,接收是在ServiceManage进程里面的onTransact:

 最终调用的是ActivityManagerService(AMS)里面的bindService:

 

6:手动实现Binder:

服务端:

复制代码
public class MyService extends Binder {

    private static final int TRANSACTION_SAY_HELLO = Binder.FIRST_CALL_TRANSACTION;

    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case TRANSACTION_SAY_HELLO:
                data.enforceInterface(null); // 检查接口标识,这里为null表示不检查
                String message = sayHello();
                reply.writeNoException();
                reply.writeString(message);
                return true;
            default:
                return super.onTransact(code, data, reply, flags);
        }
    }

    private String sayHello() {
        return "Hello from the Service!";
    }
}
复制代码

客户端:

复制代码
public class ClientApp {

    public static void main(String[] args) {
        // 模拟从服务端获取的 Binder
        IBinder binder = new MyService();

        Parcel data = Parcel.obtain();
        Parcel reply = Parcel.obtain();
        String message;

        try {
            data.writeInterfaceToken(null); // 检查接口标识,这里为null表示不检查
            binder.transact(MyService.TRANSACTION_SAY_HELLO, data, reply, 0);
            reply.readException();
            message = reply.readString();
            System.out.println("Message from Service: " + message);
        } catch (RemoteException e) {
            e.printStackTrace();
        } finally {
            data.recycle();
            reply.recycle();
        }
    }
}
复制代码

 

 

 问题:

1)为什么Aidl、intent最大传输数据位1M

Server进程和内核缓存区进行mmap映射的时候,开辟的空间大概为1M。

Binder 传递缓存有一个限定大小,通常是 1Mb。但同一个进程中所有的传输共享缓存空间。
多个地方在进行传输时,即时它们各自传输的数据不超出大小限制,TransactionTooLargeException异常也可能会被抛出。

在使用 Intent 传递数据时,1Mb 并不是安全上限。因为 Binder 中可能正在处理其它的传输工作。

如何传递大数据:

进程内部intent:

  1. 我们可以通过静态变量来共享数据
  2. 使用bundle.putBinder()方法完成大数据传递。 由于我们要将数据存放在Binder里面,所以先创建一个类继承自Binder。data就是我们传递的数据对象。

AIDL:

ParcelFileDescriptor

ParcelFileDescriptor 是 Android 框架中用于在进程间传递文件描述符的类。它通常用于实现进程间通信(IPC),尤其是在不同应用程序之间共享大数据时。ParcelFileDescriptor 的原理涉及 Android 的底层 IPC 机制以及文件描述符的传递。

在 Android 中,进程间通信通常使用 Binder IPC 机制。当一个进程需要向另一个进程传递数据时,它会创建一个 Parcel 对象,将数据写入其中,然后通过 Binder IPC 将 Parcel 对象传递给目标进程。Parcel 是一个序列化机制,用于将对象的数据编码为二进制流以进行跨进程传输。

ParcelFileDescriptorParcel 的一个特殊类型,它可以用于传递文件描述符。文件描述符是一个用于标识打开的文件、管道或套接字的整数值。ParcelFileDescriptor 提供了一种方法,可以在不将实际文件数据传递到另一个进程的情况下,在进程之间传递文件描述符。

以下是大致的步骤来实现使用 ParcelFileDescriptor 传递大数据:

  1. 打开文件或创建管道:首先,在发送端进程中,你需要打开一个文件或者创建一个管道。这将为数据的传输提供一个文件描述符。

  2. 获取 ParcelFileDescriptor:将文件描述符封装在一个 ParcelFileDescriptor 中。你可以使用 ParcelFileDescriptor 的静态方法来创建一个实例,例如 ParcelFileDescriptor.fromFd()

  3. 将 ParcelFileDescriptor 传递给目标进程:通过 Binder IPC 机制将包含文件描述符的 ParcelFileDescriptor 传递给目标进程。

  4. 接收 ParcelFileDescriptor:在接收端进程中,通过 Binder IPC 接收传递的 ParcelFileDescriptor

  5. 使用文件描述符:一旦在接收端获得了 ParcelFileDescriptor,你可以使用它来读取或写入数据,或者将它传递给其他需要访问相同文件的部分。

需要注意的是,ParcelFileDescriptor 传递的是文件描述符,而不是实际文件内容。这意味着在接收端,你需要有访问相同文件的权限,并且可以使用文件描述符来操作该文件。

总结起来,ParcelFileDescriptor 利用 Android 的 Binder IPC 机制和文件描述符的共享特性,通过 Parcel 来传递包含文件描述符的信息。这使得不同进程间能够共享文件,而不需要复制整个文件的实际数据,从而提高了效率。


2)为什么intent传递数据需要序列化

我们知道Android系统是基于Linux系统实现的,而Linux有进程隔离的机制。而进程如果传递复杂数据类型那传递的是对象的引用,本质上就是一个内存地址。但是传递内存地址的方式在跨进程中明显不行,由于Linux采用了虚拟内存机制,两个进程都有自己独立的内存地址空间,所以把A进程中某个对象的内存地址传递给B进程,这个内存地址在两个进程中映射到的物理内存地址并不是同一个,所以就得依靠上述的序列化手段来将对象的字节序列传递才能实现通信。

3)Binder导致的ANR:

线程池被耗净,同步调用的Client端被阻塞,若是用户操作的关键流程中,则发生ANR;

system_server等系统关键服务进程的Binder线程池耗尽,则造成整机卡顿

优化建议:

避免短时间内大量线程同时并行调用某Server;

 

提升Binder接口实现的执行效率。

 

 

 

 

 

参考:

https://blog.csdn.net/shenxiaolinil/article/details/128972302

https://web.aigexing.com/zhichang/1592790.html

https://baijiahao.baidu.com/s?id=1763930841789250850&wfr=spider&for=pc

https://blog.csdn.net/qq_27672101/article/details/108186072

http://wed.xjx100.cn/news/158047.html?action=onClick

http://www.taodudu.cc/news/show-3136391.html?action=onClick

https://baijiahao.baidu.com/s?id=1763930841789250850&wfr=spider&for=pc

 https://zhuanlan.zhihu.com/p/579705854

 

posted @   蜗牛攀爬  阅读(507)  评论(0编辑  收藏  举报
(评论功能已被禁用)
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示