Android:学习AIDL,这一篇文章就够了(下)

前言

上一篇博文介绍了关于AIDL是什么,为何咱们须要AIDL,AIDL的语法以及如何使用AIDL等方面的知识,这一篇博文将顺着上一篇的思路往下走,接着介绍关于AIDL的一些更加深刻的知识。强烈建议你们在看这篇博文以前先看一下上一篇博文:Android:学习AIDL,这一篇文章就够了(上)java

注:文中全部代码均源自上一篇博文中的例子。 
另:在看这篇博文以前,建议先将上一篇博文中的代码下载下来或者敲一遍,而后肯定能够正常运行后再接着看。由于文中有大量对于具体代码的分析以及相关代码片断之间的跳转,若是你手头没有一份完整代码的话很容易看得一头雾水,最后浪费了你的时间也浪费了这篇博文。android

正文

1,源码分析:AIDL文件是怎么工做的?

进行到上一篇文章的最后一步,咱们已经学会了AIDL的所有用法,接下来让咱们透过现象看本质,研究一下究竟AIDL是如何帮助咱们进行跨进程通讯的。git

咱们在上一篇提到过,在写完AIDL文件后,编译器会帮咱们自动生成一个同名的 .Java 文件——也许你们已经发现了,在咱们实际编写客户端和服务端代码的过程当中,真正协助咱们工做的实际上是这个文件,而 .aidl 文件从头至尾都没有出现过。这样一来咱们就很容易产生一个疑问:难道咱们写AIDL文件的目的其实就是为了生成这个文件么?答案是确定的。事实上,就算咱们不写AIDL文件,直接按照它生成的 .java 文件那样写一个 .java 文件出来,在服务端和客户端中也能够照常使用这个 .java 类来进行跨进程通讯。因此说AIDL语言只是在简化咱们写这个 .java 文件的工做而已,而要研究AIDL是如何帮助咱们进行跨进程通讯的,其实就是研究这个生成的 .java 文件是如何工做的。github

1.1,这个文件在哪儿?

要研究它,首先咱们就须要找到它,那么它在哪儿呢?在这里:app

它在这儿

它的完整路径是:app->build->generated->source->aidl->debug->com->lypeer->ipcclient->BookManager.java(其中com.lypeer.ipcclient 是包名,相对应的AIDL文件为 BookManager.aidl )。在Android Studio里面目录组织方式由默认的 Android 改成 Project 就能够直接按照文件夹结构访问到它。框架

1.2,从应用看原理

和我一向的分析方式同样,咱们先不去看那些冗杂的源码,先从它在实际中的应用着手,辅以思考分析,试图寻找突破点。首先从服务端开始,刨去其余与此无关的东西,从宏观上咱们看看它干了些啥:ide

private final BookManager.Stub mBookManager = new BookManager.Stub() {
    @Override
    public List<Book> getBooks() throws RemoteException {
        // getBooks()方法的具体实现
    }

    @Override
    public void addBook(Book book) throws RemoteException {
         // addBook()方法的具体实现
    }
};

public IBinder onBind(Intent intent) {
    return mBookManager;
}

能够看到首先咱们是对 BookManager.Stub 里面的抽象方法进行了重写——实际上,这些抽象方法正是咱们在 AIDL 文件里面定义的那些。也就是说,咱们在这里为咱们以前定义的方法提供了具体实现。接着,在 onBind() 方法里咱们将这个 BookManager.Stub 做为返回值传了过去。源码分析

接着看看客户端:性能

private BookManager mBookManager = null;

private ServiceConnection mServiceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) 
        mBookManager = BookManager.Stub.asInterface(service);
        //省略
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
       //省略
    }
};

public void addBook(View view) {
   //省略
   mBookManager.addBook(book);
}

简单的来讲,客户端就作了这些事:获取 BookManager 对象,而后调用它里面的方法。学习

如今结合服务端与客户端作的事情,好好思考一下,咱们会发现这样一个怪事情:它们配合的如此紧密,以致于它们之间的交互竟像是同一个进程中的两个类那么天然!你们能够回想下平时项目里的接口回调,基本流程与此通常无二。明明是在两个线程里面,数据不能直接互通,何以他们能交流的如此愉快呢?答案在 BookManager.java 里。

1.3,从客户端开始

一点开 BookManager.java ,我发现的第一件事是:BookManager 是一个接口类!一看到它是个接口,我就知道,突破口有了。为何呢?接口意味着什么?方法都没有具体实现。可是明明在客户端里面咱们调用了 mBookManager.addBook() !那么就说明咱们在客户端里面用到的 BookManager 毫不仅仅是 BookManager,而是它的一个实现类!那么咱们就能够从这个实现类入手,看看在咱们的客户端调用 addBook() 方法的时候,究竟 BookManager 在背后帮咱们完成了哪些操做。首先看下客户端的 BookManager 对象是怎么来的:

public void onServiceConnected(ComponentName name, IBinder service) 
    mBookManager = BookManager.Stub.asInterface(service);
}

在这里我首先注意到的是方法的传参:IBinder service 。这是个什么东西呢?经过调试,咱们能够发现,这是个 BinderProxy 对象。但随后咱们会惊讶的发现:Java中并无这个类!彷佛研究就此陷入了僵局——其实否则。在这里咱们没办法进一步的探究下去,那咱们就先把这个问题存疑,从后面它的一些应用来推测关于它的更多的东西。

接下来顺藤摸瓜去看下这个 BookManager.Stub.asInterface() 是怎么回事:

public static com.lypeer.ipcclient.BookManager asInterface(android.os.IBinder obj) {
    //验空
    if ((obj == null)) {
        return null;
    }
    //DESCRIPTOR = "com.lypeer.ipcclient.BookManager",搜索本地是否已經
    //有可用的对象了,若是有就将其返回
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (((iin != null) && (iin instanceof com.lypeer.ipcclient.BookManager))) {
        return ((com.lypeer.ipcclient.BookManager) iin);
    }
    //若是本地没有的话就新建一个返回
    return new com.lypeer.ipcclient.BookManager.Stub.Proxy(obj);
}

方法里首先进行了验空,这个很正常。第二步操做是调用了 queryLocalInterface() 方法,这个方法是 IBinder 接口里面的一个方法,而这里传进来的 IBinder 对象就是上文咱们提到过的那个 service 对象。因为对 service 对象咱们尚未一个很清晰的认识,这里也无法深究这个queryLocalInterface() 方法:它是 IBinder 接口里面的一个方法,那么显然,具体实现是在 service 的里面的,咱们无从窥探。可是望文生义咱们也能体会到它的做用,这里就姑且这么理解吧。第三步是建立了一个对象返回——很显然,这就是咱们的目标,那个实现了 BookManager 接口的实现类。果断去看这个 BookManager.Stub.Proxy 类:

private static class Proxy implements com.lypeer.ipcclient.BookManager {
    private android.os.IBinder mRemote;

    Proxy(android.os.IBinder remote) {
        //此处的 remote 正是前面咱们提到的 IBinder service
        mRemote = remote;
    }

    @Override
    public java.util.List<com.lypeer.ipcclient.Book> getBooks() throws android.os.RemoteException {
        //省略
    }

    @Override
    public void addBook(com.lypeer.ipcclient.Book book) throws android.os.RemoteException {
        //省略
    }
    //省略部分方法
}

看到这里,咱们几乎能够肯定:Proxy 类确实是咱们的目标,客户端最终经过这个类与服务端进行通讯。

那么接下来看看 getBooks() 方法里面具体作了什么:

@Override
public java.util.List<com.lypeer.ipcclient.Book> getBooks() throws android.os.RemoteException {
    //很容易能够分析出来,_data用来存储流向服务端的数据流,
    //_reply用来存储服务端流回客户端的数据流
    android.os.Parcel _data = android.os.Parcel.obtain();
    android.os.Parcel _reply = android.os.Parcel.obtain();
    java.util.List<com.lypeer.ipcclient.Book> _result;
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        //调用 transact() 方法将方法id和两个 Parcel 容器传过去
        mRemote.transact(Stub.TRANSACTION_getBooks, _data, _reply, 0);
        _reply.readException();
        //从_reply中取出服务端执行方法的结果
        _result = _reply.createTypedArrayList(com.lypeer.ipcclient.Book.CREATOR);
    } finally {
        _reply.recycle();
        _data.recycle();
    }
    //将结果返回
    return _result;
}

在这段代码里有几个须要说明的地方,否则容易看得云里雾里的:

  • 关于 _data 与 _reply 对象:通常来讲,咱们会将方法的传参的数据存入_data 中,而将方法的返回值的数据存入 _reply 中——在没涉及定向 tag 的状况下。若是涉及了定向 tag ,状况将会变得稍微复杂些,具体是怎么回事请参见这篇博文:你真的理解AIDL中的in,out,inout么?
  • 关于 Parcel :简单的来讲,Parcel 是一个用来存放和读取数据的容器。咱们能够用它来进行客户端和服务端之间的数据传输,固然,它能传输的只能是可序列化的数据。具体 Parcel 的使用方法和相关原理能够参见这篇文章:Android中Parcel的分析以及使用
  • 关于 transact() 方法:这是客户端和服务端通讯的核心方法。调用这个方法以后,客户端将会挂起当前线程,等候服务端执行完相关任务后通知并接收返回的 _reply 数据流。关于这个方法的传参,这里有两点须要说明的地方: 
    • 方法 ID :transact() 方法的第一个参数是一个方法 ID ,这个是客户端与服务端约定好的给方法的编码,彼此一一对应。在AIDL文件转化为 .java 文件的时候,系统将会自动给AIDL文件里面的每个方法自动分配一个方法 ID。
    • 第四个参数:transact() 方法的第四个参数是一个 int 值,它的做用是设置进行 IPC 的模式,为 0 表示数据能够双向流通,即 _reply 流能够正常的携带数据回来,若是为 1 的话那么数据将只能单向流通,从服务端回来的 _reply 流将不携带任何数据。 
      注:AIDL生成的 .java 文件的这个参数均为 0。

上面的这些若是要去一步步探究出结果的话也不是不能够,可是那将会涉及到 Binder 机制里比较底层的东西,一点点说完势必会将文章的重心带偏,那样就很差了——因此我就直接以上帝视角把结论给出来了。

另外的那个 addBook() 方法我就不去分析了,异曲同工,只是因为它涉及到了定向 tag ,因此有那么一点点的不同,有兴趣的读者能够本身去试着阅读一下。接下来我总结一下在 Proxy 类的方法里面通常的工做流程:

  • 1,生成 _data 和 _reply 数据流,并向 _data 中存入客户端的数据。
  • 2,经过 transact() 方法将它们传递给服务端,并请求服务端调用指定方法。
  • 3,接收 _reply 数据流,并从中取出服务端传回来的数据。

纵观客户端的全部行为,咱们不难发现,其实一开始咱们不能理解的那个 IBinder service 偏偏是客户端与服务端通讯的灵魂人物——正是经过用它调用的 transact() 方法,咱们得以将客户端的数据和请求发送到服务端去。从这个角度来看,这个 service 就像是服务端在客户端的代理同样——你想要找服务端?要传数据过去?行啊!你来找我,我给你把数据送过去——而 BookManager.java 中的那个 Proxy 类,就只能沦为二级代理了,咱们在外部经过它来调动 service 对象。

至此,客户端在 IPC 中进行的工做已经分析完了,接下来咱们看一下服务端。

1.4,接着看服务端

前面说了客户端经过调用 transact() 方法将数据和请求发送过去,那么理所固然的,服务端应当有一个方法来接收这些传过来的东西:在 BookManager.java 里面咱们能够很轻易的找到一个叫作 onTransact() 的方法——看这名字就知道,多半和它脱不了关系,再一看它的传参(int code, android.os.Parcel data, android.os.Parcel reply, int flags) ——和 transact() 方法的传参是同样的!若是说他们没有什么 py 交易把我眼珠子挖出来当泡踩!下面来看看它是怎么作的:

@Override
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 TRANSACTION_getBooks: {
            //省略
            return true;
        }
        case TRANSACTION_addBook: {
            //省略
            return true;
        }
    }
    return super.onTransact(code, data, reply, flags);
}

能够看到,它在接收了客户端的 transact() 方法传过来的参数后,什么废话都没说就直接进入了一个 switch 选择:根据传进来的方法 ID 不一样执行不一样的操做。接下来看一下每一个方法里面它具体作了些什么,以 getBooks() 方法为例:

case TRANSACTION_getBooks: {
    data.enforceInterface(DESCRIPTOR);
    //调用 this.getBooks() 方法,在这里开始执行具体的事务逻辑
    //result 列表为调用 getBooks() 方法的返回值
    java.util.List<com.lypeer.ipcclient.Book> _result = this.getBooks();
    reply.writeNoException();
    //将方法执行的结果写入 reply ,
    reply.writeTypedList(_result);
    return true;
}

很是的简单直了,直接调用服务端这边的具体方法实现,而后获取返回值并将其写入 reply 流——固然,这是因为这个方法没有传入参数而且不涉及定向 tag 的关系,否则还会涉及到将传入参数从 data 中读取出来,以及针对定向 tag 的操做,具体的能够参考这篇博文:你真的理解AIDL中的in,out,inout么?

另外,还有一个问题,有些读者可能会疑惑,为何这里没有看到关于将 reply 回传到客户端的相关代码?事实上,在客户端咱们也没有看到它将相关参数传向服务端的相关代码——它只是把这些参数都传入了一个方法,其中过程一样是对咱们隐藏的——服务端也一样,在执行完 return true 以后系统将会把 reply 流传回客户端,具体是怎么作的就不足为外人道也了。不知道你们发现了没有,经过隐藏了这些细节,咱们在 transact() 与 onTransact() 之间的调用以及数据传送看起来就像是发生在同一个进程甚至同一个类里面同样。咱们的操做就像是在一条直线上面走,根本感觉不出来其中原来有过曲折——也许这套机制在设计之初,就是为了达到这样的目的。

分析到这里,服务端的工做咱们也分析的差很少了,下面咱们总结一下服务端的通常工做流程:

  • 1,获取客户端传过来的数据,根据方法 ID 执行相应操做。
  • 2,将传过来的数据取出来,调用本地写好的对应方法。
  • 3,将须要回传的数据写入 reply 流,传回客户端。

1.5,总结

如今咱们已经完成了 BookManager.java 几乎全部的分析工做,接下来我想用两张图片来作一个总结。第一张是它的 UML 结构图:

AIDL的结构

第二张是客户端与服务端使用其进行 IPC 的工做流程:

AIDL的工做流程

剩下的就你们本身体味一下吧——若是前面的东西你看懂了,这里有没有我说的几句总结都差很少;若是前面你看的似懂非懂,看看这两张图片也就懂了;若是前面你几乎没有看懂,那么我写几句总结你仍是看不懂。。。

2,为何要这样设计?

这个问题能够拆分红两个子问题:

  • 为何AIDL的语法要这样设计?
  • 为何它生成的 .java 文件的结构要这样设计?

首先我有一个总的观点:在程序设计领域,任何的解决方案,无非是基于需求和性能两方面的考虑。首先是保证把需求完成,在这个大前提下保证性能最佳——这里的性能,就包括了代码的健壮性,可维护性等等林林总总的东西。

关于AIDL的语法为何要这么设计,其实没有太大的研究的必要——由于他的语法实际上和 Java 没有多大区别,区别的地方也很容易想通,可能是由于一些很显然的缘由而不得不那样作。接下来我主要分析一下 BookManager.java 的设计之道。首先咱们要明确需求:

  • 基本需求固然是实现 IPC 。
  • 在此基础上要尽量的对开发者友好,即便用方便,且最好让开发者有那种在同一个进程中调用方法传输数据的爽感。

既然要实现 IPC ,一些核心的要素就不能少,好比客户端接收到的 IBinder service ,好比 transact() 方法,好比 onTransact() 方法——可是能让开发者察觉到这些这些东西的存在甚至本身写这些东西么?不能。为何?由于这些东西作的事情其实很是的单调,无非就是那么几步,可是恰恰又涉及到不少对数据的写入读出的操做——涉及到数据流的东西通常都很繁琐。把这些东西暴露出去显然是不合适的,仍是创建一套模板把它封装起来比较的好。可是归根结底,咱们实现 IPC 是须要用到它们的,因此咱们须要有一种途径去访问它们——在这个时候,代理-桩的设计理念就初步成型了。为了达到咱们的目的,咱们能够在客户端创建一个服务端的代理,在服务端创建一个客户端的桩,这样一来,客户端有什么需求能够直接跟代理说,代理跟它说你等等,我立刻给你处理,而后它就告诉桩,客户端有这个需求了,桩就立刻让服务端开始执行相应的事件,在执行结束后再经过桩把结果告诉代理,代理最后把结果给客户端。这样一来,客户端觉得代理就是服务端,而且事实上它也只与代理进行了交互,而客户端与代理是在同一个进程中的,在服务端那边亦然——经过这种方式,咱们就可让客户端与服务端的通讯看上去简单无比,像是从头至尾咱们都在一个进程中工做同样。

在上面的设计思想指导之下,BookManager.java 为何是咱们看到的这个样子就很清楚明白了。

3,有没有更好的方式来完成 IPC ?

首先我要阐述的观点是:若是你对这篇文章中上面叙述的那些内容有必定的掌握与理解了的话,彻底脱离AIDL来手动书写客户端与服务端的相关文件来进行 IPC 是绝对没有问题的。而且在了解了 IPC 得以进行的根本以后,你甚至彻底没有必要照着 BookManager.java 来写,只要那几个点在,你想怎么写就怎么写。

可是要说明的是,相较于使用AIDL来进行IPC,手动实现基本上是没有什么优点的。毕竟AIDL是一门用来简化咱们的工做的语言,用它确实能够省不少事。

那么如今除了AIDL与本身手动写,有没有其余的方式来进行 IPC 呢?答案是:有的。前段时间饿了么(这不算打广告吧。。。毕竟没有利益相关,只是纯粹的讨论技术)的一个工程师开源了一套 IPC 的框架,地址在这里:Hermes。这套框架的核心仍是 IBinder service , transact() ,onTransact() 那些东西(事实上,任何和IPC有关的操做最终都仍是要落在这些东西上面),可是他采起了一种巧妙的方式来实现:在服务端开启了一条默认进程,让这条进程来负责全部针对服务端的请求,同时采用注解的方式来注册类和方法,使得客户端能用这种形式和服务端创建约定,而且,这个框架对绑定service的那些细节隐藏的比较好,咱们甚至都不须要在服务端写service,在客户端调用 bindService了——三管齐下,使得咱们能够远离之前那些烦人的有关service的操做了。可是也并非说这套框架就彻底超越了AIDL,在某些方面它也有一些不足。好比,不知道是他的那个 Readme 写的太晦涩了仍是怎么回事,我以为使用它须要付出的学习成本仍是比较大的;另外,在这套框架里面是将全部传向服务端的数据都放在一个 Mail 类里面的,而这个类的传输方式至关于AIDL里面定向 tag 为 in 的状况——也就是说,不要再想像AIDL里面那样客户端数据还能在服务端完成操做以后同步变化了。更多的东西我也还没看出来,还没用过这个框架,只是简单的看了下它的源码,不过总的来讲能过看出来的是做者写的很用心,做者自己的Android功底也很强大,至少不知道比我强大到哪里去了……另外,想微微的吐槽一下,为何这个框架用来进行IPC的核心类 IHermesService 里面长得和AIDL生成的 .java 如出一辙啊如出一辙……

总之,我想说的就是,虽然已经有AIDL了,可是并不意味着就不会出现比它更好的实现了——不止在这里是这样,这个观点能够推广到全部领域。

结语

这篇文章说是学习AIDL的,其实大部分的内容都是在经过AIDL生成的那个.java 文件讲 IPC 相关的知识——其实也就是 Binder 机制的利用的一部分——这也是为何文中其实有不少地方没有深刻下去讲,而是匆匆忙忙的给出告终论,由于再往下就不是应用层的东西了,讲起来比较麻烦,并且容易把人看烦。

讲到这里,基本上关于Android里面 IPC 相关的东西都已经讲得差很少了,若是你是从我写的 Android中的Service:默默的奉献者 (1) –> Android中的Service:Binder,Messenger,AIDL(2) –> Android:学习AIDL,这一篇文章就够了(上) –> 如今这篇,这样一路看下来,而且是认真的看下来的话,基本上这一块的问题都难不倒你了。

另外,除了知识,我更但愿经过个人博文传递的是一些解决问题分析问题的思路或者说是方法,因此个人不少博文都重在叙述思考过程而不是阐述结果——这样有好处也有坏处,好处是若是看懂了,可以收获更多,坏处是,大部分人都没有那个耐性慢慢的来看懂它,毕竟这须要思考,而当前不少的人都已经没有思考的时间,甚至丧失思考的能力了。

谢谢你们。

另:关于脱离AIDL本身写IPC的代码,我本身写了一份,你们能够聊做参考,传送门

相关文章
相关标签/搜索