IPC机制之AIDL的使用与原理
AIDL概述
AIDL是一个用于快速创建Binder的工具,没有AIDL文件也可以自己写Binder文件。
Binder的工作机制
各种IPC方式的优缺点和适用场景
需求目标
- 客户端向服务端添加一本书,获取图书列表
- 服务提供注册和解除注册监听新书服务,并且每5秒添加一本书
- 客户端注册监听新书服务,同时将新书打印出来
服务端
- 首先,创建一个Service用来监听客户端的连接请求
- 然后,创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明
- 最后,在Service中实现这个AIDL接口即可
package com.soubu.ipcdemo.aidl;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import com.soubu.ipcdemo.LogUtil;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 作者:余天然 on 2017/4/19 上午10:45
*/
public class BookManagerService extends Service {
private AtomicBoolean isServiceDestoryed = new AtomicBoolean(false);
private CopyOnWriteArrayList<Book> books = new CopyOnWriteArrayList<>();
private RemoteCallbackList<IOnNewBookArrivedListener> listeners = new RemoteCallbackList<>();
private Binder binder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
SystemClock.sleep(5000);
return books;
}
@Override
public void addBook(Book book) throws RemoteException {
books.add(book);
}
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
listeners.register(listener);
LogUtil.print("listener:" + listener);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
listeners.unregister(listener);
LogUtil.print("listener:" + listener);
}
};
@Override
public void onCreate() {
super.onCreate();
books.add(new Book(1, "Androird"));
books.add(new Book(2, "ios"));
new Thread(new ServiceWorker()).start();
}
private class ServiceWorker implements Runnable {
@Override
public void run() {
while (!isServiceDestoryed.get()) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int bookId = books.size() + 1;
Book newBook = new Book(bookId, "newBook#" + bookId);
try {
onNewBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
private void onNewBookArrived(Book newBook) throws RemoteException {
books.add(newBook);
final int N = listeners.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener listener = listeners.getBroadcastItem(i);
if (listener != null) {
listener.onNewBookArriced(newBook);
}
}
listeners.finishBroadcast();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
客户端
- 首先,绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型
- 接着就可以调用AIDL中的方法
package com.soubu.ipcdemo.aidl;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;
import com.soubu.ipcdemo.LogUtil;
import com.soubu.ipcdemo.R;
import java.util.List;
/**
* 作者:余天然 on 2017/4/19 上午10:51
*/
public class BookManagerActivity extends AppCompatActivity {
private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;
private IBookManager bookManager;
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_NEW_BOOK_ARRIVED:
LogUtil.print("received new book :" + msg.obj);
break;
default:
super.handleMessage(msg);
}
}
};
private IOnNewBookArrivedListener onNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArriced(Book newBook) throws RemoteException {
handler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
}
};
private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (bookManager == null) {
return;
}
bookManager.asBinder().unlinkToDeath(deathRecipient, 0);
bookManager = null;
bindBookManagerService();
}
};
private void bindBookManagerService() {
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookManager = IBookManager.Stub.asInterface(service);
try {
service.linkToDeath(deathRecipient, 0);
bookManager.registerListener(onNewBookArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView tv = (TextView) findViewById(R.id.tv_send);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
invocateRemoteMethod();
} catch (RemoteException e) {
e.printStackTrace();
}
}
});
bindBookManagerService();
}
private void invocateRemoteMethod() throws RemoteException {
new Thread(new Runnable() {
@Override
public void run() {
if (bookManager != null) {
List<Book> list = null;
try {
list = bookManager.getBookList();
} catch (RemoteException e) {
e.printStackTrace();
}
LogUtil.print("query book list :" + list.getClass().getCanonicalName());
LogUtil.print("query book list :" + list.toString());
}
}
}).start();
}
@Override
protected void onDestroy() {
if (bookManager != null && bookManager.asBinder().isBinderAlive()) {
try {
bookManager.unregisterListener(onNewBookArrivedListener);
LogUtil.print("unregister listener:" + onNewBookArrivedListener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(connection);
super.onDestroy();
}
}
AIDL使用的注意点
- AIDL支持的数据类型:基本数据类型、String和CharSequence、ArrayList、HashMap、Parcelable以及AIDL;
- 某些类即使和AIDL文件在同一个包中也要显式import进来;
- AIDL中除了基本数据类,其他类型的参数都要标上方向:in、out或者inout;
- AIDL接口中支持方法,不支持声明静态变量;
- 为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中。
- AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以要在AIDL方法中处理线程同步。
- RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。
- 客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中,同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。
- 客户端通过IBinder.DeathRecipient来监听Binder死亡,也可以在onServiceDisconnected中监听并重连服务端。区别在于前者是在binder线程池中,访问UI需要用Handler,后者则是UI线程。
- AIDL可通过自定义权限在onBind或者onTransact中进行权限验证。
测试
我们可以看到Service是在Binder_2线程,每隔5秒,客户端确实会收到服务端的通知。
当我们退出客户端时,确实会解除绑定。
AIDL实际生成的代码
我们创建的AIDL文件如下:
Book.aidl
package com.soubu.ipcdemo.aidl;
parcelable Book;
IBookManager.aidl
package com.soubu.ipcdemo.aidl;
import com.soubu.ipcdemo.aidl.Book;
import com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
IOnNewBookArrivedListener.aidl
package com.soubu.ipcdemo.aidl;
import com.soubu.ipcdemo.aidl.Book;
interface IOnNewBookArrivedListener {
void onNewBookArriced(in Book newBook);
}
然后,AIDL帮我们生成的代码如下:
IBookManager.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.soubu.ipcdemo.aidl;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import com.soubu.ipcdemo.aidl.Book;
import com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener;
import java.util.ArrayList;
import java.util.List;
public interface IBookManager extends IInterface {
List<Book> getBookList() throws RemoteException;
void addBook(Book var1) throws RemoteException;
void registerListener(IOnNewBookArrivedListener var1) throws RemoteException;
void unregisterListener(IOnNewBookArrivedListener var1) throws RemoteException;
public abstract static class Stub extends Binder implements IBookManager {
private static final String DESCRIPTOR = "com.soubu.ipcdemo.aidl.IBookManager";
static final int TRANSACTION_getBookList = 1;
static final int TRANSACTION_addBook = 2;
static final int TRANSACTION_registerListener = 3;
static final int TRANSACTION_unregisterListener = 4;
public Stub() {
this.attachInterface(this, "com.soubu.ipcdemo.aidl.IBookManager");
}
public static IBookManager asInterface(IBinder obj) {
if(obj == null) {
return null;
} else {
IInterface iin = obj.queryLocalInterface("com.soubu.ipcdemo.aidl.IBookManager");
return (IBookManager)(iin != null && iin instanceof IBookManager?(IBookManager)iin:new IBookManager.Stub.Proxy(obj));
}
}
public IBinder asBinder() {
return this;
}
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
IOnNewBookArrivedListener _arg0;
switch(code) {
case 1:
data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
List _arg02 = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_arg02);
return true;
case 2:
data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
Book _arg01;
if(0 != data.readInt()) {
_arg01 = (Book)Book.CREATOR.createFromParcel(data);
} else {
_arg01 = null;
}
this.addBook(_arg01);
reply.writeNoException();
return true;
case 3:
data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
_arg0 = com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
this.registerListener(_arg0);
reply.writeNoException();
return true;
case 4:
data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
_arg0 = com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
this.unregisterListener(_arg0);
reply.writeNoException();
return true;
case 1598968902:
reply.writeString("com.soubu.ipcdemo.aidl.IBookManager");
return true;
default:
return super.onTransact(code, data, reply, flags);
}
}
private static class Proxy implements IBookManager {
private IBinder mRemote;
Proxy(IBinder remote) {
this.mRemote = remote;
}
public IBinder asBinder() {
return this.mRemote;
}
public String getInterfaceDescriptor() {
return "com.soubu.ipcdemo.aidl.IBookManager";
}
public List<Book> getBookList() throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
ArrayList _result;
try {
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
this.mRemote.transact(1, _data, _reply, 0);
_reply.readException();
_result = _reply.createTypedArrayList(Book.CREATOR);
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
public void addBook(Book book) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
if(book != null) {
_data.writeInt(1);
book.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
this.mRemote.transact(2, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
_data.writeStrongBinder(listener != null?listener.asBinder():null);
this.mRemote.transact(3, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
_data.writeStrongBinder(listener != null?listener.asBinder():null);
this.mRemote.transact(4, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
}
}
IOnNewBookArrivedListener.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package com.soubu.ipcdemo.aidl;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import com.soubu.ipcdemo.aidl.Book;
public interface IOnNewBookArrivedListener extends IInterface {
void onNewBookArriced(Book var1) throws RemoteException;
public abstract static class Stub extends Binder implements IOnNewBookArrivedListener {
private static final String DESCRIPTOR = "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener";
static final int TRANSACTION_onNewBookArriced = 1;
public Stub() {
this.attachInterface(this, "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
}
public static IOnNewBookArrivedListener asInterface(IBinder obj) {
if(obj == null) {
return null;
} else {
IInterface iin = obj.queryLocalInterface("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
return (IOnNewBookArrivedListener)(iin != null && iin instanceof IOnNewBookArrivedListener?(IOnNewBookArrivedListener)iin:new IOnNewBookArrivedListener.Stub.Proxy(obj));
}
}
public IBinder asBinder() {
return this;
}
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch(code) {
case 1:
data.enforceInterface("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
Book _arg0;
if(0 != data.readInt()) {
_arg0 = (Book)Book.CREATOR.createFromParcel(data);
} else {
_arg0 = null;
}
this.onNewBookArriced(_arg0);
reply.writeNoException();
return true;
case 1598968902:
reply.writeString("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
return true;
default:
return super.onTransact(code, data, reply, flags);
}
}
private static class Proxy implements IOnNewBookArrivedListener {
private IBinder mRemote;
Proxy(IBinder remote) {
this.mRemote = remote;
}
public IBinder asBinder() {
return this.mRemote;
}
public String getInterfaceDescriptor() {
return "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener";
}
public void onNewBookArriced(Book newBook) throws RemoteException {
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
try {
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
if(newBook != null) {
_data.writeInt(1);
newBook.writeToParcel(_data, 0);
} else {
_data.writeInt(0);
}
this.mRemote.transact(1, _data, _reply, 0);
_reply.readException();
} finally {
_reply.recycle();
_data.recycle();
}
}
}
}
}
如果你觉得自己很牛逼的话,其实也是可以自己手写Binder的。不过要想自己写,还是得先好好地学习下AIDL自动生成代码的具体逻辑。
其实我们查看这个根据AIDL自动生成的代码就会发现,就是在AIDL定义的接口之外,又增加了两个实现接口的类,Proxy/Stub。
这里呢,我们以IOnNewBookArrivedListener生成的代码为例,来分析下这种Proxy/Stub模式。
首先看看Proxy的实现,首先是将远程的Binder作为参数传入进来,再来看看 onNewBookArriced() 这个方法里面的下面几个步骤。首先是将newBook的参数写入到_data中去,同时在 远程binder 调用结束后,得到返回的 _reply ,在没有异常的情况下,返回_reply的result结果。(这里,onNewBookArriced()方法没有返回值,不过,前面的getBookList()是有返回值的,大家可以看看它的实现。)
_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
newBook.writeToParcel(_data, 0);
this.mRemote.transact(1, _data, _reply, 0);
_reply.readException();
这里可以粗略地将 Binder 看做远程服务抛出的句柄,通过这个句柄就可以执行相应的方法了。这里需要额外说明的是,写入和传输的数据都是 Parcelable,Android Framework 中提供的。
再来看看Stub里面的实现,Stub本身继承了Binder抽象类,本身将作为一个句柄,实现在服务端,但传递给客户端使用。同样也看看 onTransact里面的方法,首先是通过 (Book)Book.CREATOR.createFromParcel(data) 读取 _arg0 参数,在this.onNewBookArriced(_arg0)计算过后,将结果写入到reply中。
细心的读者,可以从上面的描述中,发现一些有意思的东西:
Proxy 是写入参数,读取值;
Stub 是读取参数,写入值。
正好是一对,那因此我们是不是可以做出这样的论断呢?Proxy 和 Stub 操作的是一份数据?恭喜你,答对了。
用户空间和内核空间是互相隔离的,客户端和服务端在同一用户空间,而 Binder Driver 在内核空间中,常见的方式是通过 copy_from_user 和 copy_to_user 两个系统调用来完成,但 Android Framework 考虑到这种方式涉及到两次内存拷贝,在嵌入式系统中不是很合适,于是 Binder Framework 通过 ioctl 系统调用,直接在内核态进行了相关的操作,节省了宝贵的空间。
可能大家也注意到 Proxy 这里是private权限的,外部是无法访问的,但这里是 Android 有意为之,抛出了 asInterface 方法,这样避免了对 Proxy可能的修改。
总结一下Proxy/Stub的具体流程,其实,客户端主要就是调用了Proxy,而服务端主要就是实现了Stub:
我们需要注意到如果客户端和服务端在同一个进程下,那么此时就没有跨进程通讯了,asInterface()将返回Stub对象本身,否则才返回Stub.Proxy对象。
上面的分析都是基于代码细节的,可能对Proxy/Stub没有一个整体性的把握的话,很难吃透。这里再举一个生活点的栗子。
自动售货机的故事
夏天一到,天气也变得炎热,地铁旁边的自动售货机开始有更多的人关顾了。那么售货机是怎么工作的了?通过对自动售货机的分析,可以对Binder的Proxy/Stub 模式更好地理解。
和我们打交道得是售货机,而不是背后的零售商,道理很简单,零售商的位置是固定的,也就意味着有很大的交通成本,而和售货机打交道就轻松很多,毕竟售货机就在身边。因而从客户端的角度上看,只需要知道售货机即可。
再从零售商的角度来看,要和为数众多的售货机打交道也是不容易的事情,需要大量的维护和更新成本,如果将其交由另一个中介公司,就能够省心不少。零售商只关心这个中介公司,按时发货,检查营收,这就是所有它应该完成的工作。
如上图所示,在 Binder Framework 中也采用了类似的结构,
Proxy=售货机
Stub=中介公司
在这样的设计下,客户端只需要和 Proxy 打交道,服务端只需要和 Stub 打交道,条理清晰很多。这种模式又被称为 Proxy / Stub 模式,这种模式也值得我们在日后的开发中好好借鉴一下。
参考目录: