谈谈对AIDL的理解

作者: dzy是个坏人 | 来源:发表于2016-09-18 20:09 被阅读569次

    最近一些在准备各种校招面试,在安卓面试的时候,总会被问到AIDL,跨进程通信的知识,由于平时基本没有AIDL跨进程通信的需求(一般用广播和contentprovider),所以AIDL这一块一直很不熟悉,于是马上去查阅了相关的资料,引发了一些思考。

    跨进程

    为什么要有跨进程通信这个概念呢?我们学习操作系统时都知道,进程是系统分配资源的基本单位(虽然不太严谨),每个进程都有自己独立的空间地址,这个地址是逻辑地址,同一个进程内的线程共用这些资源,所以我们平时编程的时候用多线程去访问同一个对象,是没有问题的,从java的角度来说就是它们访问到的是同一个堆。但是多进程就不一样了,一个进程里面的对象是不能直接被另一个进程访问到的,多进程应用要正常工作,必需要有一个方式,来实现进程之间的沟通。

    通信与Binder

    什么是通信,就是信息的传递或流动,在计算机的世界里,所有的信息都是二进制流,如果你实现了将一些字节从一个进程发送给另一个进程,那就是实现了跨进程通信,很明显Socket就是一种跨进程通信的方式,只不过Socket从发送到接收,期间经过了多次数据的拷贝,效率低。Linux上还有其他的跨进程通信方式,它们各有优缺点,但谷歌在Android上提出了一个新的跨进程通信的方案Binder。什么是Binder呢?要把这个东西说清楚,涉及到的内容太多太深了,想要深入学习的推荐一个博客Binder系列—开篇,里面有图有文共花了十篇文章从源码的角度分析了一遍,总得来说,它就是一种通过内存拷贝的方式实现了从一个进程向另一个进程发送字节的技术(好象说了等于没说)

    AIDL

    常说AIDL是安卓的一种跨进程通信的方式,其实不太严谨,应该说Binder才是一种跨进程通信的方式,而AIDL是Binder的一种具体应用,有点类似于网络中的传输层和应用层,IBinder有自己的通信协议,负责建立和维护连接,发送数据,而AIDL则定义了更上一层的传输内容,而像ContentProvider、广播同样是利用IBinder进行工作的。

    那么AIDL到底是做什么的呢?

    用一句话来总结就是--接口的跨进程调用。举个栗子,进程A调用 Book getBook(int id)方法,另一个进程B响应这个方法并返回内容Book,A拿到从B过来的这个Book,如果愿意,不只A可以通过getBook获取Book对象,其他进程CDE同样可以调用这个方法来获取来自B的对象,典型的C/S模式。

    实现之前

    先看在这过程中两者之间传递了哪些信息。首先A要让B知道,你调用的是getBook这个方法,而不是getName或者其它方法,其次方法的参数id也要传递给B。而对B来说,要把找到的book对象传递给A。这就引出了一个问题,对象和方法是一个编程的概念,我们只可以传字节,那怎么用二进制数据来表达对象和方法呢?

    1. 二进制与对象
      对象与二进制数据的转换,也就是我们常说的对象的序列化和反序列化,安卓中的具体的方案就是Parcelable接口。Parcelable的具体思想是,它不关心你的对象有哪些成员,而是给你提供了一个Parcel对象,你可以把Parcel对象当作一个数据包一样,当你要序列化的时候,往Parcel里面写对象的成员值,具体写哪些成员、成员的顺序等自己决定,因为到时候从Parcel读数据也是你自己来读,Parcel不关心你的数据结构。而另一端,也就是需要反序列化的一端,需要你根据之前写的顺序去读Parcel里的值并利用这些值去构造你的对象。下面是一个实现了Parcelable的Book类
    public class Book implements Parcelable {
        public int id;
        public String name;
        protected Book(Parcel in)
        {
            //在构造的时候,从Parcel里面读成员值,顺序和写的时候一样
            id = in.readInt();
            name = in.readString();
        }
        public static final Creator<Book> CREATOR = new Creator<Book>() {
            @Override
            public Book createFromParcel(Parcel in)
            {
                return new Book(in);
            }
            @Override
            public Book[] newArray(int size)
            {
                return new Book[size];
            }
        };
        @Override
        public int describeContents()
        {
            return 0;
        }
        @Override
        public void writeToParcel(Parcel dest, int flags)
        {
            //在序列化的时候被调用,往Parcel里面写数据
            dest.writeInt(id);
            dest.writeString(name);
        }
    }
    
    1. 表示方法
      方法的二进制表达就更简单了,方法说白了就是一个符号,那就可以直接用数字0、1、2...去代表每个方法,而数据的含义由我们自己来决定。

    用Binder通信

    IBinder是一个接口(这里指的是java层的IBinder),当我们绑定另一个进程的Service的时候,会在onBind方法里面返回一个IBinder对象,而客户端也会在onServiceConnected拿到一个代表服务的IBinder对象,但这两个对象不是同一个对象来的,毕竟来自两个进程,IBinder有一个核心方法,注意方法的参数

    public boolean transact(int code, Parcel data, Parcel reply, int flags)  
    // code 用来代表方法
    // data 可以装方法的参数
    // reply 可以装方法的返回值 
    

    进程之间就是通过调用IBinder的这个方法来进行通信的,虽然两个IBinder不是同一个对象,但ServiceManger负责将它们关联起来,当我们本地调用transact方法时,远程的IBinder的transact就会被调用,其中间经过了ServiceManger,显然这也是一次跨进程调用,这样一来就好像为了实现跨进程调用,就要先实现跨进程调用,这就要涉及到最初的ServiceManger是怎么跑起来的,等以后把IBinder的native层吃透了再写一篇文章来讲了。
    现在,两个进程可以通过transact方法来通信了,可是怎么调用getBook呢?现在来实现Book getBook(int id)的整个过程:调用Ibinder的transact方法,用一个数字(比如2)代表getBook,即参数code为2,把id写进data里面,服务端IBinder响应transact方法,通过code知道调用的是getBook,从data中取出id并找到对应的Book,然后将Book写进reply里面,客户端再从reply里面读出返回值,再将返回值构造成Book对象。整个getBook过程就完成了。
    可是这样也太麻烦了吧,调用一个接口就这么麻烦,如果接口很多岂不是更麻烦,而且怎么保证通信的规范,比如怎么返回Null值,怎么返回异常?

    AIDL文件定义接口

    刚才说到,调用一个接口太麻烦了,而且每个调用逻辑都是差不多的,那么有没有办法可以少写代码,我只要直接调用getBook()就可以帮我做完所有工作呢?那就是用AIDL文件来定义接口。当我们用AIDL文件来描述我们的接口时,SDK就会自动帮我们生成相应的Java代码,主要内容就是实现我刚才说的繁琐流程。所以说,AIDL文件并不是什么高级的东西,只是用来描述我们的接口,然后sdk会根据AIDL文件来生成Java代码,如果你不用AIDL文件,直接手写代码也是可以的。下面看一个简单的例子

    //book.aidl
    package com.xxx.aidlsample.aidl;
    parcelable Book;
    
    // BookManagerInterface.aidl
    package com.xxx.aidlsample.aidl;
    import com.xxx.aidlsample.aidl.Book;
    interface BookManagerInterface {
       Book getBook(int id);
    }
    

    定义两个aidl文件并编译后,自动生成了一个BookManagerInterface类,这个类代码十分好理解,下面只说关键的几个地方。
    首先,它为getBook方法生成了一个静态常量,也就是TRANSACTION_getBook代表getBook这个方法,用作transact的code参数

    static final int TRANSACTION_getBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    

    生成一个描述字符串,作为IBinder的代表

    private static final java.lang.String DESCRIPTOR = "com.xxx.aidlsample.aidl.BookManagerInterface";
    

    BookManagerInterface有一个sub的内部抽象类(抽象方法为getBook),作为服务端的IBinder实现,我们在service的onBind中返回的就是sub的实现类。看它的transact方法实现

    
            public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
            {
                switch (code) //匹配方法
                {
                    ...
                    case TRANSACTION_getBook: //如果是getBook
                    {
                        data.enforceInterface(DESCRIPTOR);//验证描述符,确保客户端和服务端是对应的
                        int _arg0;
                        _arg0 = data.readInt();//读方法参数,book id
                        com.dzy.aidlsample.aidl.Book _result = this.getBook(_arg0);//调用真正的getBook逻辑,这里的getBook是个抽象方法
                        reply.writeNoException();//表示没有异常
                        if ((_result != null))
                        {
                            reply.writeInt(1);//表示结果不为null
                            //将找到的Book写入reply
                            _result.writeToParcel(reply, android.os.Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
                        } else
                        {
                            reply.writeInt(0);
                        }
                        return true;
                    }
                }
                return super.onTransact(code, data, reply, flags);
            }
    

    再看客户端的代理类,同样是一个内部类,当我们调用代理类的getBook

    public com.dzy.aidlsample.aidl.Book getBook(int id) throws android.os.RemoteException
                {
                    android.os.Parcel _data = android.os.Parcel.obtain();
                    android.os.Parcel _reply = android.os.Parcel.obtain();
                    com.dzy.aidlsample.aidl.Book _result;
                    try
                    {
                        _data.writeInterfaceToken(DESCRIPTOR);//写入描述符
                        _data.writeInt(id); //写入参数 id
                         //调用transact,code就是getBook的指示常量
                        mRemote.transact(Stub.TRANSACTION_getBook, _data, _reply, 0);
                        _reply.readException();
                        if ((0 != _reply.readInt())) //如果返回结果不为null
                        {
                            //通过reply构造返回的对象
                            _result = com.dzy.aidlsample.aidl.Book.CREATOR.createFromParcel(_reply);
                        } else
                        {
                            _result = null;
                        }
                    } finally
                    {
                        _reply.recycle();
                        _data.recycle();
                    }
                    return _result;
                }
    

    可以看到data和reply参数,客户端这边是怎么写入数据的,服务端就怎么读出来,一一对应,就像我们的网络协议一样。

    最后总结一下,AIDL的核心是Binder,我们通过AIDL文件来描述接口,使得到一个封装好的IBinder代理,来实现接口的远程调用。Binder是Android里面一个很重要的概念,是Android各种ManagerService比如AMS、PMS的工作基础,如果要深入理解Android系统,就不得不理解Binder的原理。本文跳过了很多核心的内容,只讲JAVA层的一些原理。

    相关文章

      网友评论

        本文标题:谈谈对AIDL的理解

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