阅读 388

Binder概述

原文链接:banshan.tech/Binder概述/

本文分析基于Android P(9.0) 源码

在编程的世界中,不同进程间的通信、协同、合作随处可见。很多时候,人们习惯用IPC(Inter Process Communication,跨进程通信)来称呼它们。譬如Binder在多数情况下也被称为Android世界中的IPC机制。但如果以应用开发者的视角来看,Binder也可以称为Android世界中的RPC(Remote Procedure Call,远程过程调用)机制。

  • IPC(Inter Process Communication,跨进程通信)

    泛指一切用于进程间传递信息量(传输数据只是传递信息量的一个子集)的方式,譬如socket/pipe/FIFO/semaphore等。这些名称表征的是信息传输的具体方式,而不涉及信息的处理和加工。

  • RPC(Remote Procedure Call,远程过程调用)

    一种建构于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与非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应用的开发者而言,他感知到的只有下面两件事:

  1. 我调用了clipboard.setPrimaryClip()方法
  2. 剪贴板上出现了我想要复制的文字

在这个过程中,应用开发者根本感知不到这是一次跨进程的调用,也感受不到调用背后的数据传输。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(Remote Procedure Call,远程过程调用)机制。首先介绍了RPC的通用概念,以及Android中为什么需要大量的RPC。接着进入到Binder机制内部,完整阐述了一次Binder RPC的过程,并通过“人才中心”的案例形象化地展现Binder的本质。

此外,就service/进程/线程之间的关系进行了明确的梳理,希望能够帮助大家扫除日常开发中的混淆和困惑。

原文链接:banshan.tech/Binder概述/