Binder牌胶水

Binder牌胶水,如雷贯耳,在Android中无处不在,是每个Android程序猿居家旅行必备。有了它的存在,我们甚至可以不用深入了解App进程和系统进程、用户空间和内核空间、跨进程通讯等概念也可以做好应用层开发。但是呢,阅读本文之前,还是建议先看看《Binder学习指南》了解下这些概念。

Binder属于“问题领域”的命名?见名知意,作为胶水,“粘合”是他的主要职责。在Androd世界里,他到底粘合了什么?比如我们经常使用的startActivity()就涉及到进程通讯,Binder粘合了这些进程,弱化了我们对进程的感知,降低了开发难度。

本文来聊聊四瓶Binder胶水,他们功效不同,配方却完全一致。此文的重点有两个,其一,介绍下他们的配方,也就是类图模型,了解他们,对我们阅读Android源码,寻找源码的位置有很大的帮助;其二,既然作为胶水,我们要研究下他粘合了什么东西,粘合了哪些进程。这四瓶胶水的功效和配方分别是:

  1. AIDL:
    IInterface--Stub--Proxy--Stub具体实现
  2. ContentProvider:
    IContentProvider--ContentProviderNative--ContentProviderProxy--ContentProvider.Transport
  3. 管理四大组件的AMS:
    IActivityManager--ActivityManagerNative--ActivityManagerProxy--ActivityManagerService
  4. 负责ActivityThread和AMS之间的通讯
    IApplicationThread--ApplicationThreadNative--ApplicationThreadProxy--ApplicationThread

Binder文章很多,很难写出彩,相比于一言不合就晒C++代码,晒cpp文件的,本文重点还是从Java层出发解释Binder机制。本文所有代码在此:
https://github.com/geniusmart/binder-project

Binder胶水的原始配方

image

如上图,IInterface/IBinder/Binder/BinderProxy是Binder机制的核心api,理解这些接口/类,是研究Binder的前提。

接口通常代表所具备的能力,比如我们熟悉的Api里,SerializableParcelable代表其实现类是可序列化的;Iterable代表可迭代遍历,因此其实现类HashSetArrayList可使用Iterator进行遍历。

在这个类图的最顶层,有两个接口,IInterfaceIBinderIBinder代表跨进程传输的能力,而IInterface则代表远程服务端具备的能力

BinderIBinder的实现类,因此它具备跨进程传输的能力,它实际上就是远程Server端的Binder对象本身

Binder对象是给Server端对象本身,是Server进程用的,与此对应的BinderProxy则是远程Binder的代理对象,给Client进程用的(源码位于Binder类内部)。在跨越进程的时候,Binder驱动会自动完成这两个对象的转换。

以上都是冷冰冰的理论,读到这里我们仍然很困惑,我们需要能亲手掌控两个进程:Client进程和Server进程,并进行通讯,以此来熟悉Binder机制。因此,第一瓶胶水应运而生——AIDL,AIDL能实现这个需求。

以上四个类是android.os包给我们提供的Binder机制相关的api,基于这四个类,我们可以扩展出各种各样的Binder模型,实现各种各样的跨进程传输的场景。而本文所描述的四瓶胶水便是基于此套api的四种实现。

第一瓶胶水AIDL

假设你已经熟练掌握了AIDL,首先写个ICompute.aidl,代码很简单,如下:

// ICompute.aidl
package com.geniusmart.binder.aidl;

interface ICompute {
    int add(int a,int b);
}

此时,编译器会帮我们生成一个ICompute.java文件,这个类的可读性很差,为方便大家查阅,我将代码做了格式化,并拷贝一份放在temp包下,点击这里查看

这个类的细节我不打算多讲,大家可以查看一下文章开篇的那篇文章。对照着生成出来的ICompute.java文件,绘制如下Binder模型图:

image

1. 熟悉的配方:ICompute-Proxy-Stub-Stub具体实现

通过上图,我们发现AIDL的Binder模型是基于原始配方的扩展。当我们写完ICompute.aidl之后,IComputeStubProxy已经自动生成出来,他们的作用如下:

  • ICompute接口继承了IInterface,并写了add(a,b)的方法,代表Server端进程具备计算两数相加的能力。此方法将在Stub的具体实现类中重写。
  • Stub是一个抽象类,他本质是一个Binder,他存在的目的之一是约定好asInterfaceasBinderonTransact方法,减少我们开发具体Binder对象的工作量。此外,Stub即需要跨进程传输,又需要约定远端服务端具备的能力,因此他需要同时实现IInterfaceIBinder接口,通过上文的类图可以看得出来。
  • Proxy是代理对象,它在Client进程中使用,作用是以假乱真。他持有BinderProxy对象,BinderProxy能帮我们访问服务端Binder对象(本例中即Stub具体实现类)的能力。

AIDL让我们可以饭来张口衣来伸手,但是,服务端具备的具体能力,AIDL是没法给的,需要我们自力更生,所以接下来我们来写个Stub的具体实现,并验证下这个Binder模型的准确性。

2.验证远程对象和代理对象

在Server进程启动一个Service,定义Stub的实现类ComputeBinder,等待Client进程的连接,代码如下:

public class ComputeService extends Service {

    public static final String TAG = "Server进程";

    @Override
    public IBinder onBind(Intent intent) {
        return new ComputeBinder();
    }

    private static class ComputeBinder extends ICompute.Stub {

        @Override
        public int add(int a, int b) throws RemoteException {
            Log.i(TAG, TAG + this.getClass().getName() + "执行add()");
            return a + b;
        }
    }
}

接下来,我们来写连接远程服务的代码 ,核心代码如下:(完整代码点击查看)

private ServiceConnection conn = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        Log.i(TAG, TAG + "触发onServiceConnected : " + service.getClass().getName());
        mICompute = ICompute.Stub.asInterface(service);
        Log.i(TAG, TAG + "触发asInterface : " + mICompute.getClass().getName());
        Log.i(TAG, TAG + "触发add() : 远程方法执行结果为" + mICompute.add(3, 5));
    }
};

这里我设计了两种场景,分别让Server进程和Client进程执行连接远程服务,输出结果如下:

  • Client进程绑定Server进程的Service获取Binder
    (1)Client进程输出日志:
Client进程触发onServiceConnected : android.os.BinderProxy
Client进程触发asInterface : ICompute$Stub$Proxy
Client进程触发add() : result = 8

(2)Server进程输出日志:

Server进程ComputeService$ComputeBinder执行add()
  • Server进程绑定自身进程内的Service获取Binder
    Server进程输出日志:
Server进程触发onServiceConnected : ComputeService$ComputeBinder
Server进程触发asInterface : ComputeService$ComputeBinder
Server进程ComputeService$ComputeBinder执行add()
Server进程触发add() : 远程方法执行结果为8

结论:

  • 当Client进程连接远程时,会经过如下步骤:客户端获得BinderProxy对象->转换为ICompute$Stub$Proxy->服务端ComputeService$ComputeBinder执行相应的方法,并将数据返回给客户端->客户端取得数据。
  • 当Server进程连接自身时,始终调用的是Binder本体对象ComputeService$ComputeBinder

3.工作原理

说好了不谈细节,但是对于讲清楚Binder机制来说,不谈细节臣妾真的做不到,受限于篇幅,我还是克制住了,取而代之的是一张基于aidl的工作流程图,与其贴代码讲细节,还不如大家对照源码和流程图自己解析aidl的工作原理。如下:

4. 这瓶胶水粘合了什么?

这个问题对于AIDL来说很简单,他粘合了需要通讯的两个进程,在上文我们称之为Server进程和Client进程,在上文的UML类图中也有所体现。

第二瓶胶水ContentProvider

为了巩固这个模型,Binder牌胶水隆重推出第二个款式ContentProvider。谈起跨进程,我们自然而然会想起内容提供者。即使你不了解ContentProvider的原理,我们平常在使用api的时候,所做的事情就是让内容使用者进程去访问内容提供者进程的数据,所以这第二瓶胶水很贴近我们日常的Android开发生活。

首先先给出结论,也就是有关ContentProvider的Binder模型图:

1. ContentProvider Binder模型来源

接下来我们来分析下这张图是怎么来的:
作为一名Coder,应该永远保持好奇心,使用ContentProvider时,我们经常用这样的api:getContentResolver().query()/insert()/update()/delete(),你只要点进去看看源码,都能很轻易发现这个模型图,这个过程大概是这样的:

  • (1) 从内容使用者的角度出发,查看insert()源码
  • (2) 发现关键代码IContentProvider provider = acquireProvider(url);
  • (3) 活捉一只IInterface的子接口IContentProvider,对应AIDL的ICompute
  • (4) 那么问题来了,对应的Proxy\Stub\Stub具体实现在哪?一筹莫展中。
  • (5) 此路不通,换一条路,即从内容提供者角度出发,查看 ContentProvider的源码,找找线索。
  • (6) AS切换到Structure视图,观察类的结构,此时应该有点运气成分,我们发现了内部类Transport,他的继承关系是:class Transport extends ContentProviderNative{}。虽说此步需要点运气,但是Transport的中文意思为运输,颇有点跨进程传输的意思,所以也并非毫无根据。
  • (7) 看ContentProviderNative源码,发现其继承关系class ContentProviderNative extends Binder implements IContentProvider{},这就是我们熟悉的模型啊,对应AIDL中的Stub,而Transport则对应着Stub的具体实现类ComputeBinder
  • (8) 在ContentProviderNative内部,与他同一级的还有一个类,即ContentProviderProxy,至此,Binder模型相关的4个类或接口,我们已经集齐完毕。
  • (9) 最后,召唤神龙,深入学习ContentProvider的原理。

2.验证远程对象和代理对象

ContentProvider与AIDL不一样,AIDL中Binder对象可以轻易拿到,而我们在使用ContentProvider时候,通常都是通过ContentResolver来执行CRUD的操作,显然,他并不是一个Binder对象,追溯其源码,我们可以发现这样一行关键的代码:

public final @Nullable Uri insert(@NonNull Uri url, @Nullable ContentValues values) {
IContentProvider provider = acquireProvider(url);
//省略若干代码..

}

看到返回类型IContentProvider时就是你大声呼喊“真相只有一个”的时候,阅读源码的乐趣就在于抽丝剥茧,轻解衣裳,窥探最本真和纯洁的胴体。。。

这时候问题又来了,acquireProvider(url)是一个@hide方法,没法直接调用。解决办法很简单,即使用反射。核心代码如下:

Uri uri = Uri.parse("[content://com.geniusmart.binder.AccountProvider/account](https://link.jianshu.com?t=content://com.geniusmart.binder.AccountProvider/account)");
ContentResolver contentResolver = getContentResolver();
//通过反射调用hide方法
Method method = ContentResolver.class.getMethod("acquireProvider", new Class[]{Uri.class});
Object object = method.invoke(contentResolver, uri);
//打印Binder类型
Log.i(TAG, object.getClass().toString());

与AIDL中的验证思路一样,我们来分别验证下客户端和服务端执行此段逻辑之后的结果:

  • Client进程使用Server进程的ContentProvider
    输出结果为:class android.content.ContentProviderProxy
  • Server进程使用自己的ContentProvider
    输出结果为:class android.content.ContentProvider$Transport

结论:Client进程通过ContentProviderProxy访问Server进程的ContentProvider$Transport,实现进程间通讯。

以上所有代码均在文章开篇的Github项目里。

3. 这瓶胶水粘合了什么?

这个问题也比较简单,ContentProvider这瓶胶水粘合了内容使用者和内容提供者两个进程,写ContentProvider的这一方称之为Server进程,用ContentResolver的这一方称之为Client进程,两个进程通过Binder进行通讯。

另外两瓶胶水AMS和ApplicationThread

AIDL和ContentProvider这两个Binder模型,都有清晰的Client进程和Server进程,理解起来相对容易,而另外两个Binder模型所涉及到的进程双方则比较模糊。AMS和ApplicationThread有着千丝万缕的关系,所以我们放在一起讲。首先贴一下这两瓶胶水的配方:

AMS的Binder模型
ApplicationThread的Binder模型

看到这两个Binder模型图,真是感慨,这简直是一样的配方,熟悉的味道,所以以后如果有看到类似IXxxx/XxxxProxy/XxxxNative/Xxxx的命名规则,一定要想起这是Binder的配方。

Binder模型来源

这两个Binder模型图从何而来?
在一个夜黑风高的晚上,你我寂寞难耐,准备Fuck Source Code,于是决定挑个并不软的柿子来捏一捏,他就是startActivity(),大概会这么一些个前戏:
(1)context.startActivity()
(2)mBase.startActivity()
(3)ContextImpl.startActivity()
(4)mMainThread.getInstrumentation().execStartActivity()
(5)Instrumentation.execStartActivity()

源码阅到这里已经有一个小高潮了,因为我们发现了如下代码:


//IApplicationThread 是IInterface类型
IApplicationThread whoThread = (IApplicationThread) contextThread;
//ActivityManagerNative是Binder和IInterface类型
ActivityManagerNative.getDefault().startActivityAsUser()

此时Binder机制的雏形我们已经心中有数,之后少侠们可以各自发挥,深入理解这两个Binder机制的作用,获得更多的快感。

所以,理解这个模型的意义在于,当我们看到XxxxProxy执行逻辑时,当前进程所属的角色是Client进程,要查看该逻辑源码的时候,应该找到Server进程的Xxxx类查看。

Activity的启动流程的解析漫长而枯燥,不是此文的重点,大家了解下这两个类的意义即可:

  • ActivityManagerService
    AMS是Android中最核心的服务,主要负责系统中四大组件的启动、切换、调度及应用进程的管理和调度等工作。
  • ApplicationThread
    ApplicationThreadActivityThread的内部类,负责ActivityThreadActivityManagerService之间的通讯。

这两瓶胶水粘合了哪些进程?

通过Activity启动流程图我们来看下这两个Binder模型是如何发挥作用的,如下图:

Activity启动流程图

注:此张图片来源于这篇文章《startActivity流程分析(一)》,已征得博主同意引用在此处。

在这张图里,我们意识到除了第一、二瓶胶水描述到的App进程之外,Android世界里还有Launcher进程,系统进程等等,而Binder在其中如鱼得水,散发着万丈光芒。

AMS位于系统进程,处于Server进程的位置,Launcher进程和App进程作为Client进程,持有ActivityManagerProxy,与AMS进行通讯,召唤四大组件。而ApplicationThread位于应用进程,处于Server进程的位置,系统进程则作为Client进程,持有ApplicationThreadProxy,使得应用进程中的主线程(ActivityThread)和AMS之间可以进行通讯。

至此,对于Binder机制的认知应该要有所升华。

总结

用一张表格描述一下上文所讲的四个Binder模型:

能力 IInterface Binder抽象 BinderProxy Binder
AIDL ICompute Stub Proxy ComputeBinder
内容提供者 IContentProvider ContentProviderNative ContentProviderProxy ContentProvider.Transport
AMS IActivityManager ActivityManagerNative ActivityManagerProxy ActivityManagerService
ActivityThread和AMS之间的通讯 IApplicationThread ApplicationThreadNative ApplicationThreadProxy ApplicationThread

Binder并不神秘,对于应用层来说,Binder便是上文中我所绘制的各种模型图,我们需要明确两点,第一,Proxy作为代理对象,以假乱真,Client进程通过持有Proxy对象,进而调用Server进程中实际Binder对象的能力;第二,在使用任何一个Binder对象时,我们要明确,此时进行通讯的是哪两个进程,以及哪个进程充当Client或Server进程。

最后多说一句,关于Binder写了那么多,大牛们自成体系自然无需阅读此小文,而小小牛们看完之后也许仍然十分懵懂,所以写Binder文章略显尴尬,而在这个过程中收获最大的其实是作者本人。学习Binder,没有捷径,找准切入点(本文的切入点在于应用层的Binder模型以及每种模型涉及通讯的两个进程),然后Read the Fucking Source Code,最后写写文章做总结。

参考文章

Binder学习指南
startActivity流程分析(一)
Android应用程序组件Content Provider简要介绍和学习计划
Android进程间通信(IPC)机制Binder简要介绍和学习计划

作者:geniusmart
链接:https://www.jianshu.com/p/3d053abba04b
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345