Android学习之AIDL

概述

AIDL是一个缩写,全称是Android Interface Definition Language,也就是Android接口定义语言,设计这门语言的目的是为了实现进程间通信,简称IPC。

语法

1、文件类型:用AIDL书写的文件的后缀是 .aidl,而不是 .java
2、数据类型
Java中的八种基本数据类型 byte,short,int,long,float,double,boolean,char。
String 类型
CharSequence类型
List类型:List中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable,List可以使用泛型
Map类型:Map中的所有元素必须是AIDL支持的类型之一,或者是一个其他AIDL生成的接口,或者是定义的parcelable。Map是不支持泛型的
定向tag:AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。Java 中的基本类型和 String ,CharSequence 的定向 tag 默认且只能是 in 。请不要滥用定向 tag ,而是要根据需要选取合适的,全都一上来就用 inout ,等工程大了系统的开销就会大很多——因为排列整理参数的开销是很昂贵的。
两种AIDL文件:所有的AIDL文件大致可以分为两类。一类是用来定义parcelable对象,以供其他AIDL文件使用AIDL中非默认支持的数据类型的。一类是用来定义方法接口,以供系统使用来完成跨进程通信的。所有的非默认支持数据类型必须通过第一类AIDL文件定义才能被使用。

用法

1、使数据类实现 Parcelable 接口(以Book.java为例)
首先,创建一个类,正常的书写其成员变量,建立getter和setter并添加一个无参构造:

public class Book{
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getBookId() {
        return bookId;
    }


    public void setBookId(int bookId) {
        this.bookId = bookId;
    }

    public int bookId;
    public String bookName;
    public Book() {}

}

然后 implements Parcelable ,接着 as 就会报错,将鼠标移到那里,按下 alt+enter,让它自动解决错误,这个时候它会帮你完成一部分的工作,在弹出来的框里选择所有的成员变量,然后确定。你会发现类里多了一些代码,但是现在还是会报错,Book下面仍然有一条小横线,再次将鼠标移到那里,按下 alt+enter 让它自动解决错误,这次解决完错误之后就不会报错了,这个 Book 类也基本上实现了 Parcelable 接口,可以执行序列化操作了
这里有一个坑:默认生成的模板类的对象只支持为 in 的定向 tag 。为什么呢?因为默认生成的类里面只有 writeToParcel() 方法,而如果要支持为 out 或者 inout 的定向 tag 的话,还需要实现 readFromParcel() 方法——而这个方法其实并没有在 Parcelable 接口里面,所以需要我们从头写
完整的 Book 类的代码:

package com.example.maxiaolong.aidlserver;

import android.os.Parcel;
import android.os.Parcelable;

public class Book implements Parcelable{
    public int bookId;
    public String bookName;

    public Book() {
    }

    public Book(int bookId, String bookName) {
        this.bookId = bookId;
        this.bookName = bookName;
    }

    public int getBookId() {
        return bookId;
    }

    public String getBookName() {
        return bookName;
    }

    public void setBookId(int bookId) {
        this.bookId = bookId;
    }

    public void setBookName(String bookName) {
        this.bookName = bookName;
    }


    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(this.bookId);
        dest.writeString(this.bookName);
    }

    protected Book(Parcel in) {
        this.bookId = in.readInt();
        this.bookName = in.readString();
    }
    public void readFromParcel(Parcel dest){
        this.bookId = dest.readInt();
        this.bookName = dest.readString();
    }
    public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel in) {
            return new Book(in);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
}

2、写AIDL文件
首先我们需要一个 Book.aidl 文件来将 Book 类引入使得其他的 AIDL 文件其中可以使用 Book 对象。那么第一步,如何新建一个 AIDL 文件呢?Android Studio已经帮我们把这个集成进去了,鼠标移到app上面去,点击右键,然后 new->AIDL->AIDL File,按下鼠标左键就会弹出一个框提示生成AIDL文件了
这里写图片描述
生成AIDL文件之后,项目的目录会变成这样的
这里写图片描述
比起以前多了一个叫做 aidl 的包,而且他的层级是和 java 包相同的,并且 aidl 包里默认有着和 java 包里默认的包结构。
Book.aidl文件源码:

package com.example.maxiaolong.aidlserver;

parcelable Book;

IBookManager.aidl文件源码:

package com.example.maxiaolong.aidlserver;
import com.example.maxiaolong.aidlserver.Book;

interface IBookManager {
    void addBook(in Book book);
    List<Book> getBookList();
}

这里又有一个坑!Book.aidl与Book.java的包名应当是一样的。这似乎理所当然的意味着这两个文件应当是在同一个包里面的——事实上,很多比较老的文章里就是这样说的,他们说最好都在 aidl 包里同一个包下,方便移植——然而在 Android Studio 里并不是这样。如果这样做的话,系统根本就找不到 Book.java 文件,从而在其他的AIDL文件里面使用 Book 对象的时候会报 Symbol not found 的错误。为什么会这样呢?因为 Gradle 。大家都知道,Android Studio 是默认使用 Gradle 来构建 Android 项目的,而 Gradle 在构建项目的时候会通过 sourceSets 来配置不同文件的访问路径,从而加快查找速度——问题就出在这里。Gradle 默认是将 java 代码的访问路径设置在 java 包下的,这样一来,如果 java 文件是放在 aidl 包下的话那么理所当然系统是找不到这个 java 文件的。
又要 java文件和 aidl 文件的包名是一样的,又要能找到这个 java 文件——那么仔细想一下的话,其实解决方法是很显而易见的。首先我们可以把问题转化成:如何在保证两个文件包名一样的情况下,让系统能够找到我们的 java 文件?这样一来思路就很明确了:要么让系统来 aidl 包里面来找 java 文件,要么把 java 文件放到系统能找到的地方去,也即放到 java 包里面去。接下来我详细的讲一下这两种方式具体应该怎么做:
第一种方法:修改 build.gradle 文件:在 android{} 中间加上下面的内容:

sourceSets {
    main {
        java.srcDirs = ['src/main/java', 'src/main/aidl']
    }
}

也就是把 java 代码的访问路径设置成了 java 包和 aidl 包,这样一来系统就会到 aidl 包里面去查找 java 文件,也就达到了我们的目的
第二种方法:把 java 文件放到 java 包下去:把 Book.java 放到 java 包里任意一个包下,保持其包名不变,与 Book.aidl 一致。只要它的包名不变,Book.aidl 就能找到 Book.java ,而只要 Book.java 在 java 包下,那么系统也是能找到它的。但是这样做的话也有一个问题,就是在移植相关 .aidl 文件和 .java 文件的时候没那么方便,不能直接把整个 aidl 文件夹拿过去完事儿了,还要单独将 .java 文件放到 java 文件夹里去。
我们可以用上面两个方法之一来解决找不到 .java 文件的坑,具体用哪个就看大家怎么选了。
做到这里执行Make Project 编译一下项目,就算大功告成了。
3、编写服务端代码
上面创建AIDL的过程都是在服务端应用里面进行的,下面来编写服务端代码。
在我们写完AIDL文件并 clean 或者 rebuild 项目之后,编译器会根据AIDL文件为我们生成一个与AIDL文件同名的 .java 文件,这个 .java 文件才是与我们的跨进程通信密切相关的东西。事实上,基本的操作流程就是:在服务端实现AIDL中定义的方法接口的具体逻辑,然后在客户端调用这些方法接口,从而达到跨进程通信的目的。
先创建一个Service,直接上服务端代码:

public class MyService extends Service {
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();
    private final IBookManager.Stub mbinder = new IBookManager.Stub() {
        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this){
                if(!mBookList.contains(book)){
                    mBookList.add(book);
                }
            }
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            synchronized (this){
                return mBookList;
            }
        }
    };
    public MyService() {
    }
    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mbinder;
    }
}

CopyOnWriteArrayList是支持并发访问的线程安全的容器,整体的代码结构很清晰,大致可以分为二块:第一块是重写 IBookManager.Stub 中的方法。在这里面提供AIDL里面定义的方法接口的具体实现逻辑。第二块是重写 onBind() 方法。在里面返回写好的 IBookManager.Stub。
接下来在 Manefest 文件里面注册这个我们写好的 Service :

<service
            android:name=".MyService"
            android:process=":remote"
            android:enabled="true">
            <intent-filter>
                <category android:name="android.intent.category.DEFAULT"/>
                <action android:name="BOOKMANAGER_SERVICE"/>
            </intent-filter>
</service>

到这里我们的服务端代码就编写完毕了。
4、编写客户端代码
我们需要保证,在客户端和服务端中都有我们需要用到的 .aidl 文件和其中涉及到的 .java 文件,因此不管在哪一端写的这些东西,写完之后我们都要把这些文件复制到另一端去。如果是用修改build.gradle文件解决的找不到 .java 文件的问题,那么直接将 aidl 包复制到另一端的 main 目录下就可以了,因为里面包含所有的aidl文件和相关的java文件。如果是使用第二个方法的话,就除了把把整个 aidl 文件夹拿过去,还要单独将 .java 文件放到 java 文件夹里去。
在客户端我们要完成的工作主要是调用服务端的方法,但是在那之前,我们首先要连接上服务端,完整的客户端代码是这样的:

public class MainActivity extends AppCompatActivity {
    private IBookManager mIBookManager;
    private boolean mBound = false;
    private List<Book> mBooks;
    private int bookSize;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button add = (Button)findViewById(R.id.add_book);
        add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!mBound){
                    attemptToBindService();
                    Toast.makeText(getApplicationContext(),"当前与服务未连接,正在尝试重新连接",Toast.LENGTH_SHORT).show();
                    return;
                }
                try{
                    mBooks = mIBookManager.getBookList();
                    bookSize = mBooks.size();
                    mIBookManager.addBook(new Book(bookSize + 1,"书"+ (bookSize + 1)));
                    Toast.makeText(MainActivity.this,mIBookManager.getBookList().size() + "",Toast.LENGTH_SHORT).show();
                }catch(RemoteException e){
                    e.printStackTrace();
                }
            }
        });
    }
    private void attemptToBindService(){
        Intent intentService = new Intent();
        intentService.setAction("BOOKMANAGER_SERVICE");
        intentService.setPackage("com.example.maxiaolong.aidlserver");
        bindService(intentService,connection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if(!mBound){
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mBound){
            unbindService(connection);
            mBound = false;
        }
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            mIBookManager = IBookManager.Stub.asInterface(iBinder);
            if(mIBookManager != null) {
                mBound = true;
                Toast.makeText(getApplicationContext(),"绑定了远程服务",Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
        }
    };

}

同样很清晰,首先建立连接,然后在 ServiceConnection 里面获取 BookManager 对象,接着通过它来调用服务端的方法。

分析

我们已经学会了AIDL的全部用法,接下来让我们透过现象看本质,研究一下究竟AIDL是如何帮助我们进行跨进程通信的。
我们之前提到过,在写完AIDL文件后,编译器会帮我们自动生成一个同名的 .java 文件——也许大家已经发现了,在我们实际编写客户端和服务端代码的过程中,真正协助我们工作的其实是这个文件,而 .aidl 文件从头到尾都没有出现过。这样一来我们就很容易产生一个疑问:难道我们写AIDL文件的目的其实就是为了生成这个文件么?答案是肯定的。事实上,就算我们不写AIDL文件,直接按照它生成的 .java 文件那样写一个 .java 文件出来,在服务端和客户端中也可以照常使用这个 .java 类来进行跨进程通信。所以说AIDL语言只是在简化我们写这个 .java 文件的工作而已,而要研究AIDL是如何帮助我们进行跨进程通信的,其实就是研究这个生成的 .java 文件是如何工作的。
要研究它,首先我们就需要找到它,那么它在哪儿呢:
这里写图片描述
它的完整路径是:app->build->generated->source->aidl->debug->com->example->maxiaolong->aidlserver->IBookManager.java. 底下那个INewBookArrivedListener文件先不管,后面才用到。
我们先不去看那些冗杂的源码,先从它在实际中的应用着手,辅以思考分析,试图寻找突破点。首先从服务端开始,刨去其他与此无关的东西,从宏观上我们看看它干了些啥:

private final IBookManager.Stub mBookManager = new IBookManager.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) 
        mIBookManager = IBookManager.Stub.asInterface(service);
        //省略
    }
    @Override
    public void onServiceDisconnected(ComponentName name) {
       //省略
    }
};

简单的来说,客户端就做了这些事:获取 IBookManager 对象,然后调用它里面的方法。

现在结合服务端与客户端做的事情,好好思考一下,我们会发现这样一个怪事情:它们配合的如此紧密,以至于它们之间的交互竟像是同一个进程中的两个类那么自然!大家可以回想下平时项目里的接口回调,基本流程与此一般无二。明明是在两个线程里面,数据不能直接互通,何以他们能交流的如此愉快呢?答案在 IBookManager.java 里。
一点开 IBookManager.java ,我发现的第一件事是:BookManager 是一个接口类!一看到它是个接口,我就知道,突破口有了。为什么呢?接口意味着什么?方法都没有具体实现。但是明明在客户端里面我们调用了 mIBookManager.addBook() !那么就说明我们在客户端里面用到的 IBookManager 绝不仅仅是 IBookManager,而是它的一个实现类!那么我们就可以从这个实现类入手,看看在我们的客户端调用 addBook() 方法的时候,究竟 IBookManager 在背后帮我们完成了哪些操作。首先看下客户端的 BookManager 对象是怎么来的:

public void onServiceConnected(ComponentName name, IBinder service) 
        mIBookManager = IBookManager.Stub.asInterface(service);
        //省略
    }

在这里我首先注意到的是方法的传参:IBinder service 。这是个什么东西呢?通过调试,我们可以发现,这是个 BinderProxy 对象。但随后我们会惊讶的发现:Java中并没有这个类!接下来顺藤摸瓜去看下这个 BookManager.Stub.asInterface() 是怎么回事:

public static com.example.maxiaolong.aidlserver.IBookManager asInterface(android.os.IBinder obj)
{
if ((obj==null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin!=null)&&(iin instanceof com.example.maxiaolong.aidlserver.IBookManager))) {
return ((com.example.maxiaolong.aidlserver.IBookManager)iin);
}
return new com.example.maxiaolong.aidlserver.IBookManager.Stub.Proxy(obj);
}

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

private static class Proxy implements com.example.maxiaolong.aidlserver.IBookManager {
    private android.os.IBinder mRemote;

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

    @Override
    public void addBook(com.example.maxiaolong.aidlserver.Book book) throws android.os.RemoteException {
        //省略
    }

    @Override
    public java.util.List<com.example.maxiaolong.aidlserver.Book> getBookList() throws android.os.RemoteException {
        //省略
    }
    //省略部分方法
}

看到这里,我们几乎可以确定:Proxy 类确实是我们的目标,客户端最终通过这个类与服务端进行通信
那么接下来看看 getBookList() 方法里面具体做了什么:

public java.util.List<com.example.maxiaolong.aidlserver.Book> getBookList() throws android.os.RemoteException
{
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
java.util.List<com.example.maxiaolong.aidlserver.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.example.maxiaolong.aidlserver.Book.CREATOR);
}
finally {
_reply.recycle();
_data.recycle();
}
return _result;
}

关于 _data 与 _reply 对象:一般来说,我们会将方法的传参的数据存入_data 中,而将方法的返回值的数据存入 _reply 中——在没涉及定向 tag 的情况下。如果涉及了定向 tag ,情况将会变得稍微复杂些,这里不做讨论。
关于 Parcel :简单的来说,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 中进行的工作已经分析完了,接下来我们看一下服务端。

前面说了客户端通过调用 transact() 方法将数据和请求发送过去,那么理所当然的,服务端应当有一个方法来接收这些传过来的东西:在 BookManager.java 里面我们可以很轻易的找到一个叫做 onTransact() 的方法——看这名字就知道,多半和它脱不了关系,再一看它的传参 (int code, android.os.Parcel data, android.os.Parcel reply, int flags) ——和 transact() 方法的传参是一样的!下面来看看它是怎么做的:

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_addBook:
{
data.enforceInterface(DESCRIPTOR);
com.example.maxiaolong.aidlserver.Book _arg0;
if ((0!=data.readInt())) {
_arg0 = com.example.maxiaolong.aidlserver.Book.CREATOR.createFromParcel(data);
}
else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.example.maxiaolong.aidlserver.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}

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

case TRANSACTION_getBookList:
{
data.enforceInterface(DESCRIPTOR);
java.util.List<com.example.maxiaolong.aidlserver.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}

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

分析到这里,服务端的工作我们也分析的差不多了,下面我们总结一下服务端的一般工作流程:

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

手动实现Binder

1、 声明一个AIDL性质的接口,只需要继承IInterface接口即可

public interface IBookManager extends IInterface{
    static final String DESCRIPTOR = "com.example.maxiaolong.ipcserver.IBookManager";

    static final int TRANSACTION_addBook = IBinder.FIRST_CALL_TRANSACTION + 0;
    static final int TRANSACTION_getBookList = IBinder.FIRST_CALL_TRANSACTION + 1;

    public void addBook(Book book) throws RemoteException;
    public List<Book> getBookList() throws RemoteException;
}

接口中声明了一个Binder描述符和两个id,这两个id分别表示addBook 和 getBookList方法,用作transact方法的第一个参数。
2、实现Stub类和Stub类中的Proxy代理类,代码如下:

package com.example.maxiaolong.ipcserver;

import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;

import java.util.ArrayList;
import java.util.List;

public class BookManager extends Binder implements IBookManager {

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    public BookManager()
    {
        this.attachInterface(this, DESCRIPTOR);
    }
    public static IBookManager asInterface(IBinder obj)
    {
        if ((obj==null)) {
            return null;
        }
        IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
        if (((iin!=null)&&(iin instanceof IBookManager))) {
            return ((IBookManager)iin);
        }
        return new BookManager.Proxy(obj);
    }

    @Override
    public IBinder asBinder() {
        return this;
    }

    @Override
    public void addBook(Book book) throws RemoteException {
        synchronized (this){
            if(!mBookList.contains(book)){
                mBookList.add(book);
            }
        }
    }

    @Override
    public List<Book> getBookList() throws RemoteException {
        synchronized (this){
            return mBookList;
        }
    }
    @Override
    public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException
    {
        switch (code)
        {
            case INTERFACE_TRANSACTION:
            {
                reply.writeString(DESCRIPTOR);
                return true;
            }
            case TRANSACTION_addBook:
            {
                data.enforceInterface(DESCRIPTOR);
                Book arg0;
                if ((0!=data.readInt())) {
                    arg0 = Book.CREATOR.createFromParcel(data);
                }
                else {
                    arg0 = null;
                }
                this.addBook(arg0);
                reply.writeNoException();
                return true;
            }
            case TRANSACTION_getBookList:
            {
                data.enforceInterface(DESCRIPTOR);
                List<Book> result = this.getBookList();
                reply.writeNoException();
                reply.writeTypedList(result);
                return true;
            }
        }
        return super.onTransact(code, data, reply, flags);
    }

    private static class Proxy implements IBookManager{
        private IBinder mRemote;
        Proxy(IBinder remote)
        {
            mRemote = remote;
        }
        @Override public IBinder asBinder()
        {
            return mRemote;
        }
        public String getInterfaceDescriptor()
        {
            return DESCRIPTOR;
        }
        @Override
        public void addBook(Book book) throws RemoteException {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            try {
                data.writeInterfaceToken(DESCRIPTOR);
                if ((book!=null)) {
                    data.writeInt(1);
                    book.writeToParcel(data, 0);
                }
                else {
                    data.writeInt(0);
                }
                mRemote.transact(TRANSACTION_addBook, data, reply, 0);
                reply.readException();
            }
            finally {
                reply.recycle();
                data.recycle();
            }
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            List<Book> result;
            try {
                data.writeInterfaceToken(DESCRIPTOR);
                mRemote.transact(TRANSACTION_getBookList, data, reply, 0);
                reply.readException();
                result = reply.createTypedArrayList(Book.CREATOR);
            }
            finally {
                reply.recycle();
                data.recycle();
            }
            return result;
        }
    }
}

可以发现手动修改后的代码和AIDL接口自动生成的代码几乎一模一样,接下在在服务端只需要创建一个BookManager 对象,并在onBind方法中返回即可:

public class MyService extends Service {

    private BookManager mBookManager = new BookManager();

    public MyService() {
    }

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

为IBookManager添加Listener

在有些场景中,调用方如其他应用,需要在进程在运行到某个阶段执行调用方的一些操作,或者远程服务在进行一些异步操作的时候需要将结果回调给调用方,这时候,使用接口回调就能解决,Android中也提供了这样的接口:RemoteCallbackList

这里要多创建一个AIDL文件,用于回调的接口类型,之所以选择AIDL接口而不是普通接口,是因为AIDL中无法使用普通接口。这里我们提供一个AIDL接口,每个用户都需要实现这个接口并向图书馆申请新书提醒功能(registerListener),当然也可以随时取消提醒(unregisterListener)。
1、创建 INewBookArrivedListener.aidl文件:

package com.example.maxiaolong.aidlserver;
import com.example.maxiaolong.aidlserver.Book;
interface INewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

2、在之前的IBookManager.aidl文件中添加这些:

package com.example.maxiaolong.aidlserver;
import com.example.maxiaolong.aidlserver.Book;
import com.example.maxiaolong.aidlserver.INewBookArrivedListener;

interface IBookManager {
    void addBook(in Book book);
    List<Book> getBookList();
    void registerListener(INewBookArrivedListener listener);
    void unregisterListener(INewBookArrivedListener listener);
}

3、在服务端实现新添加的这些方法:

public class MyService extends Service {
    private static final String TAG = "MyService";
    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();
    private RemoteCallbackList<INewBookArrivedListener> mListenerList = new RemoteCallbackList<INewBookArrivedListener>();

    private final IBookManager.Stub mbinder = new IBookManager.Stub() {
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }

        @Override
        public void addBook(Book book) throws RemoteException {
            synchronized (this){
                if(!mBookList.contains(book)){
                    mBookList.add(book);
                }
            }
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            synchronized (this){
                return mBookList;
            }
        }

        @Override
        public void registerListener(INewBookArrivedListener listener) throws RemoteException {
            mListenerList.register(listener);
            final int N = mListenerList.beginBroadcast();
            Log.d(TAG, "registerListener:" + N);
            mListenerList.finishBroadcast();
        }

        @Override
        public void unregisterListener(INewBookArrivedListener listener) throws RemoteException {
            mListenerList.unregister(listener);
            final int N = mListenerList.beginBroadcast();
            Log.d(TAG, "unregisterListener:" + N);
            mListenerList.finishBroadcast();
        }
    };
    public MyService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while(!mIsServiceDestoryed.get()){
                    try{
                        Thread.sleep(5000);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                    int bookId = mBookList.size() + 1;
                    Book newBook = new Book(bookId,"new book#" + bookId);
                    try{
                        onNewBookArrived(newBook);
                    }catch (RemoteException e){
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }

    @Override
    public void onDestroy() {
        mIsServiceDestoryed.set(true);
        super.onDestroy();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return mbinder;
    }

    private void onNewBookArrived(Book book)throws RemoteException{
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        Log.d(TAG, "onNewBookArrived: notify listeners:" + N);
        for(int i = 0;i < N;i++){
            INewBookArrivedListener listener = mListenerList.getBroadcastItem(i);
            if(listener != null){
                try{
                    listener.onNewBookArrived(book);
                }catch (RemoteException e){
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }
}

可以看到在MyService中增加了一个线程,每个5S就调用onNewBookArrived方法,向图书馆中增加一本新书,并调用所有已注册的监听者里面的onNewBookArrived方法。RemoteCallbackList是系统专门提供的用于处理跨进程listener的接口,关于他的介绍可参考《Android开发与艺术探索》,他的用法非常简单,只需调用register和unregister即可完成注册和取消注册,beginBroadcast可以查询容器中已注册的listener数量,注意beginBroadcast和finishBroadcast必须成对出现,即使我们只想查看RemoteCallbackList中的元素个数,也必须配对使用。

4,修改客户端代码注册INewBookArrivedListener接口到远程服务端,并实现onNewBookArrived方法,注意该方法是在客户端的Binder线程池中执行的,如果需要在该方法中操作UI的话就需要有一个Handler去切换到主线程中执行,具体代码如下:

public class MainActivity extends AppCompatActivity {
    private IBookManager mIBookManager;
    private boolean mBound = false;
    private List<Book> mBooks;
    private int bookSize;

    private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;
    private Handler mHandler = new Handler(){
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what){
                case MESSAGE_NEW_BOOK_ARRIVED:
                    Book book = (Book)msg.obj;
                    Toast.makeText(MainActivity.this,"receive new book:" + book.getBookName(),Toast.LENGTH_SHORT).show();
                    break;
                    default:super.handleMessage(msg);
            }
        }
    };

    private INewBookArrivedListener mNewBookArrivedListener = new INewBookArrivedListener.Stub(){
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED,newBook).sendToTarget();
        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button add = (Button)findViewById(R.id.add_book);
        Button regListener = (Button)findViewById(R.id.register_listener);
        Button unregListener = (Button)findViewById(R.id.unregister_listener);

        add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!mBound){
                    attemptToBindService();
                    Toast.makeText(getApplicationContext(),"当前与服务未连接,正在尝试重新连接",Toast.LENGTH_SHORT).show();
                    return;
                }
                try{
                    mBooks = mIBookManager.getBookList();
                    bookSize = mBooks.size();
                    mIBookManager.addBook(new Book(bookSize + 1,"书"+ (bookSize + 1)));
                    Toast.makeText(MainActivity.this,mIBookManager.getBookList().size() + "",Toast.LENGTH_SHORT).show();
                }catch(RemoteException e){
                    e.printStackTrace();
                }
            }
        });
        regListener.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!mBound){
                    attemptToBindService();
                    Toast.makeText(getApplicationContext(),"当前与服务未连接,正在尝试重新连接",Toast.LENGTH_SHORT).show();
                    return;
                }
                try{
                    mIBookManager.registerListener(mNewBookArrivedListener);
                    Toast.makeText(MainActivity.this,"开启通知",Toast.LENGTH_SHORT).show();
                }catch (RemoteException e){
                    e.printStackTrace();
                }
            }
        });
        unregListener.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(!mBound){
                    attemptToBindService();
                    Toast.makeText(getApplicationContext(),"当前与服务未连接,正在尝试重新连接",Toast.LENGTH_SHORT).show();
                    return;
                }
                try{
                    mIBookManager.unregisterListener(mNewBookArrivedListener);
                    Toast.makeText(MainActivity.this,"关闭通知",Toast.LENGTH_SHORT).show();
                }catch (RemoteException e){
                    e.printStackTrace();
                }
            }
        });
    }
    private void attemptToBindService(){
        Intent intentService = new Intent();
        intentService.setAction("BOOKMANAGER_SERVICE");
        intentService.setPackage("com.example.maxiaolong.aidlserver");
        bindService(intentService,connection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onStart() {
        super.onStart();
        if(!mBound){
            attemptToBindService();
        }
    }

    @Override
    protected void onStop() {
        super.onStop();
        if(mBound){
            unbindService(connection);
            mBound = false;
        }
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            mIBookManager = IBookManager.Stub.asInterface(iBinder);
            if(mIBookManager != null) {
                mBound = true;
                Toast.makeText(getApplicationContext(),"绑定了远程服务",Toast.LENGTH_SHORT).show();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {
        }
    };

}

最后还有一点要注意,服务端提供的远程调用方法是运行在服务端的Binder线程池中的,同时客户端在执行的时候会被挂起,如果服务端方法执行比较耗时,就会导致客户端长时间阻塞,如果客户端是UI线程的话,就会导致ANR。因此客户端调用远程方法时要另开一个线程。同理对于服务端回调客户端方法时也要单开线程。

猜你喜欢

转载自blog.csdn.net/u011893710/article/details/82216308