在上一篇文章的铺垫下,今天来讲述 Android 进程间通信的机制。
IPC
IPC是 Inter-Process Communication 的缩写 ,即进程间通信。
我们知道Android基于Linux系统,而Linux系统自带多种IPC方式,主要有:
- 管道(Pipe):在创建时分配一个page大小的内存,缓存区大小比较有限;
- 消息队列(Message):信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
- 共享内存(Share Memory):无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
- 套接字(Socket):作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
- 信号量(Semaphore):常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段;
- 信号(Signal): 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等。
但是在Android的世界中,Binder才是IPC的核心,Framework中大量的服务都是由Binder来实现。Google这么做的原因主要有性能、安全和稳定性。在繁多的IPC选项中,BInder在当时是最适合Android这种移动设备的。
Binder虽然重要,但是我们平常都接触不到,大多数人头一次看到Binder应该是在Service的onBind方法。就像AndroidDoc所说
Most developers will not implement this class directly, instead using the aidl tool to describe the desired interface, having it generate the appropriate Binder subclass.
在大多数情况下,我们应该使用诸如AIDL之类的系统封装好的IPC方式,而不是直接实现Binder。我们最常使用也是最简单的IPC方式其实是通过Intent(Bundle)
实现的,比如我们通过Intent
+startActivity
唤起电话应用拨打指定的号码,在这个过程中,电话应用已经是另一个进程,而电话号码就是我们传递的数据,Android通过Bundle、Intent和AMS(底层也是Binder)等工具为我们简化了大量工作。除此之外还有基于Binder和Handler
的Messenger,四大组件之一的ContentProvider。这几种方式我在这就不详细讲解了,大家可以很方便地查看到相关资料,比如AndroidDev上的这篇文章。下面列出Android中常见的IPC方式及优缺点,方便大家根据实际情况选择合适的IPC方式。
名称 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
Bundle | 简单易用 | 只能传输Bundle支持的数据类型 | 四大组件间的进程间通信 |
文件共享 | 简单易用 | 不适合高并发场景,并且无法做到进程间的即时通 | 无法并发访问情形, 交换简单的数据实时性不高的场景 |
AIDL | 功能强大 | 使用稍复杂,需要处理好线程同步 | 一对多通信且有RPC需求 |
ContentProvider | 在数据源访问方面功能强大,支持一对多并发数据共享 | 可以理解为受约束的AIDL,主要提供数据源的CRUD操作 | 一对多的进程间的数据共享 |
Messenger | 功能一般, 支持一对多串行通信,支持实时通信 | 不能很好处理高并发,不支持RPC,数据通过Message进行传输, 因此只能传输Bundle支持的数据类型 | 低并发的一对多即时通信,无RPC需求,或者无需要返回结果的RPC需求 |
Socket | 功能强大,可以通过网络传输字节流,支持一对多并发实时通信 | 实现细节稍微有点繁琐,不支持直接的RPC | 网络数据交换 |
Binder
学习目的
Binder 是Android IPC 的核心,作为应用层开发,我觉得学习Binder主要有两个作用,一是应对项目多进程开发,毕竟AIDL和Messenger等常用的通信方式就是基于Binder。另一点因为Binder是Framework层的基础,我们打开一个Actvity,启动一个服务都有Binder的身影。学习Binder对我们理解四大组件的工作过程有帮助,比如我就是通过研究Activity启动相关的源码了解了Activity启动模式的工作原理。
Binder通信模型
Binder说到底还是一种通信方式,因此对初学者来说最好的方式就是使用Binder,用AIDL自己实现一个远程Service,然后理解各个类的功能与联系。所谓AIDL,是 Android Interface Definition Language 的缩写,也就是安卓接口定义语言。我们编写一个aidl,就是在定义一个远程调用的协议,build之后,AS会帮我们生相关类。可以说AIDL就是一个快速实现Binder IPC 的工具。假如我们新建的是一个叫IGoodsManager
的aidl,自动生成的是一个继承了IInterface
的IGoodsManager
接口,里面包含AIDL中定义的方法;接口的一个公有静态抽象类Stub
,继承于Binder
并且实现了IGoodsManager
;在Stub
内又有一个私有的静态类Proxy
,同时也继承了IGoodsManager
。下面对这几个类做一个大体的介绍:
- IInterface
表示远程调用的协议,也就是Binder对象的能力。里面只有一个asBinder的方法,说明这个接口不是孤立的,必须结合Binder使用。我们定义远程调用接口的时候必须继承此类,比如刚才AIDL中生成的接口。Stub和Proxy也实现了该接口。 - IBinder
它表示远程对象的基本接口,是轻量级RPC(远程过程调用)的核心,为进程内或者进程间高性能调用而设计的。其中的 transact 方法是远程调用的入口。实现了它也就拥有了跨进程传输的能力。 - Binder 与 Stub
Binder 是 IBinder标准的本地实现,其中的 onTransact 方法与 IBinder 的 transact 方法相对应。Stub就继承了Binder,在onTransact 方法中根据方法标识来调用对应的方法。Stub类是Binder本地服务,继承此类,实现其中的抽象方法即协议的所定义的功能。 - Proxy
很明显,这是一个代理,准确的说是远程Binder在客户端中的代理对象,通过它可以调用我们定义的方法。
上面是我画的一个类图。GoodsManager
是Binder服务的最终实现,也就是Binder实体;BinderProxy
是Binder在客户端中的代理,Proxy
中的IBinder
对象其实就是BinderProxy
。其中Proxy
的接口实现中先将方法参数包装到Parcel
对象_data
中,再通过调用BinderProxy
对象mRemote
的 transact 方法交互,这时Stub
的onTransact
方法被回调,最终辗转调用GoodsManager
中的实现Proxy
将返回的Parcel
对象_reply
转换成方法返回类型_result
并返回给调用方(如果定义的方法需要返回结果)。以上就完成了一次 Binder IPC。从中我们也可以发现Binder通信是典型的C/S架构。
顺便解释一下,跨进程传输的数据必须是序列化的,作为有Android特色的IPC方式Binder当然得用在Android平台上效率更高的Parcelable了。Serializable和Parcelable这两种序列化方式在平常的应用开发中就有不少接触,这里就不细说了。
Binder实现原理
上篇文章提到了进程隔离的概念,因此进程间无法直接通信。
Linux系统内存分用户空间(user space)和内核空间(kernel space)。用户空间就是我们进程运行的区域,而内核空间则是内核运行和提供服务的地方。内核可以访问其他用户空间也可以与底层的硬件交互,充当了用户空间或者说用户进程的管理者的角色,防止各个用户进程之间互相干扰。
我们要访问内核空间只有使用系统调用(system calls)这个方法,在我们的进程处于运行态时(占有CPU正在运行)。现在我们知道了访问内核空间的方法,但很遗憾的是Linux内核是不支持Binder通信的。但是Linux支持动态可加载模块:
可载入核心模组(英语:Loadable kernel module,缩写为 LKM),又译为载入式核心模组、可装载模块、可加载内核模块,或直接称为内核模块,是一种目的档(object file),在其中包含了能在作业系统内核空间运行的程式码。它们运行在核心基底(base kernel),通常是用来支援新的硬体,新的档案系统,或是新增的系统呼叫(system calls)。当不需要时,它们也能从记忆体中被卸载,清出可用的记忆体空间。
以上内容来自维基百科。总之Android系统就是通过这个模块访问内核空间。而这个模块就是大名鼎鼎的Binder驱动。
有了Binder驱动,用户进程直接是否可以通过它互相通信了呢?答案是否定的,这里要介绍一个ServiceManager(ContextManager)的角色,ServiceManager作为一个Binder服务,也是处于用户空间。SM所在的进程是Binder
IPC的守护进程,在系统启动的时候打开binder驱动,注册成为binder服务的大管家。我觉得这是很好理解的,毕竟有大量的server和client,需要一个角色来分发管理service。举个例子,SM好比是应用市场,我们开发者就是server,向应用市场上传应用(service),然后应用市场将应用分发给客户。
有了SM之后,应用进程间的通信可按顺序分解为三个Binder通信:
- server启动时向SM注册(
addService
),在这个通信过程中SM是服务端,而server是客户端 - client需要调用服务时通过SM获取服务(
getService
),在这个通信过程中SM是服务端,而client进程是客户端。 -
最终两个进程之间建立通信,此时显然client和server就是C/S的角色。
讲解了大体的通信过程,我们再回顾下前面那个AIDL的例子。
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
iGoodsManager = IGoodsManager.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
iGoodsManager = null;
}
};
在客户端进程中,我们通过Stub
的静态方法asInterface
将 IBinder
转化为需要的接口。
public static com.jellybean.ipclab.IGoodsManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.jellybean.ipclab.IGoodsManager))) {
return ((com.jellybean.ipclab.IGoodsManager) iin);
}
return new com.jellybean.ipclab.IGoodsManager.Stub.Proxy(obj);
}
在asInterface
方法内部又调用了IBinder
的queryLocalInterface
来获取对应的接口iin
,就像这个方法名所说,它会去查找IBinder
的本地接口,即Binder服务在本进程内的实现。假如server和client属于同一进程,iin
则不为null
,client可以直接调用。在我们的例子中,由于service运行在远程服务,所以最终会将这个IBinder
对象obj
作为参数创建一个Proxy
对象。前面有讲过,Proxy
实现了接口IGoodsManager
,但它实际上只是远程服务在客户端进程中的代理。通过IBinder#transact
与Binder#onTransact
(BinderProxy#transact
与Stub#onTransact
),代理调用远程服务中对应的方法交换数据,这一过程有Binder驱动在背后帮我们完成。Proxy将方法参数及方法签名交给Binder驱动,驱动转交给远程服务,远程服务处理完后将结果返回给驱动,最终驱动将处理结果传递给代理对象。整个过程对调用方来说只需要在调用接口内合适的方法即可,无需关注实现细节,这就是C/S架构和面向对象的抽象带来的力量。
public void addGoods(View view) {
if (iGoodsManager != null) {
try {
iGoodsManager.addGoods(new Goods("金坷垃", 9527));
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
public void getGoods(View view) {
if (iGoodsManager != null) {
try {
Goods goods = iGoodsManager.getGoods(9527);
if (goods != null) {
Toast.makeText(getApplicationContext(), "Got goods! goodsId:" + goods.getId() + " " +
"goodsName:" + goods.getName(), Toast.LENGTH_LONG).show();
}
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
Framework中的Binder
Framework中有大量服务是在Binder的基础上工作,比如四大组件相关的ActivityManagerService
、包管理PackageManagerService
和窗口管理WindowManagerService
等等。我们可以复习下Android系统的架构
上图大家应该比较熟悉了,初学Android的时候我们就是通过这张图了解了Android系统架构的分层设计。上层应用通过IPC与Java Framework中的Service通信,Framework使用JNI和IPC调用Native,Native层通过 系统调用底层驱动。
对Binder有了一定的理解后,我们就可以阅读Framework的源码。你会发现底层的BinderIPC和AIDL模型几乎是一样的,唯一的区别可能是AIDL的本地Binder命名是
XXX.Stub
,而Framework中则是
XXXNative
。比如Activity启动相关的
IActivityManager
接口对应于IGoodsManager,代表服务协议;
ActivityMangerNative
对应于
IGoodsManager.Stub
,代表本地Binder;
ActivityMangerProxy
对应于
IGoodsManager.Stub.Proxy
,表示远程代理;
ActivityMangerService
对应于
GoodsMangerService
中的
GoodsManager
,表示实际Binder服务。他们之间的继承和组织关系也是相同的。
Activity启动过程浅析
网上已经有不少Activity启动过程的分析,但是稍微过时了点,我发现Sources25中的实现又有更新。但是毕竟涉及到代码量过大,我就不一一具体分析了,有兴趣的可与自己去看源码。大概讲述下启动过程和几个注意点:
- Activity启动从Activity发起,在相关的重载方法中最终会调用
startActivityForResult(String who, Intent intent, int requestCode, @Nullable Bundle options)
,方法内再调用Instrumentation#execStartActivity
- 然后发起Binder IPC连接System Server中的Service,通过
ActivityManagerProxy
、ActivityManagerNative
和ActivityManagerService
-
ActivityManagerService
将任务先后交由ActivityStarter
、ActivityStackSupervisor
和ActivityStack
处理,这三个类之间的互相调用是整个启动过程中最复杂的。其中有解析Intent、识别启动模式、通过PackageManagerService
寻找目标Activity、创建进程、暂停其余Activity等步骤。 - 最终在
ActivityStackSupervisor
中调用IApplicationThread#scheduleLaunchActivity
再次发起IPC与目标Activity所在的App进程通信。其中涉及到IApplicationThread
接口,ApplicationThreadNative
和ApplicationThread
本地实现及ApplicationThreadProxy
代理对象。 - 在
ApplicationThread
中的具体实现最终辗转到ActivityThread
中的Handler内部类H
回调方法。 - 来到
ActivityThread#handleLaunchActivity
,通过ClassLoader创建Activity实例,将Activity
交由Instrumentation
来执行onCreate过程。
几个需要注意的细节:
-
ActivityStarter
是Android 7 新增的,专门用于解释如何启动并且实现启动过程。所有关于如何解析Intent
根据flags
找到对应的Activity和关联的Task
与Stack
的逻辑都在这里。起承上启下的作用 —— 上:AMS,下:ASS。
Controller for interpreting how and then launching activities.
This class collects all the logic for determining how an intent and flags should be turned into an activity and associated task and stack.
- 在启动过程和Activity对象管理中都用到
IBinder
对象token
作为标识,因为IBinder 传输时通过写入元数据到Parcel作为标识,在返回时可得到之前的Binder,实现了全局唯一的特性。
The data sent through transact() is a link Parcel, a generic buffer of data that also maintains some meta-data about its contents. The meta data is used to manage IBinder object references in the buffer, so that those references can be maintained as the buffer moves across processes. This mechanism ensures that when an IBinder is written into a Parcel and sent to another process, if that other process sends a reference to that same IBinder back to the original process, then the original process will receive the same IBinder object back. These semantics allow IBinder/Binder objects to
be used as a unique identity (to serve as a token or for other purposes) that can be managed across processes.
- 客户端发起IPC调用是同步等待的,直到结果返回才能执行别的操作。因此不应该在UI线程中执行Binder IPC。
This transaction API is synchronous, such that a call to
transact()
does not return until the target has returned from
Binder#onTransact
this is the expected behavior when calling an object that exists in the loca process, and the underlying inter-process communication (IPC) mechanism ensures that these same semantics apply when going across processes.
- Activity启动从App近处发起,传递到System Server进程后又回调App进程创建并调用onCreate等方法。两次IPC中,App进程先后是Client和Server,而 System Server 服务进程则先是Server后是Client 。刚不是说客户端发起IPC时会被挂起吗,怎么又能响应启动Activity的请求了?这是因为Binder 系统支持跨进程的递归调用。
The Binder system also supports recursion across processes. For example if process A performs a transaction to process B, and process B while handling that transaction calls transact() on an IBinder that is implemented in A, then the thread in A that is currently waiting for the original transaction to finish will take care of calling Binder.onTransact() on the object being called by B. This ensures that the recursion semantics when calling remote binder object are the same as when calling local objects.
-
onTransact
方法运行在本地Binder线程池中,这也是为什么ApplicationThread需要通过Handler调度将启动过程转移到UI线程。
The system maintains a pool of transaction threads in each process that it runs in. These threads are used to dispatch all IPCs coming in from other processes. For example, when an IPC is made from process A to process B, the calling thread in A blocks in transact() as it sends the transaction to process B. The next available pool thread in B receives the incoming transaction, calls Binder.onTransact() on the target object, and replies with the result Parcel. Upon receiving its result, the thread in process A returns to allow its execution to continue. In effect, other processes appear to use as additional threads that you did not create executing in your own process.
- ActivityThread 和 Instrumentation 在App启动过程中多次出现,其中ActivityThread是外交官,负责与外部通信。而Instrumentation 是内政大臣,Activity的创建暂停等都是由它来控制,每个Activity也都会持有对他的引用。前面有提到,打开一个Activity也是从
Instrumentation 中发起——Instrumentation#execStartActivity
。
总结
本篇主要介绍了Android IPC,以大量篇幅讲解了Binder的工作方式和原理。那么我们说Binder的时候到底在说什么。我觉得广义上的Binder就是指Binder通信方式。从C/S的角度看,Binder是server的本地服务——Binder实体,client中远程服务的代理对象——Binder引用。而底层实现上可以是Java层和C++层相关的几个类,甚至是一种物理设备:设备驱动在 dev/binder。对初学者来说Binder 可能不太好理解,建议先粗略地查看源码以及背景资料,然后写一个AIDL熟悉一下,最后通过阅读Framework的源码巩固理解。
参考
Kernel Space Definition
深刻理解Linux进程间通信(IPC)
Android Binder设计与实现 - 设计篇
Android进程间通信(IPC)机制Binder简要介绍和学习计划
绑定服务