Android 关于AIDL你需要知道的一切
好的,由于上周拖更,本次带来Android进程通信详解–AIDL,虽然网上关于这个内容的资料丰富且详细,但一千个读者就有一千个哈姆雷特不是嘛,对于任何东西都要有自己的见解。
Service
关于AIDL,Service就是我们不得不提的东西了,它是Android四大组件之一,太基础的东西这里我们就不再赘述,下面还是给出一些我对于Service的认识:
上图为Service的生命周期
从图里不难看出,Service有两种启动方式:
①用startservice启动服务,第一次会调用oncreate–>onStartCommand,而第二次就会直接到onStartCommand
②用bindservice启动服务,第一次会调用oncreate–>onBind,后面直接到onBind
注意:
如果即startservice又bindservice了,那么单独stopservice和unBindService都没有效果,必须两个也都执行。
Service和Thread的关系
没有必然关系,service和activity一样运行于主线程,只是他没有view不能被我们所看见。
所以如果我们在service里执行长时间耗时操作是会导致ANR的,这里我就不做演示,大家可以自行检测。
然而,当我们给service注册时,加入process标签,加入:remote属性,这时这个service就成为了远程服务,它就和我们的activity处于截然不同的两个进程中,这个时候再执行耗时操作并不会让我们的程序报ANR,应该两个进程的执行并不会相互影响。与此同时,如果之前我们使用的binder是本地服务的binder,这个时候我们再次bindservice就会导致程序崩溃,是因为service已经是远程服务了,而binder还是本地binder,不同的两个进程自然不能相互调用。这个时候就会需要我们用到android的IPC机制的AIDL了。下文详述。
Binder
Binder是Android中一个很重要且很复杂的概念,它在系统的整体运作中发挥着极其重要的作用,不过我并不打算从深层次分析Binder机制,有两点原因:1是目前网上关于Binder无论从底层到应用层的有关解析文章实在是太多了,2是对Binder机制进行深入底层乃至驱动的分析这一过程相当困难且相当耗时,而且我也没那个实力和精力去解读底层代码(很痛苦)。所以我只给大家给出一些总领性的分析,和几篇关于Binder分析的很好的文章,希望读者们能从这些文章里找到你们想要的答案!
什么是Binder
- 直观来说,Binder是Android中的一个类,它继承了IBinder接口
- 从IPC角度来说,Binder是Android中的一种跨进程通信方式,Binder还可以理解为一种虚拟的物理设备,它的设备驱动是/dev/binder,该通信方式在Linux中没有
- 从Android Framework角度来说,Binder是ServiceManager连接各种Manager(ActivityManager、WindowManager,etc)和相应ManagerService的桥梁
- 从Android应用层来说,Binder是客户端和服务端进行通信的媒介,当你bindService的时候,服务端会返回一个包含了服务端业务调用的Binder对象,通过这个Binder对象,客户端就可以获取服务端提供的服务或者数据,这里的服务包括普通服务和基于AIDL的服务
Android需要建立一套新的IPC机制来满足系统对通信方式,传输性能和安全性的要求,这就是Binder。Binder基于 Client-Server通信模式,传输过程只需一次拷贝,为发送发添加UID/PID身份,既支持实名Binder也支持匿名Binder,安全性高。
Binder的工作机制图(来源:Android开发艺术探索):
参考:
- http://www.cnblogs.com/innost/archive/2011/01/09/1931456.html
- http://blog.csdn.net/universus/article/details/6211589
- http://blog.csdn.net/luoshengyang/article/details/6618363
-
http://blog.csdn.net/huachao1001/article/details/51504469
书籍:《Android开发艺术探索》
在android开发中,binder主要用于service中,包括Messenger和AIDL,其中普通的service里面的binder不涉及进程间通信,AIDL则是binder的完美使用,下面带大家一起用小例子分析一下AIDL。
使用
在写出用法之前,必须理清楚几个概念,这对于后文的理解非常重要:
进程:一个程序或一个应用或后台,占有内存空间,是一个执行单元。
线程:CPU调度的最小单元,多个线程可存在于一个进程中。
在安卓应用中,一个应用的不同组件,如果他们运行在不同进程中,那么和它们分别属于两个应用没有本质区别,关于这点需要深刻理解,这也是进程通信的基础。
下文的例子使用的就是activity和service进行通信,给service单独设置了进程:
<service android:name="com.example.dosomeinterestingthings.WxjAidlService"
android:process=":remote">
</service>
服务端
1、创建一个service用来监听客户端的连接请求。
2、创建AIDL文件将暴露给客户端接口在这个AIDL文件中声明。
3、在service中实现这个AIDL接口即可。
客户端
1、绑定服务端的service。
2、成功后,将服务端返回的Binder对象转成AIDL接口所属的类型,接着就能调用方法了。
AIDL接口的创建
这里给大家抱歉一下,我两个电脑的Android studio都坏了,不能给大家贴AS的创建步骤,待我修复问题后,再补上AS版的,这里先用eclipse讲解过程。(^__^) 嘻嘻……
上图为项目结构
创建步骤:
1、 创建com.example.aidl包
2、 在包内创建Book.java、Book.aidl、IWxjAidl.aidl三个文件
3、 Build project(执行这步后才会有gen文件夹里自动生成的IWxjAidl.java这个文件)
注意第三步需要选Eclipse的工具栏的project项的Build project
Book类:
public class Book implements Parcelable{
public int bookId;
public String bookName;
public Book(int bookId,String bookName){
this.bookId = bookId;
this.bookName = bookName;
}
//反序列构造器
public Book(Parcel source) {
// TODO Auto-generated constructor stub
bookId = source.readInt();
bookName = source.readString();
}
@Override
public int describeContents() {
// TODO Auto-generated method stub
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
// TODO Auto-generated method stub
dest.writeInt(bookId);
dest.writeString(bookName);
}
//反序列化
public static final Parcelable.Creator<Book> CREATOR = new Parcelable.Creator<Book>() {
@Override
public Book createFromParcel(Parcel source) {
// TODO Auto-generated method stub
return new Book(source);
}
@Override
public Book[] newArray(int size) {
// TODO Auto-generated method stub
return new Book[size];
}
};
}
注意:
使用AIDL进行进程通信,所有的信息都要经过序列化和反序列化,也就意味着AIDL并不是支持所有类型的数据的。
一般情况,AIDL支持如下:
1、 基本数据类型(int、long、char、boolean、double等)
2、 String和CharSequence
3、 List:ArrayList,内部元素也需要被AIDL支持
4、 Map:HashMap,内部元素包括Key都要被AIDL支持
5、 Parcelable:所有实现Parcelable接口的对象
6、 AIDL:AIDL接口本身也可以在AIDL里声明
自定义的Parcelable对象和AIDL对象必须要显示的import进来
这样,我们需要的接口aidl和系统生成的对应java文件我们就全部生成好了。
远程服务端Service
用service实现AIDL接口:
public class WxjAidlService extends Service {
private static final String TAG = "wxj";
//支持并发读写的list
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();
private Binder mBinder = new IWxjAidl.Stub(){
@Override
public List<Book> getBooKList() throws RemoteException {
// TODO Auto-generated method stub
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
// TODO Auto-generated method stub
mBookList.add(book);
}
};
@Override
public IBinder onBind(Intent intent) {
// TODO Auto-generated method stub
return mBinder;
}
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
mBookList.add(new Book(1,"Android"));
mBookList.add(new Book(2,"wxj"));
}
}
这里采用了CopyOnWriteArrayList,它支持并发读写操作。注意前文我们说到AIDL支持的List只有ArrayList,但这里我们用CopyOnWriteArrayList是因为binder的底层机制是在线程池里执行的,所以会存在多个客户端多个线程同时访问的情况,而且AIDL所支持的是抽象的List而List只是一个接口,在Binder中会按照List的规范去访问数据并最终形成一个ArrayList传递给客户端,所以采用CopyOnWriteArrayList是完全可以的,类似的情况还有ConcurrentHashMap。
客户端实现
在activity里绑定service
public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private ServiceConnection conn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//实例化Stub里的Proxy对象
IWxjAidl wxj = IWxjAidl.Stub.asInterface(service);
try {
List<Book> list = wxj.getBooKList();
Log.i(TAG, "book list , list type:"+list.getClass().getCanonicalName());
Log.i(TAG, "book list:" + list.toString());
//add a new book
Book newbook = new Book(3,"xiyouji");
wxj.addBook(newbook);
Log.i(TAG, "book list:" + wxj.getBooKList().toString());
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//bindservice
Intent intent = new Intent(this,WxjAidlService.class);
bindService(intent, conn, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
// TODO Auto-generated method stub
unbindService(conn);
super.onDestroy();
}
}
运行log结果如下:
这样就完成了一次完整AIDL进行进程通信的过程了。
可是这还远远不够,上面的所有工作,我们都是客户端主动去请求远程,调用方法去获取远程的信息,那么我们能不能在远程里给出接口,每当有书添加进来的时候就让客户端选择是否需要接收这些信息呢?答案当然是肯定的,我们接着分析:
反监听
这里我们要使用的就是一种典型的观察者模式,每个感兴趣的用户都观察新书,每当有新书加入的时候,远程就告诉用户,这时我们需要在AIDL里提供一个接口,每个用户可以实现这个接口来接收远程的提醒,当然用户也可以随时取消这种提醒。这里我们创建INewBookListener.aidl文件,里面有新书添加方法:
package com.example.aidl;
import com.example.aidl.Book;
interface INewBookListener{
void onNewAddBook(in Book book);
}
同样的我们build project步骤同上文所述,这样我们就能得到INewBookListener.java这个映射文件
然后,我们修改上一个AIDL文件,给出两个方法让用户选择是否接收这个提醒功能的函数
//观察者模式 用户决定是否需要接受新的书信息
void accpetListener(INewBookListener listener);
void unaccpetListener(INewBookListener listener);
再build project,然后来到我们的service类,注册这个提醒,并且我们启动一个线程让它5S一次自动添加一本书
public class WxjAidlService extends Service {
······
// 标识位
private static final boolean flag = false;
private CopyOnWriteArrayList<INewBookListener> mListenerList = new CopyOnWriteArrayList<INewBookListener>();
private Binder mBinder = new IWxjAidl.Stub() {
······
@Override
public void accpetListener(INewBookListener listener)
throws RemoteException {
// TODO Auto-generated method stub
if (!mListenerList.contains(listener)) {
mListenerList.add(listener);
} else {
Log.e(TAG, "has added!");
}
}
@Override
public void unaccpetListener(INewBookListener listener)
throws RemoteException {
// TODO Auto-generated method stub
if (mListenerList.contains(listener)) {
mListenerList.remove(listener);
} else {
Log.e(TAG, "not found!");
}
}
};
······
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
mBookList.add(new Book(1, "Android"));
mBookList.add(new Book(2, "wxj"));
// 开启五秒自动添加线程
new Thread(new ServiceWorker()).start();
}
private class ServiceWorker implements Runnable {
@Override
public void run() {
// TODO Auto-generated method stub
while (!flag) {
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int bookId = mBookList.size() + 1;
Book newBook = new Book(bookId, "new Book create" + bookId);
try {
onNewAddBook(newBook);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public void onNewAddBook(Book book) throws RemoteException {
mBookList.add(book);
Log.d(TAG, "new book add,listener" + mListenerList.size());
for (int i = 0; i < mListenerList.size(); i++) {
INewBookListener listener = mListenerList.get(i);
Log.d(TAG, "notify listener:" + listener);
listener.onNewAddBook(book);
}
}
······
}
这么一来,service里面我们给出了添加新书的方法了,然后我们在activity里面只要注册这个提醒功能,并且用handler将信息从binder线程池提取到主线程里显示就行了,还有,我们需要在activity销毁的时候,取消这个提醒
private INewBookListener mINewBookListener = new INewBookListener.Stub() {
@Override
public void onNewAddBook(Book book) throws RemoteException {
// TODO Auto-generated method stub
mHandler.obtainMessage(1,book).sendToTarget();
}
};
wxj.accpetListener(mINewBookListener);
private Handler mHandler = new Handler(){
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1:
Log.d(TAG, "get a new book"+msg.obj);
break;
default:
break;
}
};
};
protected void onDestroy() {
// TODO Auto-generated method stub
if (mRemote!=null&&mRemote.asBinder().isBinderAlive()) {
try {
//activity销毁时,取消这个提醒
mRemote.unaccpetListener(mINewBookListener);
} catch (RemoteException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
unbindService(conn);
super.onDestroy();
}
下图是运行结果:
和我们预想的一样,每5S添加了一本书,并且用户也能正确得到提醒。但是,当我们用back键销毁activity时,却得到了如下结果:
我注销了这个提醒,但是,之后还能收到这个提醒,这就很神奇了我第一次也非常纳闷,为什么我们明明打印了注销日志,却还是可以收到提醒,然而形成这个场景的原因我们还是得从binder的转换机制说起,因为对象在跨进程过程里是不能直接传输的,binder会把客户端传过来的listener对象进行序列化和反序列化,这样一来,即使我们在activity里注册的listener和注销的listener是同一对象,但是经过binder传输之后就会产生两个完全不一的对象,这就是为什么AIDL定义的对象都必须实现Parcelable的原因,那么我们如何真正的去注销这个提醒功能呢?
RemoteCallBackList
下面我们就来使用RemoteCallBackList:
查看API,我们可以发现这个类是系统专门提供的用于删除跨进程listener的接口。RemoteCallBackList是一个泛型,支持管理任意的AIDL接口
在它内部有一个Map专门用来保存所有的AIDL回调,这个Map的key是IBinder类型,value是CallBack类型
ArrayMap<IBinder,CallBack> mCallBacks = new ArrayMap<IBinder,CallBack>();
//CallBack封装了真正的远程listener
IBinder key = listener.asBinder()
Callback value = new Callback(listener,cookie)
这样的话,虽然经过binder传输的listener是不同的,但是他们底层的binder却是同一个对象,当用户需要注销的时候,我们只要遍历远程的listener,找出和需要注销的listener具有相同的binder的key的listener并删掉就可以了,并且这个类还帮我们完成了线程同步和自动注销的功能,真是省事省心。
然后,我们进行代码修改:
private RemoteCallbackList<INewBookListener> mListenerList = new RemoteCallbackList<>();
@Override
public void accpetListener(INewBookListener listener)
throws RemoteException {
// TODO Auto-generated method stub
mListenerList.register(listener);
}
@Override
public void unaccpetListener(INewBookListener listener)
throws RemoteException {
// TODO Auto-generated method stub
mListenerList.unregister(listener);
}
public void onNewAddBook(Book book) throws RemoteException {
mBookList.add(book);
int n = mListenerList.beginBroadcast();
for(int i = 0;i<n;i++){
INewBookListener l = mListenerList.getBroadcastItem(i);
if(l!=null){
l.onNewAddBook(book);
}
}
mListenerList.finishBroadcast();
}
这次运行结果就正常了,注销之后,我们再也收不到提醒消息了!
到这里,AIDL的基本使用方法就介绍完了,但是还有一些需要我们注意的点,下面给大家说明。
AIDL使用注意
- Binder机制中,客户端调用远程方法的时候,远程的方法是运行在Binder的线程池里的,这时客户端还会被挂起,也就是说,如果远程的方法有长时间的耗时操作是会引起我们的客户端ANR现象的,读者们可以自行验证,只需要给addBook方法加个sleep就行,这里不贴代码了。解决方法也很简单,在activity里调用远程方法的时候,我们起子线程不让他在UI线程里执行就行了;同样的道理,当远程去实现客户端listener中的方法时,也要注意耗时问题,因为远程也没办法执行客户端的UI线程的耗时方法,所以我们要注意在service里的对外方法运行在非UI线程,否则将导致服务端无法响应。
- Binder是会意外死亡的,比如服务端进程意外停止了,那么我们需要在binder死亡的时候,重新连接服务,一般有两种方法:一是给binder设置DeathRecipient监听,binder死亡的时候我们会受到binderDied方法的回调,可以在这方法里进行重新连接操作;二是在onServiceDisconnected中重新连接,二者的区别就是binderDied运行在binder的线程池里,onServiceDisconnected运行在客户端的UI线程里。
- 正常情况下,我们是需要给远程服务设置权限验证的,不然任何人都能连接我们程序的远程服务这样在实际应用中是不允许的,而验证方式有许多种:一是在Mainifest文件里申明权限,然后在onBind方法里验证权限,不通过就给null,二是在服务端的onTransact方法里验证权限,不通过则返回false,三是给service指定android:permission属性等,方法方式大家可以自行验证和搜索。这里不一一贴出代码,不然篇幅收不住。
到这里,关于AIDL你所需要知道的一切基本讲述完了,希望读者能从中获益,也可以欢迎各位指出文章里的错误!