原文链接:banshan.tech/Binder%E6%A…
本文分析基于Android P(9.0) 源码
在编程的世界中,不同进程间的通信、协同、合作随处可见。很多时候,人们习惯用IPC(I
nter P
rocess C
ommunication,跨进程通信)来称呼它们。譬如Binder在多数情况下也被称为Android世界中的IPC机制。但如果以应用开发者的视角来看,Binder也可以称为Android世界中的RPC(R
emote P
rocedure C
all,远程过程调用)机制。
-
IPC(
I
nterP
rocessC
ommunication,跨进程通信)泛指一切用于进程间传递信息量(传输数据只是传递信息量的一个子集)的方式,譬如socket/pipe/FIFO/semaphore等。这些名称表征的是信息传输的具体方式,而不涉及信息的处理和加工。
-
RPC(
R
emoteP
rocedureC
all,远程过程调用)一种建构于IPC基础之上的方法调用。对于客户端而言,它可以感知到的仅仅是方法调用和获取返回值两个过程。然而实际上这个方法内部完成了客户端参数打包,数据传输,服务端数据解包,服务端数据处理,服务端结果返回等一系列中间过程。只不过这些过程对于客户端而言都是“透明”的。所以可以说IPC只是RPC中的一个环节,除此之外,RPC还包含数据打包,解包以及处理的过程,它们可以统称为信息的处理和加工过程。
1. Android中为什么需要大量的RPC?
下面举一个剪贴板的例子,来直观地呈现RPC的内涵。
通过如下代码,我们可以将一段文字复制到剪贴板。在执行第8行代码后,便可以将文本复制到剪贴板上。这里事先透露下,第8行代码本质上是一个RPC。
1 // 获取系统剪贴板
2 ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
3
4 // 创建一个剪贴数据集,包含一个普通文本数据条目(需要复制的数据)
5 ClipData clipData = ClipData.newPlainText(null, "需要复制的文本数据");
6
7 // 把数据集设置(复制)到剪贴板
8 clipboard.setPrimaryClip(clipData);
复制代码
那么RPC和非RPC的差异到底在什么地方呢?下面用一幅图来解释这个问题。
在RPC的过程中,进程B来进行真正的方法执行,相当于为进程A提供了某种服务,因此进程B在RPC的过程中也可以称为Server进程,进程A对应地可以称为Client进程。
接着再来讨论一个问题,为什么Android中需要使用大量的RPC?将RPC换成本地方法不可以么?
答案是不可以。因为Android本身是一个中心化管理的系统,RPC可以保证一个Server进程管理众多Client进程的调用请求,且能够实现系统状态的统一管理。举个例子,如果我们在一个App中将一段文字复制到剪贴板,假设这个复制过程是调用本地方法完成的(复制状态仅局限于App进程),那么离开这个App后新的剪贴板就不会再有这段文字。反之,如果我们采用RPC来完成复制,那么最终的文字将传递给了Server进程。而Server进程通常是常驻内存,所以即便我们离开App,剪贴板上的文字也依然存在,保证它可以被粘贴到其他App中。在Android中,大量的系统服务都是在system_server进程中完成各自功能的,譬如ActivityManagerService。
2. Binder世界里的service到底是什么概念?
在进行具体阐述之前,我们先要做一个限定。以下所讨论的service是类似于ActivityManagerService、PackageManagerService、WindowManagerService等一系列的服务,它们是Binder语义下的service,而不是Android四大组件中的service。
不过“服务(service)”到底是什么意思呢?它是一类近似功能的统称。譬如“商人的服务”,就可以包含“购买薯片”、“购买可乐”、“购买沙发”、“购买电视”等一系列功能。ActivityManagerService是一个类,它里面定义实现了很多方法,详细描述了每一项功能应该来如何提供。但是它只是一个模板,是无法实际提供服务的。真正提供服务的是ActivityManagerService实例化出来的对象。在Android中,ActivityManagerService只会实例化出来一个对象,而它就是真正为应用提供AMS服务的人。
/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
2877 public static final class Lifecycle extends SystemService {
2878 private final ActivityManagerService mService;
2879
2880 public Lifecycle(Context context) {
2881 super(context);
2882 mService = new ActivityManagerService(context);
2883 }
复制代码
这个对象是在system_server进程启动的时候创建出来的,具体是在ActivityManagerService.Lifecycle的构造过程中完成。
/frameworks/base/services/java/com/android/server/SystemServer.java
559 mActivityManagerService = mSystemServiceManager.startService(
560 ActivityManagerService.Lifecycle.class).getService();
复制代码
由于整个系统中,ActivityManagerService实例化的对象只有一个,所以将它称为“AMS”、“AMS实例化的对象”都无所谓。这就好比整个饭店就一个厨师,你叫他“厨师”还是“王厨师”都不影响理解。
2.1 service与进程/线程的关系
如果将service看作一个可以提供服务的Java对象,那么这个问题将会迎刃而解。
Java对象是存放在堆上的,因此可以在同一个进程里的不同线程间共享,所以service(准确来说应该是service对象的方法)也可以在不同的线程里运行。
另外,一个进程里可以构造出成千上万的对象,因此一个进程里也可以存在成千上万的service。而且同一个类型的service也可以存在很多个,譬如我们可以在system_server进程中同时构造ActivityManagerService对象A/B/C,三个AMS对象表示三个可以提供服务的实体,这就好比饭店里现在有了三个厨师,你可以跟其中任何一个请求服务。当然,在实际的Android系统中,同一个类型的service多数情况下只有一个对象。但现实情况不存在并不代表理论上不可实现,所以理论上同一个类型的service可以在一个进程中存在多个对象。
3. RPC的内部是如何构成的?
本文开篇提到,对于客户端而言,它可以感知到的仅仅是方法调用和获取返回值两个过程。那么具体到剪贴板这个例子来的话,对于Android应用的开发者而言,他感知到的只有下面两件事:
对于Android应用的开发者而言,他感知到的只有下面两件事:
- 我调用了clipboard.setPrimaryClip()方法
- 剪贴板上出现了我想要复制的文字
在这个过程中,应用开发者根本感知不到这是一次跨进程的调用,也感受不到调用背后的数据传输。RPC机制将这一切都封装了起来,因此开发者可以天真地认为所有这一切都发生在应用进程。而这也正是系统设计者希望给开发者带去的便利和简化,既是理解上的简化,也是使用上的简化。
不过一个有追求的开发者通常只会选择使用上的简化,而不会局限在理解上的简化。所以下面我将用一种颇具趣味性的方式来继续阐述。
3.1 “人才中心”的例子
所有的算法和设计模式都是从社会生活中抽象提炼出来的。所以本着“从群众中来,到群众中去”的原则,我们赋予冰冷的源码以生命,从社会生活的角度来理解RPC,来理解Binder。
前几年,城市里面新建了几座各具特色的人才中心,每一个中心都汇聚了来自五湖四海的奇人异士。他们中有手艺精湛的厨师,有投机倒把的商人,有妙笔生花的作家,还有勤勤恳恳的果农。人才中心有很多电话间,方便他们打电话的时候互不干扰。有一天,小明想从人才中心A里面的贾商人那里买一个键盘,于是拨通了A大厦的电话……
上面的例子完全可以映射成一次RPC。人才中心表示进程,电话间表示线程,职业表示service所属的类,贾商人表示一个具体的提供service的对象,买键盘表示服务的方法,打电话表示数据传输的方式,小明表示Client进程。
小明打电话给人才中心A:Client进程跟Server进程A通信,除此之外,Client进程也可以跟Server进程B/C/D通信。
小明想找贾商人买一个键盘(伪代码表示如下):
第1行表示贾商人出现了,第2行表示他在人才登记处(非人才中心)将自己的名字登记在册。
1 Merchant jia = new Merchant("Jia");
2 ServiceManager.addService("MerchantJia", jia);
复制代码
下面的代码表示小明打电话购买键盘的过程。第1行表示他从人才登记处要到了贾商人的号码,第2行表示他打电话给贾商人提出购买键盘的请求。贾商人接到请求后,立马发货,返回值result表明小明收到了键盘。
1 IMerchant merchantJia = IMerchant.Stub.asInterface(ServiceManager.getService("MerchantJia"));
2 Keyboard result = merchantJia.buyKeyboard();
复制代码
那如果小明不想找贾商人买了,换成甄商人会怎么样?
甄商人在人才登记处(非人才中心)登记:
1 Merchant zhen = new Merchant("Zhen");
2 ServiceManager.addService("MerchantZhen", zhen);
复制代码
小明打电话购买键盘:
1 IMerchant merchantZhen = IMerchant.Stub.asInterface(ServiceManager.getService("MerchantZhen"));
2 Keyboard result = merchantZhen.buyKeyboard();
复制代码
不管是贾商人还是甄商人,他们都是商人,因此都是由Merchant类实例化而来。因此,职业“商人”就映射为了“Merchant”类,而类实例化出来的对象就是具体提供service的对象(贾商人和甄商人),表示提供某一类服务的实体。
贾商人去电话间接电话:
人才中心接线员收到小明的电话,说要找贾商人,于是给贾商人分配了电话间D。之所以分配D,是因为A/B/C三个电话间现在都有人在使用。类比回源码,人才中心表示Server进程, 电话间表示线程。
人才中心接收到小明的请求,表示Server进程接收到Client进程的数据。之后便决定将它交由贾商人处理,表示Server进程会将Client传输过来的数据交由MerchantJia这个对象来处理。接着是分配电话间,A/B/C三个电话间正在被使用,表示目前有三个Binder线程正在处理其他请求。于是最终将电话间D分配给贾商人,表示service的方法接下来将运行在线程D中。
3.2 剪贴板的例子
接下来以上文中剪贴板复制文本的代码为样本,来阐述一次RPC中所涵盖的具体过程。
1 // 获取系统剪贴板
2 ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
3
4 // 创建一个剪贴数据集,包含一个普通文本数据条目(需要复制的数据)
5 ClipData clipData = ClipData.newPlainText(null, "需要复制的文本数据");
6
7 // 把数据集设置(复制)到剪贴板
8 clipboard.setPrimaryClip(clipData);
复制代码
3.2.1 service对象的创建过程
剪贴板服务对象位于system_server进程,而它的代理对象则可以分布在所有需要此项服务的App进程中。
因此,剪贴板服务对象的创建也发生在system_server进程(Server进程)。它是ClipboardImpl类型,这个类里面的方法就是剪贴板服务的具体实现。
/frameworks/base/services/core/java/com/android/server/clipboard/ClipboardService.java
232 private class ClipboardImpl extends IClipboard.Stub {
233 @Override
234 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
235 throws RemoteException {
236 try {
237 return super.onTransact(code, data, reply, flags);
238 } catch (RuntimeException e) {
239 if (!(e instanceof SecurityException)) {
240 Slog.wtf("clipboard", "Exception: ", e);
241 }
242 throw e;
243 }
244
245 }
246
247 @Override
248 public void setPrimaryClip(ClipData clip, String callingPackage) {
249 synchronized (this) {
250 if (clip == null || clip.getItemCount() <= 0) {
251 throw new IllegalArgumentException("No items");
252 }
253 final int callingUid = Binder.getCallingUid();
254 if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage,
255 callingUid)) {
256 return;
257 }
258 checkDataOwnerLocked(clip, callingUid);
259 setPrimaryClipInternal(clip, callingUid);
260 }
261 }
复制代码
剪贴板服务对象是在ClipboardService.onStart方法中创建并注册到ServiceManager中的,整个过程都发生在system_server进程的启动过程中。
/frameworks/base/services/core/java/com/android/server/clipboard/ClipboardService.java
192 @Override
193 public void onStart() {
194 publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl());
195 }
复制代码
/frameworks/base/services/java/com/android/server/SystemServer.java
1064 traceBeginAndSlog("StartClipboardService");
1065 mSystemServiceManager.startService(ClipboardService.class);
1066 traceEnd();
复制代码
3.2.2 寻找service对象的代理对象
1 // 获取系统剪贴板
2 ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
3
4 // 创建一个剪贴数据集,包含一个普通文本数据条目(需要复制的数据)
5 ClipData clipData = ClipData.newPlainText(null, "需要复制的文本数据");
6
7 // 把数据集设置(复制)到剪贴板
8 clipboard.setPrimaryClip(clipData);
复制代码
寻找代理对象的过程发生在Client进程中。上面第2行的clipboard对象是一层封装,其内部就是剪贴板服务对象的代理对象(原谅我表述得这么拗口,但为了含义的准确表达,牺牲一些语言的美感也无可厚非)。
/frameworks/base/core/java/android/app/ContextImpl.java
1719 @Override
1720 public Object getSystemService(String name) {
1721 return SystemServiceRegistry.getSystemService(this, name);
1722 }
复制代码
/frameworks/base/core/java/android/app/SystemServiceRegistry.java
1012 public static Object getSystemService(ContextImpl ctx, String name) {
1013 ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
1014 return fetcher != null ? fetcher.getService(ctx) : null;
1015 }
复制代码
/frameworks/base/core/java/android/app/SystemServiceRegistry.java
178 private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
179 new HashMap<String, ServiceFetcher<?>>();
复制代码
/frameworks/base/core/java/android/app/SystemServiceRegistry.java
261 registerService(Context.CLIPBOARD_SERVICE, ClipboardManager.class,
262 new CachedServiceFetcher<ClipboardManager>() {
263 @Override
264 public ClipboardManager createService(ContextImpl ctx) throws ServiceNotFoundException {
265 return new ClipboardManager(ctx.getOuterContext(),
266 ctx.mMainThread.getHandler());
267 }});
复制代码
SYSTEM_SERVICE_FETCHERS是一个HashMap,它以键值对的方式存储了很多系统服务对象的代理对象(或其wrapper对象)。对剪贴板而言,getSystemService方法最终会创建一个ClipboardManager对象并返回。
/frameworks/base/core/java/android/content/ClipboardManager.java
85 public ClipboardManager(Context context, Handler handler) throws ServiceNotFoundException {
86 mContext = context;
87 mHandler = handler;
88 mService = IClipboard.Stub.asInterface(
89 ServiceManager.getServiceOrThrow(Context.CLIPBOARD_SERVICE));
90 }
复制代码
ClipboardManager的构造方法里,88行尤为关键。首先根据字符串"clipboard"去ServiceManger中找到剪贴板服务对象的代理对象,此时获得的代理对象只具有跨进程通信的能力。接着通过asInterface为这个代理对象赋予剪贴板的能力。
/frameworks/base/core/java/android/content/Context.java
3727 public static final String CLIPBOARD_SERVICE = "clipboard";
复制代码
对于Java而言,接口相当于一种能力。在Binder的世界中,一个服务对象的代理对象通常封装了特定的服务接口,譬如剪贴板就是IClipboard,表示该对象具有剪贴板服务所能提供的诸多能力,譬如复制文字、粘贴文字等。另外,该代理对象内部有个字段封装了IBinder接口,表示该字段具有跨进程通信的能力,它在每次IPC的过程中都会发挥作用。
IClipboard.Stub是AIDL文件自动生成的一个类,在最终生成的文件中还有一个类是IClipboard.Stub.Proxy,它们是Binder原生类的一层封装。相较于Binder原生类,它们多了一层数据打包和解包的过程。
IClipboard.java(源码中没有,是在编译时由IClipboard.aidl文件间接生成的)
1 /** Local-side IPC implementation stub class. */
2 public static abstract class Stub extends android.os.Binder implements android.content.IClipboard
复制代码
1 private static class Proxy implements android.content.IClipboard
复制代码
IClipboard.Stub.asInterface方法给原本只具有IBinder(跨进程通信)能力的对象赋予IClipboard(剪贴板)能力。这样一来,得到的代理对象就同时兼具了跨进程通信的能力和剪贴板的能力。跨进程通信的能力对开发者而言是透明的,而剪贴板的能力才是他们真正关心的。
3.2.3 通过代理对象进行RPC
最终调用clipboard.setPrimaryClip(clipData)往剪贴板上写数据时,实际底层调用的却是mService.setPrimaryClip方法,mService就是刚刚通过asInterface得到的代理对象。
/frameworks/base/core/java/android/content/ClipboardManager.java
100 public void setPrimaryClip(@NonNull ClipData clip) {
101 try {
102 Preconditions.checkNotNull(clip);
103 clip.prepareToLeaveProcess(true);
104 mService.setPrimaryClip(clip, mContext.getOpPackageName());
105 } catch (RemoteException e) {
106 throw e.rethrowFromSystemServer();
107 }
108 }
复制代码
mService.setPrimaryClip方法最终调用的是IClipboard.Stub.Proxy.setPrimaryClip方法,将参数打包放入 _data中,并从 _reply中解包读出Server端传输过来的返回值(这个用来示例的方法中没有返回值)。而真正的跨进程传输是通过下面第16行完成的。mRemote的类型为android.os.IBinder,表明它具有跨进程传输的能力。调用它的transact方法表示将打包后的参数发送给Server进程。
IClipboard.java(源码中没有,是在编译时由IClipboard.aidl文件间接生成的)
1 @Override public void setPrimaryClip(android.content.ClipData clip, java.lang.String callingPackage, int userId) throws android.os.RemoteException
2 {
3 android.os.Parcel _data = android.os.Parcel.obtain();
4 android.os.Parcel _reply = android.os.Parcel.obtain();
5 try {
6 _data.writeInterfaceToken(DESCRIPTOR);
7 if ((clip!=null)) {
8 _data.writeInt(1);
9 clip.writeToParcel(_data, 0);
10 }
11 else {
12 _data.writeInt(0);
13 }
14 _data.writeString(callingPackage);
15 _data.writeInt(userId);
16 boolean _status = mRemote.transact(Stub.TRANSACTION_setPrimaryClip, _data, _reply, 0);
17 if (!_status && getDefaultImpl() != null) {
18 getDefaultImpl().setPrimaryClip(clip, callingPackage, userId);
19 return;
20 }
21 _reply.readException();
22 }
23 finally {
24 _reply.recycle();
25 _data.recycle();
26 }
27 }
复制代码
数据传输的过程最终由Binder Driver来负责,所以上述的transact方法最终会通过ioctl
的系统调用进入到内核空间,通过一系列的驱动函数将数据发送给Server进程。
3.2.4 服务对象处理接收到的请求
Server进程接收到Client进程传输来的参数数据后,就会开始实际的处理。这里我们跳过Binder线程选取的过程,因为这个选择过程发生在Binder Driver中,等到以后专门写Binder Driver的时候我们再展开讨论。
剪贴板服务对象接收到请求后,最终会调用ClipboardImpl.onTransact方法。
/frameworks/base/services/core/java/com/android/server/clipboard/ClipboardService.java
234 public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
235 throws RemoteException {
236 try {
237 return super.onTransact(code, data, reply, flags);
238 } catch (RuntimeException e) {
239 if (!(e instanceof SecurityException)) {
240 Slog.wtf("clipboard", "Exception: ", e);
241 }
242 throw e;
243 }
244
245 }
复制代码
这个方法接着会调用父类IClipboard.Stub的onTransact方法。
IClipboard.java(源码中没有,是在编译时由IClipboard.aidl文件间接生成的)
1 @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
2 {
3 java.lang.String descriptor = DESCRIPTOR;
4 switch (code)
5 {
6 case INTERFACE_TRANSACTION:
7 {
8 reply.writeString(descriptor);
9 return true;
10 }
11 case TRANSACTION_setPrimaryClip:
12 {
13 data.enforceInterface(descriptor);
14 android.content.ClipData _arg0;
15 if ((0!=data.readInt())) {
16 _arg0 = android.content.ClipData.CREATOR.createFromParcel(data);
17 }
18 else {
20 _arg0 = null;
21 }
22 java.lang.String _arg1;
23 _arg1 = data.readString();
24 int _arg2;
25 _arg2 = data.readInt();
26 this.setPrimaryClip(_arg0, _arg1, _arg2);
27 reply.writeNoException();
28 return true;
29 }
....
....
131 }
复制代码
onTransact方法是一个大型switch-case现场,通过传输数据中的code来判断将要调用哪个方法。譬如当Client进程如下RPC时,Server进程便会走进上述代码中第11行的代码分支。
1 clipboard.setPrimaryClip(clipData);
复制代码
走进分支后,会将参数解包,并最终调用this.setPrimaryClip方法。这时回到原来的ClipboardImpl类,执行它的setPrimaryClip方法。
/frameworks/base/services/core/java/com/android/server/clipboard/ClipboardService.java
247 @Override
248 public void setPrimaryClip(ClipData clip, String callingPackage) {
249 synchronized (this) {
250 if (clip == null || clip.getItemCount() <= 0) {
251 throw new IllegalArgumentException("No items");
252 }
253 final int callingUid = Binder.getCallingUid();
254 if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage,
255 callingUid)) {
256 return;
257 }
258 checkDataOwnerLocked(clip, callingUid);
259 setPrimaryClipInternal(clip, callingUid);
260 }
261 }
复制代码
当setPrimaryClip执行完毕后,(假设它有返回值)返回值将会一层层传递到native层,并最终再次通过系统调用进入Binder Driver将返回值发送回Client进程。
Client进程接收到返回值之后,便会结束此次RPC,然后继续执行RPC后面的代码。
至此,一次完整的RPC过程便结束了。
4. 总结
本文从应用开发者的视角出发,将Binder看作Android世界中的RPC(R
emote P
rocedure C
all,远程过程调用)机制。首先介绍了RPC的通用概念,以及Android中为什么需要大量的RPC。接着进入到Binder机制内部,完整阐述了一次Binder RPC的过程,并通过“人才中心”的案例形象化地展现Binder的本质。
此外,就service/进程/线程之间的关系进行了明确的梳理,希望能够帮助大家扫除日常开发中的混淆和困惑。