本文分为三个阶段,
相信这一套军体拳下来,应该能薛伟地掌握Binder的相关原理。
P.S. 本文code部分使用了伪代码,包含但不限于kotlin、java和汉字。。。不过应该都能看懂
Binder是Android系统用来实现高效IPC(进程间通信)所搭建的框架。在Android系统中的作用和他的名字一样,是像胶水一样,把系统各个服务进程粘合到了一起。
那么Android系统为什么要重新构建一个IPC框架呢?而不是直接使用Linux提供的IPC方案,类似管道,信号,socket等,首先是安全性的考量,这些Linux 原生IPC方案,都不能直接携带进程的信息,只能依赖上层的协议,比如socket是通过ip和端口来区分不同的进程,让伪造访问的难度降低,而Binder 的设计里是带有uid和pid来标识进程身份的,所以安全性会高很多;其次Binder机制的性能会非常的高,他只需要一次内存的拷贝即可, 而Android系统有很多个重要的服务进程,例如 AMS,PMS,SMgr,进程之间的交互非常的频繁,对IPC的性能有较高的要求, 最简单的例子,启动一个Activity也是需要IPC的,启动过程不再本文的描述范围内,有兴趣的话可以自行查阅资料。
首先为什么要IPC框架:
Android系统是基于linux的, 所以存在一个进程隔离的概念, 每个进程都由于虚拟地址的技术,认为自己独享整个系统, 所以并不能直接访问对方的内存地址,导致进程隔离。
那么物理概念的内存肯定只是大家共用的, 一定是可以访问的, 那么如果想完成这种操作就需要进入到内核空间来完成,此时沉入内核空间的进程被称为进入内核态, 而在用户空间则是用户态,
在内核空间可以获得高级别权限,例如访问所有的内存地址。
那么显然我们打通User Space 中的两个进程,肯定需要一个内核模块来完成,所以我们需要一个Binder Driver,作为桥梁来互相访问各自进程的内存。
那么为什么Binder Driver这个外来物可以做为内核空间的驱动而存在呢,这是因为LKM机制,可以让一个可编译的模块作为内核模块进入到内核空间提供服务,所以Android 系统构建了一个Binder Driver 在内核态提供支持,讲了这么多,好像想做什么大概是明白了,但具体是怎么做的呢,还是不清楚,面对这种复杂的问题,就是拆分,把他拆分成一个一个问题逐一解决,Android团队设计的时候估计也是这么想的。
假设有一个服务进程S,一个客户端进程C(Binder机制是个C/S架构,你们应该已经发现了)首先一个问题就扑面而来,客户端C如何调用服务端S提供的服务呢,或者说我作为C如何能执行到S的函数,在说白了,需要个协议之类的东西,Binder Driver 对C提供了一个代理,一个IBinder对象,这个东西在C看来就是S,里面有相同的函数可以提供调用, 当C调用了代理IBinder提供的函数时候,Binder Driver就会让C的线程暂时挂起, 然后他去S那执行真正的函数,执行完毕,就把结果返回给C,唤醒C的线程。感觉好像C调用了S一样。
那么第二个问题就来了,我怎么找到C说的这个S,此时需要一个ServiceManager进程登场了, Server进程会通过Binder Driver向ServiceManager注册自己,好比加入通讯录的感觉, 此时能叫上来名字的话就可以找到本人的电话了。
所以大致想实现的设计是这样的:
首先定义一个接口 IDoSomeThing, 这个接口代表了S能干什么的能力,所以S需要实现这个接口 IDoSomeThingImpl , 并且给出这个实现的实例, 供别人调用。
但Binder Driver也要去S实现另一个接口,代表你要通过驱动来提供给其他进程使用,这个接口叫刚才提到的IBinder,所以这个IDoSomeThing还需要实现IBinder,至此Server进程的工作算是做完了。
那么C进程呢, C进程也得有这个IDoSomeThing的接口, 而且要和S的定义一模一样,虽然没有实现,但是起码知道对方S有什么能力, 当我想调用的时候, 我需要和BinderDriver要一个代理对象,代表S,对吧,这个代理对象的类型,也是IBinder, 这个IBinder 就被设计成代表一种可以使用BinderDriver的支持的能力, 至于是提供S的实现,还是提供给C的代理对象,那么就交给S的实现类和BinderDriver来处理。
至此,技术上的设计完成了,感觉上是可行的,那么我们来通过源码来看Android是怎么把这一套玩转跑通的。
从AIDL开始吧,aidl第一部就是使用aidl的语句定义一个接口,例如IDoSomeThing,里面有一个方法doSomeThing(), 你要把它定义为.aidl文件, 编译一下 gradle会帮你生成这个你定义的java版本 的接口:
interface IDoSomeThing : android.os.IInterface{
doSomeThing() : Unit;
// android.os.IInterface 定义的方法
//@Override asBinder(): Binder
}
这个IInterface意思就是你是一个Binder官方认证的接口,你需要实现一个asBinder,来提供一个Binder对象。
但还没完,他还会在你的接口里生成一个内部抽象类Stub,而这个内部抽象类是无比的重要。
interface IDoSomeThing : IInterface{
class Stub extends Binder : IDoSomeThing {
public fun asInterface ( ibinder:IBinder ): IDoSomeThing {
if(调用我的是自己进程内)
//返回自己的实现
return ibinder as IDoSomeThing
else 证明是其他的进程来调用
返回代理对象
return new Stub.Proxy(ibinder);
}
@Override
fun asBinder:IBinder = this;
fun onTransact(){
调用我的doSomeThing函数
读一下有没有参数传过来
val result = this.doSomeThing()
把result结果写回去。
}
}
.
.
.
}
这个抽象类,有啥用,首先他也是一个IDoSomeThing ,所以在我们S进程里面,提供的实现类其实是这个Stub 的实现类。
class MyServiceProcess: Service{
class MyBinder : Stub{
doSomeThing(){
//实现
doSomeRealThing...
}
}
@Override
fun onBind(intent: Intent):IBinder = new MyBinder();
}
这里通过Service 的方法onBind,就把我们的实例提供给了BinderDriver.所有的跨进程通讯Server端都需要一个Service来提供给BinderDriver吗?大家可以扒一扒 ActivityManagerServer、IActivityManager、ActivityManagerNative、ActivityManagerProxy、ActivityManager这几个类的源码,看看AMS的IPC过程。
之前说过了, C和S必须都有接口,所以aidl生成的类,必须在两个进程都有才可以,两个APP的话就复制过去。
Server端先看到这里, 我们再看一下Client端,客户端绑定 远程服务的时候,是调用bindService方法(aidl的具体使用方式不再本文描述内,可以查看官方文档,写的特别好).
bindService(Intent(this, RemoteService.class){ service: IBinder->
//onServiceConnected的回调内
我的server进程服务代理对象 mService = Stub.asInterface(service);
}
欧,看到了asInterface, 这个方法,特别的简单,如果没有跨进程,就是直接返回sub, 如果跨了进程就返回代理对象,而Proxy这个类,又是stub的子类。
class Stub...{
class Proxy:IDoSomeThing {
private mRemote: IBinder;
constuct(remote : IBinder){
mRemote = remote;
}
@Override
fun asBinder = mRemote;
fun doSomeThing(){
写参数
....
val result = mRemote.transact(参数);
....
读结果
}
}
}
马上可以看到的是,她和stub不同,stub is 一个IBinder, 而Proxy只是持有了IBinder的引用,一个是策略模式,一个是代理模式。
可以看到C端通过bind到远程服务S端,会获得一个IBinder对象,我们用asInterface把这个IBinder传过去,换成S端的代理对象Proxy, Proxy是一个IDoSomeThing,所以当我们调用方法doSomeThing的时候, Proxy使用了Binder的一个方法transact,这就通知BinderDriver来处理一下, BInderDriver会调用server中的stub的实现类中的 onTransact()方法,我们在翻上去看一下, 完整的流程就无比清晰了:
C端进程的一个线程tc1,调用Proxy.doSomeThing, Proxy调用transact通知驱动, tc1挂起,
S端进程的一个线程ts1, 会被驱动回调onTransact,执行doSomeThing的真正实现 doRealSomeThing完成后返回给驱动,驱动在唤醒tc1线程,把结果返回,至此结束。
当然,一个S端可能对应着多个C端, S端是一个线程池维护的,给每个C端分别分配线程, 所以,onTransact可能会被多个线程回调,就会ts1,ts2,ts3等等,如果涉及到共享数据,做好多线程控制吧!
至此Binder机制 Java层的代码基本分析完毕,放一张剽窃来的画的贼屌的图吧,接口可能定义的不一样不过不妨碍感受一下。
终于到最后一步,实战阶段了,我们的目标是: 不使用aidl工具生成, 我们手写代码完成功能:
同学们自己去写吧。。反正我写了这个博客,基本上都是手撸了一遍, 还需要掌握一下Parcel这个类,毕竟跨进程涉及到把对象序列化和反序列化,相信你们也可以的。