美文网首页AndroidAndroid原理android 源码分析
可能是讲解Binder机制最好的文章

可能是讲解Binder机制最好的文章

作者: 工程师milter | 来源:发表于2016-07-13 00:33 被阅读11430次

    网上关于Binder的文章有很多,不少大牛高手围绕Binder施展了十八般武艺,想要将它解剖干净展示给大家看,文章的水平都很高,但可惜的是,都有点对开发者不友好,为什么?因为它们都是down-to-top,而不是top-to-down。作为应用开发者,我们肯定更喜欢top-to-down的讲解。本文则是一种尝试,可把它当作Weishu这篇文章的上篇来阅读。
    声明:本文属作者原创,欢迎随意转载。但请不要修改文章内容并在开头注明本人简书账号milter和原文链接。

    一、确定问题

    本文围绕着这样一个问题展开:如何从进程A传两个整数给进程B,进程B把两个数相加后返回结果给进程A。

    二、Binder机制框架概览

    Android给我们提供了跨进程通信的一揽子解决方案。下面我们从总体上看一看这个方案是怎样设计的:
    进程A通过bindService方法去绑定在进程B中注册的一个service,系统收到进程A的bindService请求后,会调用进程B中相应serviceonBind方法,该方法返回一个特殊对象,系统会接收到这个特殊对象,然后为这个特殊对象生成一个代理对象,再将这个代理对象返回给进程A,进程A在ServiceConnection回调的onServiceConnected方法中接收该代理对象,依靠这个代理对象的帮助,就可以解决我们的问题啦。

    三、分步骤分析

    step 1: 进程B创建Binder 对象

    为进程B实现一个特殊的对象,就是前面提到的serviceonBind方法要返回的对象。这个对象有两个特性:

    • 一个是具有完成特定任务的能力(在我们的问题中,就是将两个整数相加并返回结果的能力)
    • 一个是被跨进程传输的能力。

    什么样的对象具有这样的能力呢?答案是Binder类的对象。下面我们分析一下Binder是怎样拥有这两个能力的。
    Binder中有如下关键方法:

     public class Binder implement IBinder{
            void attachInterface(IInterface plus, String descriptor)
            IInterface queryLocalInterface(Stringdescriptor) //从IBinder中继承而来
            boolean onTransact(int code, Parcel data, Parcel reply, int flags)//暂时不用管,后面会讲。
            ......
            final class BinderProxy implements IBinder {
            ......//Binder的一个内部类,暂时不用管,后面会讲。
            }
    }```
    Binder具有被跨进程传输的能力是因为它实现了`IBinder`接口。系统会为每个实现了该接口的对象提供跨进程传输,这是系统给我们的一个很大的福利。
    Binder具有的完成特定任务的能力是通过它的`attachInterface`方法获得的,我们可以简单理解为该方法会将(`descriptor,plus`)作为(`key,value`)对存入`Binder`对象中的一个`Map<String,IInterface>`对象中,`Binder`对象可通过`attachInterface`方法持有一个`IInterface`对象(即`plus`)的引用,并依靠它获得完成特定任务的能力。`queryLocalInterface`方法可以认为是根据`key`值(即参数 `descriptor`)查找相应的`IInterface`对象。`onTransact`方法暂时不用管,后面会讲到。
    
    好的,现在我们来实现`IInterface`和`Binder`对象,概略代码如下:
    
    ```java 
     public interface IPlus extends IInterface {
             public int add(int a,int b);
    }
    
    public class Stub extends Binder {
              @Override
              boolean onTransact(int code, Parcel data, Parcel reply, int flags){
              ......//这里我们覆写了onTransact方法,暂时不用管,后面会讲解。
              }
              ......
    }
    IInterface plus = new IPlus(){//匿名内部类
     public int add(int a,int b){//定制我们自己的相加方法
                 return a+b;
             }
              public IBinder asBinder(){ //实现IInterface中唯一的方法,
                    return null ;
               }
    };
    Binder binder = new Stub();
    binder.attachIInterface(plus,"PLUS TWO INT");```
    ##step 2: 进程A接收进程B的Binder对象
    好了,现在我们有了这个特殊的对象`binder`,可以在进程B的`service`中的`onBind`方法将它返回了,即`return binder ;`
    下面就是见证奇迹的时候。系统会首先收到这个`binder`对象,然后,它会生成一个`BinderProxy`(就是前面提到的Binder 的内部类)类的对象,姑且称之为`binderproxy`,然后将该对象返回给进程A,现在进程A终于在`onServiceConnected`方法中接收到了`binderproxy`对象(心情有木有小激动?)。为了下面讲解方便,再次贴出`Binder`类的概要信息。
    ```java
    public class Binder implement IBinder{
            void attachInterface(IInterface plus, String descriptor)
              IInterface queryLocalInterface(Stringdescriptor) //从IBinder中继承而来
              boolean onTransact(int code, Parcel data, Parcel reply, int flags)//暂时不用管,后面会讲。
             final class BinderProxy implements IBinder {
                        IInterface queryLocalInterface(Stringdescriptor) {
                              return null ;//注意这行代码!!
                                                      //下面会讲到。这行代码只是示例,不是源代码。
                          }
                   ......
                }
    }```
    
    此时的进程A以为收到的是`binder`对象,它兴奋了,它迫不及待地要通过`queryLocalInterface`方法获取这个`binder`的`plus`对象,利用该对象的加法功能进行加法计算。可结果呢?
    首先,`binderproxy.queryLocalInterface("PLUS TWO INT")`调用是合法的,因为`queryLocalInterface`方法是`IBinder`中的方法,而`BinderProxy`和`Binder`都实现了`IBinder`接口。但是,`binderproxy`对象显然没有`plus`对象,因为它根本就没有`attachInterface`方法(这是`Binder`才有滴)。所以,可想而知,进程A的`binderproxy.queryLocalInterface("PLUS TWO INT")`调用返回的将是一个`null`(参见上面的示例代码)。
    ##step 3: 进程A利用进程B传过来的对象发起请求
    进程A出离愤怒了,我要的是`binder`,我要的是它里面的`plus`来帮我完成加法运算,进程B竟然给我一个冒牌货`binderproxy`(显然,它冤枉了进程B,都是系统惹得祸)。
    正在进程A气得头顶冒烟时,`binderproxy`对象说话了:“别生气进程A,我虽然只是`binder`对象的代理,但是,我也不是吃素的,你把你的数据(两个`int`)和你想进行的操作(`plus.add`)通过我的`transact`方法(这是在`IBinder`接口中定义的方法)交给我,我可以替你向`binder`对象请求你需要的功能,等`binder`对象把结果给我时,我再把结果交给你不就行了?”
    于是,进程A通过`binderproxy`对象的`transact`方法,提交了请求。代码概略如下:
     ```java
     android.os.Parcel data = android.os.Parcel.obtain();
    android.os.Parcel reply = android.os.Parcel.obtain();
     int _result;
     data.writeInterfaceToken("PLUS TWO INT"); 
      data.writeInt(a); 
     data.writeInt(b); 
    binderproxy.transact(1, data, reply, 0);//为简单起见,最后一个0暂时不管它```
    简单解释一下上面代码。`data`是用来写进程A的数据的(即整数 a和b),`reply`是准备用来接收结果的。`transact`方法中的第一个参数是整数1,它是进程A与进程B的一个约定,1就代表想让进程B对进程A传入的数据执行加法操作。这个约定也可以定义在 Stub类中,如下所示:
    `public static final int ADD = 1;`此时,我们可以将`binderproxy.transact(1, data, reply, 0);`中的1替换为`Stub.ADD`。`Stub.ADD`其实可以是任何整数值的,我们选择1纯属为了简单。
    ##step 4: 进程B收到并处理进程A的请求
    `binderproxy.transact`调用发生后,会引起系统的注意,系统意识到`binderproxy`想找它的真身`binder`对象执行一个操作了(看!系统其实一直存着`binder`和`binderproxy`的对应关系呢!)。于是系统将这个请求中的数据转发给`binder`对象,`binder`对象将会在`onTransact`中收到`binderproxy`传来的数据(`Stub.ADD,data,reply,0`),于是它从`data`中取出进程A传来的数据,又根据`Stub.ADD`确定进程A想让它执行加法操作,于是它就执行了加法操作,并把结果写回`reply`。代码概略如下:
    ```java
     public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException 
    { 
              switch (code) { 
                    case INTERFACE_TRANSACTION: { 
                      reply.writeString(DESCRIPTOR);
                         return true; 
                    } //样板代码,不用管,下一行才是重点。
                    case Stub.ADD: { 
                          data.enforceInterface("PLUS TWO INT"); 
                            int _arg0; 
                            _arg0 = data.readInt();
                           int _arg1; 
                            _arg1 = data.readInt();
                           int  _result = this.queryLocalIInterface("PLUS TWO INT") .add(_arg0, _arg1); 
                           reply.writeNoException(); 
                            reply.writeInt(_result); 
                            return true; 
                      }
               } 
          return super.onTransact(code, data, reply, flags); 
    }```
    
    简单解释一下以上代码。我们知道进程A写数据时写入了一个`InterfaceToken`,就是这行代码
    `data.writeInterfaceToken("PLUS TWO INT"); `
    这个意思是说,让进程B在自己的`binder`对象中利用`PLUS TWO INT`调用`queryLocalIInterface`方法查找相应的`IInterface`对象,进程A要执行的操作就在该对象中,至此,我们很容易理解`Stub.ADD`就代表了`plus`中的`add`方法。这是一个二级查找过程,即通过`PLUS TWO INT`确定要`plus`来执行功能,通过`Stub.ADD`确定要执行`plus`中的`add`方法。
    ##step 5: 进程A获取进程B返回的处理结果
    
    进程B把结果写入`reply`后,进程A就可以从`reply`读取结果了。代码概略如下:
    ```java
    binderproxy.transact(Stub.ADD, data, reply, 0); 
    reply.readException(); 
    _result = reply.readInt();```
    
    -----2016.07.19补充----
    好了,借助Android给我们提供的`Binder`机制,我们成功解决了文章开头提出的问题。但我们可以做得更好一点。比如,我们可以将下面这段代码封装一下。
     ```java
     android.os.Parcel data = android.os.Parcel.obtain();
    android.os.Parcel reply = android.os.Parcel.obtain();
     int _result;
     data.writeInterfaceToken("PLUS TWO INT"); 
      data.writeInt(a); 
     data.writeInt(b); 
    binderproxy.transact(1, data, reply, 0);//为简单起见,最后一个0暂时不管它 
     reply.readException(); 
    _result = reply.readInt();
    

    具体封装方法是建一个PlusProxy类,如下:

    public class PlusProxy implements IPlus {
              private IBinder  binderproxy ;
              public PlusProxy(IBinder binderproxy){
                    this.binderproxy = binderproxy ;
              }
              public int add (int a ,int b ){
                   android.os.Parcel data = android.os.Parcel.obtain();
                    android.os.Parcel reply = android.os.Parcel.obtain();
                  data.writeInterfaceToken("PLUS TWO INT"); 
                  data.writeInt(a); 
                 data.writeInt(b); 
                  binderproxy.transact(1, data, reply, 0);
                   int _result;
                  reply.readException(); 
                  _result = reply.readInt();
                  return _result ;
              }
    }
                
    

    有了PlusProxy类,进程A就可以利用收到的binderproxy对象创建一个PlusProxy对象plusproxy,然后就可以将它当作plus使用了。我们可以非常愉悦地调用plusproxy.add(2,3)得到2+3的和。
    还能做得更好一点吗?答案是yes。
    我们可以在Stub类中增加一个静态辅助方法public static IPlus asInterface(Ibinder),让进程A收到binderproxy对象时调用Stub.asInterface(binderproxy);该方法负责利用binderproxy对象构造一个PlusProxy对象,然后作为IPlus返回给我们。

    总结:进程A向进程B申请Stub类(继承自Binder)的对象binder,想从binder中获得IPlus接口类型的对象plus,以便利用plus中的add方法做加法计算。当进程A发现收到的不是binder真身而是它的代理binderproxy时,它在自己进程内构建了一个plus的代理对象plusproxy(类型为PlusProxy,实现了与plus相同的的IPlus接口),该代理对象的add方法利用binderproxy去向binder申请加法计算,并把结果返回。这样,从外表上来看,进程A获得了进程B中的plus对象,这就是Binder跨进程通信的本质。
    如果你理解了本文的思想,再去看aidl生成的代码,就一定会觉得很简单的。

    如果通过本文,让你对Binder有了更深的认识,消解了你心头萦绕许久的困惑。点赞吧!

    相关文章

      网友评论

      • 望臻:写得不错:+1:
      • 乘化游:请问,在同一进程下,binder机制onServiceConnected使用的是代理对象还是直接使用binder,谢谢
        cengdong:@乘化游 真实对象,不是代理对象
      • 88b85c173c5b:看完对于Binder有了一个大致的了解了,非常棒!希望后续能有关于IBinder接口为何具有跨进程能力的分析(自己没看懂QAQ)~
      • 076cf7b6c9f9:从另外一种途径理解了binder
      • b3fee0a6a5e8:写的很好,好的文章就是在合适的时间看到收获最大,看binder有一段时间了,对于大量的源码现在还没有能力看下去,楼主这写的简单但又不失功能,很好!收藏一下!
      • 愤怒的小鸡丁:作者你好,有没有联系方式发出来,大家讨论一下为什么这个AIDL生成的asInterface,转换对象要queryLocalInterface(),Stub()对象要attachInterface??我个人感觉这个步骤很多余,不知道google为什么这样设计,因为在asInterface静态方法下,传递的obj,如果是同一个进程,就是Binder本身,既然这样直接return this就是了,为啥要去查一下queryLocalInterface,同一个进程返回的还是自己啊。
        愤怒的小鸡丁:因为我学它写这个asInterface()方法的时候,我可以直接判断if(object instantof Binder)
        {
        //同一个进程
        }else
        {
        //不是同一个进程
        }

        所以感觉这样写很奇怪,把自己存到自己里面去,这样子查的时候必然会查到自己啊,我直接判断一下类型不就可以了么
        工程师milter:@愤怒的小鸡丁 这是我的理解
        工程师milter:@愤怒的小鸡丁 查一下就是为了判断是不是同一个进程
      • 苏易川:不错,的确是top2down,很容易理解
      • aaed935de08a:没有深入到本地层和内核 :pensive:
        工程师milter: @他叫萝卜糕 欢迎你写一篇
      • 5142e554e23e:结合着代码看了几遍,逐渐对跨进程流程有种豁然开朗的赶脚~不过系统远端binderproxy对象调用transact方法后,最终是如何调用到Stub对象的onTransact方法,依旧不理解。感谢楼主!
        morn___:@milter 不是吧 Binder中transact方法 有调onTransact吧
        工程师milter: @行人不在 这是Android系统帮我们实现的。
      • cef091f9ff1c:看了楼主很多文章了,为楼主点赞
        工程师milter: @KoalaHui 谢谢,如果觉得好,就分享吧
      • 工程师milter:因为我是 "坐着"写的,哈哈!
      • 十月1024:坐着写的不错,竟然没晕!
      • 234f933911dc:楼主是个好人~~~不像其他的故意把我绕晕!然后开始卖关子
        工程师milter:@玛雅string 觉得好就分享给其他朋友哈,让更多人受益。我就是看不惯故作高深,才写了这篇文章的。😊
      • b09dbea7de6a:另外一提,这篇文章让我懂了Binder机制。写的很好
        工程师milter:@b09dbea7de6a 觉得好就分享给自己的朋友,让大家都受益哈
      • b09dbea7de6a:我想问下,在第三步中,进程A通过Binder代理对象封装请求信息的时候,interfaceToken和transact()中第一个参数,我理解interfaceToken会作为key查找IIterface对象,而transact()第一个参数是随意的,是进程B对任务的一个大体分类,并且第一个参数也不是必须的?
        工程师milter:@b09dbea7de6a 你可以认为transact第一个参数是A和B之间关于做什么操作的一个约定,这是必须的,而且B 的 onTransact()
        {
        switch (code) {
        }
        }
        中正是依靠第一个参数去寻找对应的case 。比方说,A 在transact第一个参数中发送了1,那么B就知道A在请求加法操作,但B还是不放心,还要再确认一下是不是plus中的加法操作,这就是enforeIInterface方法的作用。
      • 朔野:标题党
      • 沁浒:建议把源码放到github上
      • 162abef5ea09:楼主再讲解下aidl和binder就完美了
        工程师milter:@小米能怎样 觉得好要点赞哈!我其实aidl无非就是简化你完成文中所提的五个步骤而已。
      • RE_my_world:不错 :+1:
        工程师milter:@向往着非日常 谢谢,觉得好就分享给朋友们,让更多的人受益咯^o^
      • 捡淑:可能是讲解****最好的一篇文章 系列
        捡淑:@milter :grin:
        工程师milter:@捡淑 彪悍的头像😜
      • BetterCoder:赞:+1:
        工程师milter:@BetterCoder 😜
      • 7133cd0cb3bf:写到不错,通俗易懂。谢谢!
        工程师milter: @笑中带泪 感谢帮我消灭零回复,哦耶!

      本文标题:可能是讲解Binder机制最好的文章

      本文链接:https://www.haomeiwen.com/subject/fplsjttx.html