一、前言
在上篇博客中,我们详细的介绍了 Binder 的概念以及工作原理,本篇将介绍 Android IPC中最常用的一种方式 - AIDL,通过 AIDL 也可以帮助我们更好的理解 Binder。
关于AIDL,我之前翻看过很多博客,里面先是写一大堆的概念然后堆代码。然而,按照作者的方式,写出来的代码根本运行不成功或者达不到同样的效果,这样就会浪费了读者的时间。因此我决定这篇博客换种方式,先把代码放出来,让大家的运行结果跟我这版一致,然后再来讲解原理。(运行不成功或者运行结果跟我不一样你直接关闭本篇博客)。这里我们选择的在同一个应用(工程)中进行进程间通信,两个工程的情况,除了要复制AIDL接口所相关的包到客户端,其他全都一样。
二、AIDL的使用
首先我们在Android Studio中新建一个项目,建好后的目录结构如下图:
接下来我们按照以下顺序编写代码(注:以下代码中用的是我的包名"com.lerendan.senior",各位复制代码的时候记得修改成自己项目的包名):
步骤1、新建 Book.java
package com.lerendan.senior;
import android.os.Parcel;
import android.os.Parcelable;
public class Book implements Parcelable {
public int bookId;
public String bookName;
public Book(int bookId, String bookName) {
this.bookId = bookId;
this.bookName = bookName;
}
protected Book(Parcel in) {
bookId = in.readInt();
bookName = in.readString();
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(bookName);
}
@Override
public int describeContents() {
return 0;
}
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 String toString() {
return "Book{" +
"bookId=" + bookId +
", bookName='" + bookName + '\'' +
'}';
}
}
步骤2、新建 Book.aidl
选中app目录然后点击右键,New -> AIDL -> AIDL File 如下图:
点击后出现下图:
默认取的名字 "IMyAidlInterface" 现在先不用改,直接点右下角的Finish按钮完成创建,创建好后的目录结构如下图:
接下来将 "IMyAidlInterface.aidl" 改名为 "Book.aidl",并修改其代码为以下所示代码:
package com.lerendan.senior;
parcelable Book;
步骤3、新建 IBookManager.aidl
按上一个步骤中新建AIDL文件的方式再建一个 "IBookManager.aidl",并修改代码为如下所示代码:
package com.lerendan.senior;
import com.lerendan.senior.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
步骤4、Rebuild项目
完成了以上步骤后,Rebuild 一下整个项目(再次提醒,注意检查代码中的包名,否则会Rebuild失败),完成后的目录结构如下:
上图红框中的 "IBookManager.java" 文件就是根据 "IBookManager.aidl" 为我们生成的。这个文件就是核心,后面会详细说明。
步骤5、新建服务端 BookManagerService.java
package com.lerendan.senior;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class BookManagerService extends Service {
public final String TAG = "BMS";
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private Binder mBinder = new IBookManager.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1,"android开发艺术探索"));
mBookList.add(new Book(2,"android内核探究"));
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
步骤6、在 MainManifest.xml 文件中注册 BookManagerService
<service
android:name=".BookManagerService"
android:enabled="true"
android:exported="true"
android:process=":remote" />
步骤7、修改客户端 MainActivity.java
这里我们直接修改MainActivity.java的代码就可以了,不用修改布局文件。
package com.lerendan.senior;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import java.util.List;
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
List<Book> list = bookManager.getBookList();
Log.i(TAG, "query book list, list type:" + list.getClass().getCanonicalName());
Log.i(TAG, "query book list:" + list.toString());
bookManager.addBook(new Book(3,"IOS book"));
List<Book> newList = bookManager.getBookList();
Log.i(TAG, "query book list:" + newList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onDestroy() {
unbindService(mConnection);
super.onDestroy();
}
}
好的,所有代码都写完了,这里我再放一下写完代码后的代码目录结构:
步骤8、运行项目
运行结果如下:
运行到这里说明大功告成了。我们先自己回顾下上面各个步骤的代码,然后再来看下面的解析。
三、AIDL 的解析
3.1 AIDL 接口
上一节步骤1中创建的Book.java是一个表示图书信息的类,它实现了 Parcelabel 接口。不了解序列化的可以看我的另一篇博客 - Android 序列化之 Serializable 和 Parcelable。Book.aidl 是 Book.java 类在 AIDL 中的声明。IBookManager.aidl 是我们定义的一个接口,里面有两个方法 getBookList 和 addBook,其中 getBookList 用于从远程服务端获取图书列表,addBook 用于往图书列表里添加一本书。这里需要注意的是,尽管 Book 类已经和 IBookManager 位于相同的包中,但是在 IBookManager 中仍然要导入 Book 类,这就是 AIDL 的特殊之处。
在 AIDL 中并不是所有的数据类型都可以使用,支持的数据类型如下:
1、Java中的八种基本数据类型,包括 byte,short,int,long,float,double,boolean,char 。
2、String 和 CharSequence 。
3、List。只支持 ArrayList ,且里面每个元素的类型都必须能够被 AIDL 支持。
4、Map。只支持 HashMap,且里面每个 Key 和 Value 的类型都必须能够被 AIDL 支持。
5、Parcelabel。所有实现了 Parcelabel 接口的对象。
6、AIDL。所有的 AIDL 接口本身也可以在 AIDL 文件中使用。
需要注意的是,其中 Parcelabel 和 AIDL 对象都必须显式的添加进来,不管它们是否和当前这个 AIDL 文件位于同一个包内。另一个需要注意的是,如果 AIDL 文件中用到了自定义的 Parcelabel 对象,那么必须新建一个和它同名的 AIDL 文件。比如在上面的 IBookManager.aidl 中用到了 Book.java 这个类,所以必须创建 Book.aidl,Book.aidl 是 Book.java 类在 AIDL 中的声明。
// Book.aidl
package com.lerendan.senior;
parcelable Book;
AIDL中每个实现了 Parcelabel 接口的类都要按如上方式创建相应的 AIDL 文件并声明那个类为 parcelabel。此外,AIDL中除了基本数据类型,其他类型的参数都必须标上方向:
1、in:输入型参数
2、out:输出型参数
3、inout:输入输出型参数
要根据实际需要去指定参数方向,不能一概使用 out 或 inout,因为在底层实现是有开销的。
3.2 根据 AIDL 文件生成的 Java 类(核心)
在上一节步骤4 Rebuild 项目后,可以看到系统根据 IBookManager.aidl 为我们在 gen 目录下生成了一个 IBookManager.java 这个类。为了方便查看做了格式化处理并加入了一些注释,如下:
package com.lerendan.senior;
public interface IBookManager extends android.os.IInterface {
public static abstract class Stub extends android.os.Binder implements com.lerendan.senior.IBookManager {
//Binder的唯一标识,一般用当前Binder的类名表示
private static final java.lang.String DESCRIPTOR = "com.lerendan.senior.IBookManager";
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
//用于将服务端的Binder对象转换成客户端所需的AIDL接口类型的对象
public static com.lerendan.senior.IBookManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof com.lerendan.senior.IBookManager))) {
return ((com.lerendan.senior.IBookManager) iin);
}
return new com.lerendan.senior.IBookManager.Stub.Proxy(obj);
}
//返回当前Binder对象
@Override
public android.os.IBinder asBinder() {
return this;
}
@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_getBookList: {
data.enforceInterface(DESCRIPTOR);
java.util.List<com.lerendan.senior.Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(DESCRIPTOR);
com.lerendan.senior.Book _arg0;
if ((0 != data.readInt())) {
_arg0 = com.lerendan.senior.Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
private static class Proxy implements com.lerendan.senior.IBookManager {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public android.os.IBinder asBinder() {
return mRemote;
}
public java.lang.String getInterfaceDescriptor() {
return DESCRIPTOR;
}
@Override
public java.util.List<com.lerendan.senior.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.lerendan.senior.Book> _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(com.lerendan.senior.Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
@Override
public void addBook(com.lerendan.senior.Book book) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
try {
_data.writeInterfaceToken(DESCRIPTOR);
if ((book != null)) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
//声明了两个整型的 id 分别用于标识这两个方法,这两个 id 用于标识在 transact 过程中客户端所请求的到底是哪个方法
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
}
public java.util.List<com.lerendan.senior.Book> getBookList() throws android.os.RemoteException;
public void addBook(com.lerendan.senior.Book book) throws android.os.RemoteException;
}
IBookManager.java 继承了 IInterface 接口,同时它自己也是一个接口,所有可以在 Binder 中传输的接口都必须继承 IInterface 接口。这个类刚开始看起来逻辑混乱,但是实际上还是很清晰的,结构也很简单,通过它我们可以清楚地了解到Binder的工作机制。首先,它声明了两个方法 getBookList 和 addBook,显然这就是我们在 IBookManager.aidl 中所声明的方法,同时它还声明了两个整型的 id 分别用于标识这两个方法,这两个 id 用于标识在 transact 过程中客户端所请求的到底是哪个方法。接着,它声明了一个内部类 Stub,这个 Stub 就是一个 Binder 类,当客户端和服务端都位于同一个进程时,方法调用不会走跨进程的 transact 过程,而当两者位于不同进程时,方法调用需要走 transact 过程,这个逻辑由 Stub 的内部代理类 Proxy 来完成。这么来看,IBookManager 这个接口的确很简单,但是我们也应该认识到,这个接口的核心实现就是它的内部类 Stub 和 Stub 的内代理类 Proxy,下面详细介绍针对这两个类的每个方法的含义。
asInterface(android.os.IBinder obj)
用于将服务端的 Binder 对象转换成客户端所需的 AIDl 接口类型的对象,这种转换过程是区分进程的,如果客户端和服务端位于统一进程,那么此方法返回的就是服务端的 Stub 对象本身,否则返回的是系统封装后的 Stub.proxy 对象。
asBinder
此方法用于返回当前 Binder 对象。
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags)
这个方法运行在服务器中的 Binder 线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理。服务端通过 code 可以确定客户所请求的目标方法是什么,接着从 data 中取出目标方法所需的参数(如果目标方法有参数的话),然后执行目标方法。当目标方法执行完毕后,就向 reply 中写入返回值(如果目标方法有返回值的话),onTransact 方法的执行过程就是这样的。需要注意的是,如果此方法返回 false ,那么客户端的请求会失败,因此我们可以利用这个特性来做权限验证,毕竟我们也不希望随便一个进程都能远程调用我们的服务。
Proxy#getBookList
这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现是这样的:首先创建该方法所需要的输入型 Parcel 对象 _data、输出型 Parcel 对象 _reply 和返回值队形 List;然后把该方法的参数信息写入 _data 中(如果有参数的话);接着调用 transact 方法来发起 RPC(远程过程调用)请求,同时当前线程挂起;然后服务端的 onTransact 方法会被调用,知道 RPC 过程返回后,当前线程继续执行,并从 _reply 中取出 RPC 过程的返回结果;最后返回 _reply 中的数据。
Proxy#addBook
这个方法运行在客户端,它的执行过程和 getBookList 是一样的,addBook 没有返回值,所以它不需要从 _reply 中取出返回值。
下面给出一个 Binder 的工作机制图:
通过上面的分析和图解,大家应该已经了解了 Binder 的工作机制,但是需要注意的是:首先,当客户端发起远程请求时,由于当前线程会被挂起直至服务端进程返回数据,所以如果一个远程方法是很耗时的,那么不能在 UI 线程中发起此远程请求;其次,由于服务端的 Binder 方法运行在 Binder 的线程池中,所以 Binder 方法不管是否耗时都应该采用同步的方式去实现,因为它已经运行在一个线程中了。
从上述的分析过程来看,我们完全可以不提供 AIDL 文件即可实现 BInder,之所以提供 AIDL 文件,是为了方便系统为我们生成代码。系统根据 AIDL 文件生成 Java 文件的格式是固定的,我们可以抛开 AIDL 文件直接写一个 Binder 出来,二者的工作原理是完全一样的,所以说 AIDL 文件并不是实现 Binder的必需品,AIDL 文件的本质是系统为我们提供的一种快速实现 Binder 的工具。
3.3 远程服务端
上一节步骤五中创建了一个 BookManagerService.java,是一个服务端 Service 的典型实现。首先在 onCreate 中初始化添加了两本图书的信息,然后创建一个 Binder 对象并在 onBind 中返回它,这个对象继承自 IBookManager.Stub 并实现了它内部的 AIDL 方法。然后我们需要在 AndroidManifest.xml注册这个 Service,BookManagerService 是运行在独立进程中的,它和客户端的Activity 不在同一个进程中,这样就构成了进程间通信的场景。
前面我们提到,AIDL 中能够使用的 List 只有 ArrayList,但是这里却使用了 CopyOnWriteArrayList(注意它不是继承自 ArrayList)。这是因为 AIDL 中所支持的是抽象的 List,而 List 是一个接口,因此虽然服务端返回的是 CopyOnWriteArrayList,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 传递给客户端。所以我们在服务端采用 CopyOnWriteArrayList 是完全可以的。这里为什么采用 CopyOnWriteArrayList呢,是因为这个 CopyOnWriteArrayList 支持并发读 / 写。在前面我们也提到, AIDL 方法是在服务端的 Binder 线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以我们要在 AIDL 方法中处理线程同步,而我们这里直接使用 CopyOnWriteArrayList 来进行自动的线程同步。
这里我们介绍一下, Android 中开启多进程模式只有一种方法,那就是给四大组件在 AndroidMenifest.xml 中指定 android:process 属性,除此之外没有其他办法,也就是说我们无法给一个线程或者一个实体类指定其运行时所在的进程。其实还有另一种非常规的多进程方法,那就是通过 JNI 在 native 层去 fork 一个新的进程,但是这种方法属于特殊情况,也不是常用的方式,因此我们暂时不考虑这种方式。下面这个示例描述了如何在 Android 中创建多进程:
<service
android:name=".Service1"
android:process=":remote" />
<service
android:name=".Service2"
android:process="com.lerendan.senior.remote" />
上面这个示例分别为 Service1 和 Service2 指定了 process 属性,并且它们的属性值不同,这意味着当前应用又增加了两个新进程。这两种 process 属性值的写法还是有区别的," : " 的含义是指要在当前的进程名前面附加上当前的包名,这是一种简写的方法,其次,进程名以 " : " 开头的进程属于当前应用的私有进程,其他应用的组件不可以和它跑在同一个进程中,而进程名不以 " : " 开头的进程属于全局进程,其他应用通过 ShareUID 方式可以和它跑在同一个进程中。
3.4 客户端
上一节步骤7中是客户端的实现,首先要绑定远程服务,绑定成功后将服务端返回的 Binder 对象转换成 AIDL 接口,然后就可以通过这个接口去调用服务端的远程方法了。通过 bookManager 去调用 getBookList 方法,然后打印出所获取的图书信息。接着再调用另一个接口 addBook,在客户端给服务端添加一本书,然后再获取一次。
四、难点
到这里相信读者应该对 AIDL 有了一个整体的认识了,但是 AIDL 的复杂性远不止这些,下面继续介绍 AIDL 中常见的一些难点。