android 面试题 - 深入了解
ps :持续更新ing...
目前涉及内容:
AIDL & Binder
AIDL 是android 特有的 IPC 跨进程通信机制
这里先了解2个概念:
-
IPC - Inter Process Communication 跨进程通信
这个概念泛指进程之间任何形式的通信行为,是个可以拿来到处套的术语。它不仅包括各种形式的消息传递,还可以指共享资源,以及同步对象[mutex 或者其他类似的东西,即确保安全的并发访问共享资源(也就是防止两个或两个以上的对象同事对同一个数据成员进行修改,从而导致数据被破坏,或者竞争条件下同事读/写数据而导致错误的情况发生)]的东西 -
RPC - Reomote Procedure Call 远程过程调用
特指一种隐藏了过程调用时实际通信细节的IPC方法。客户端将调用一个本地方法,而这个本地方法则是负责透明的与远程服务端进行过程间通信。这个本地方法会讲相关参数顺序打包到一个消息中,然后把这个消息发送给服务端提供的方法,服务端的方法会从消息中解出序列化发出来的参数,然后执行,最后仍以同样的方式将方法的返回值发送给客户端
ok 先有点打底,就怕面试时问概念答不上来。AIDL 采用经典的 C/S 架构(客户端,服务端),其底层原理就是采用的 android 自己的跨进程通信 Binder 来做的
AIDL 自动生成的接口文件中 Stub 这个内部类继承自 Binder,Proxy 远程代理帮我们实现了使用 Binder 来传递获取数据的操作
AIDL 的基础部分看这里:AIDL 从入门到精通
Android 是基于 Linux 内核的,但是 android 即使没用 Linux 自带的诸如:
管道 Pipe,信号 Signal,跟踪 Trace,插口 Socket,消息队列 Message,共享内存 Share Memory,信号量Semaphore
来实现 IPC,非要自己做一套也就是 Binder 出来,自然是有其原因的,所以对于 Binder 我们是需要清楚的,面试大公司会问的,除了面试外也非常有注意我们理解 android 的全貌,对 android 的系统运行有个清晰的认识
进程间通信考虑3个因素:
-
效率 - Socket 作为一款通用接口,其传输效率低,开销大,主要用在跨网络的进程间通信和本机上进程间的低速通信。消息队列和管道采用存储-转发方式,即数据先从发送方缓存区拷贝到内核开辟的缓存区中,然后再从内核缓存区拷贝到接收方缓存区,至少有两次拷贝过程。共享内存虽然无需拷贝,但控制复杂,难以使用。Binder 只需要一次数据拷贝,性能上仅次于共享内存
- 稳定性 - Binder 基于 C|S 架构,客户端(Client)有什么需求就丢给服务端(Server)去完成,架构清晰、职责明确又相互独立,自然稳定性更好。 共享内存虽然无需拷贝,但是控制负责,难以使用。从稳定性的角度讲,Binder 机制是优于内存共享的
-
安全性 - Binder 通过在内核层为客户端添加身份标志 UID|PID,来作为身份校验的标志,保障了通信的安全性。 传统 IPC 访问接入点是开放的,无法建立私有通道。比如,命名管道的名称,SystemV 的键值,Socket 的 ip 地址或文件名都是开放的,只要知道这些接入点的程序都可以和对端建立连接,不管怎样都无法阻止恶意程序通过猜测接收方地址获得连接
好了再次介绍概念:
- PID - 进程 ID,程序一旦运行,就会给应用分配一个独一无二的 PID
- UID - 指用户ID,在 linux 系统中 UID 就是用户的 ID,主要用于权限管理。但是 android 是单用户系统,不支持多用户,自然这 UID 就变成了 app ID,用于数据共享,权限等方面
- 在 android 中 PID,UID 都是用来识别应用程序的身份的,但 UID 是为了不同的程序来使用共享的数据
Binder 实现原理
Binder 的实现原理不能不提2个概念:用户空间 | 内核空间,大家还记得 AIDL 中的那个 Binder 桥吗,Binder 就是运行在内核空间的,内核空间的特性就是对所有的进程开放,Linux 大多数 IPC 手段都是通过这个 内核空间 实现的,用 内核空间 做数据中转
- 用户空间 - 好理解,就是每个进程自己拥有的内存空间,这块内存是独属于该进程的,别的进程访问不了,这也是需要 IPC 的根本原因
- 内核空间 - 是系统内核运行的空间,独立于普通的应用程序,可以访问受保护的内存空间,也可以访问底层硬件设备的权限
为了保护用户进程不能直接操作内核,保证内核的安全,操作系统从逻辑上将虚拟空间划分为用户空间(User Space)和内核空间(Kernel Space)。针对 Linux 操作系统而言,将最高的 1GB 字节供内核使用,称为内核空间;较低的 3GB 字节供各进程使用,称为用户空间
所以跨进程通信必然需要有个依托,介于内核空间的特性,Binder 当仁不让的就运行在内核空间里了
Binder 是典型的 C/S 架构,在这个架构中有4个角色:
- Client - 客户端,数据发送方
- Service - 服务端,数据接受返回最终结果的一端
- ServiceManager - 统一管理 Server
- Binder 驱动 - C 层数据传递支持
ServiceManager 大家可能对他干什么的不是很熟悉,ServiceManager 是管理所有的 Service, Service 的启动,生命周期管理,注册都是由 ServiceManager 完成的,我们 startService 即使请求的 ServiceManager 的相关方法。我们可以 Binder 的过程看成网络请求
通常我们访问一个网页的步骤是这样的:首先在浏览器输入一个地址,如 http://www.google.com 然后按下回车键。但是并没有办法通过域名地址直接找到我们要访问的服务器,因此需要首先访问 DNS 域名服务器,域名服务器中保存了 http://www.google.com 对应的 ip 地址 10.249.23.13,然后通过这个 ip 地址才能放到到 http://www.google.com 对应的服务器。
Service 在启动时对把自己注册到 ServiceManager 中,以 key/value 的是方式,key 是一个字符串,也是该 AIDL 接口的名字,value 就是这个 Binder 实体在内核空间的地址,然后 ServiceManager 把 Service 端 Binder 的内核引用传递给任意多个 Client 端进程,然后 Client 端借助 Binder 与内核空间通信传递数据以实现跨进程通信,Binder 的核心就是传递 Service 端进程内存中 Binder 对象对应的内核空间中的地址引用
这是最通俗的描述,其实说"Binder 对象对应的内核空间中的地址引用"并不是十分准备的,为了方便理解可以这么说,但是我们大家还可以再深入一点来看看
Linux 下的传统 IPC 通信原理
通常的做法是消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copy_from_user() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copy_to_user() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。如下图:
这种传统的 IPC 通信方式有两个问题:
性能低下,一次数据传递需要经历:内存缓存区 --> 内核缓存区 --> 内存缓存区,需要 2 次数据拷贝;
接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间
Binder 跨进程通信原理
Binder 并不是 Linux 自己的 IPC 手段,是 Google 自己实现的,是不能同原生 IPC 方案那样直接操作内核空间的,Binder 直接的是内存映射
这个概念
得益于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)机制,模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信
而这个内核模块
就是 Binder 了,Binder 为什么就驱动,是因为 Binder 的运行机制像和硬件通信一样罢了,区别是 Binder 不涉及硬件
一次典型的 Binder 通信是这样的:
- Service 端先在
内核空间
创建一个数据接受缓存区
和进程私有内存中的 Binder 对象实现内存映射 - 然后再创建一个
内核缓存区
出来,和数据接受缓存区
实现内存映射 - client 拿到这个
内核缓存区
的地址,通过 copy_from_user 方法将数据 copy 进内核空间,然后通过内存映射自然就能让 Service 端拿到数据了
还有一点 Service 端的 Binder 是运行在线程池中的,为啥呢,因为一个 Service 进程是要求可以同时与多个 client 进程通信的,这样的话不上线程池怎么能跑得起来
Android 自己的系统服务都是独立运行在各自的进程中的,比如 ServiceManager,ActivityManageService,surafceFiler 都是如此的,他们之中是如何管理的,其实和普通的 Service 一样,这些系统系统服务都是以 Service 的形式存在对外提供服务,那么自然就受 ServiceManager 管制,ServiceManager 是 android 系统启动时第一个启动的系统服务,然后再由 ServiceManager 启动其他的系统服务,系统服务会以自己的类名注册到 ServiceManager 中
但是 ServiceManager 一样也是运行在自己的进程中的,那么最初的 Binder 是怎么通信的,ServiceManager 的 Binder 对应的内核空间引用是固定的 = 0,相当于一个固定的 IP 地址,所以能够与其他进程直接通信,从而简历最初的 Binder 通道出来
其实 Binder 对象可以为方法参数可以在进程间传递,AIDL 的双向通信就是在建立 Client 和 service 的通信后,Client 调用 service 的方法把 Client 端的 Binder 对象传给 service,这样双方各自拥有对方的 Binder,就可以双向通信了
好了基本上就是这样了,更详细的原理看下面2篇吧