前言
实现Android中的IPC(进程间通信)
有多种方式,而AIDL
则是其中功能最强大的一种方式。通过AIDL
,我们不仅可以在进程间传递数据,还可以实现RPC(远程方法调用)
。很多时候,一些业务需求只能使用AIDL
这种方式进行实现,因此掌握AIDL
的使用至关重要。本文就简单讲解一下如何使用AIDL
实现IPC
。
基础概念
支持的数据类型
首先,我们要知道AIDL
支持哪些数据类型,如下所示:
- 基本数据类型
String
和CharSequence
- 实现
Parcelable
接口的对象 List
,准确来说是ArrayList
,并要求包含的元素也是AIDL
支持的数据类型Map
,准确来说是HashMap
,并要求键、值都是AIDL
支持的数据类型- 其它AIDL接口对应的对象
AIDL
主要就支持这6种数据。
AIDL接口的创建
知道了AIDL
支持那些数据类型,接下来就以AndroidStudio
为例讲解AIDL
接口如何创建。我们需要先创建一个aidl
文件,文件的后缀名是.aidl
。在这个文件中,我们需要书写提供给客户端的接口。需要注意的是,在这个接口中只能有方法,不能提供静态常量。此时我们需要执行Build->Clean Project
,AndroidStudio
会借助这个aidl
文件自动生成对应的Java文件。实际上,我们后续使用的就是这个自动生成的Java文件中的类。以下是一个简单的aidl
文件:
//IBookManager.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
可以看到,在这个aidl
文件中,我们声明了一个AIDL
接口,并声明了两个方法。需要注意的是,如果在接口中使用了自定义的类对象,那么必须将这个类显式
地import
进来。此外,如果将自定义的类对象作为参数,那么必须指明它是什么类型的参数,可以选择in、out、inout
三者中的一种。比如本例中的Book
类,需要显式地导入,并且指明其为in
类型,即输入型参数。
import com.example.ipcdemo.aidl.Book;
void addBook(in Book book);
Book
类实现了Parcelable
接口,其结构如下:
public class Book implements Parcelable {
private int bookId;
private String name;
public Book(int bookId, String name) {
this.bookId=bookId;
this.name = name;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(bookId);
dest.writeString(name);
}
public static final Creator<Book> CREATOR=new Creator<Book>(){
@Override
public Book createFromParcel(Parcel source) {
return new Book(source);
}
@Override
public Book[] newArray(int size) {
return new Book[size];
}
};
//用于反序列化Book的私有构造方法
private Book(Parcel source){
bookId=source.readInt();
name=source.readString();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "bookId:"+bookId+" bookName:"+name;
}
}
Parcelable
是AIDL
支持的数据类型之一,用于实现对象的序列化
。关于对象序列化的具体内容,可以参考这一篇博客:
需要注意的是,在AIDL
中使用的自定义类,必须为它们创建对应的aidl
文件,在这个文件中声明它是Parcelable
对象。比如本例中的Book
类,需要为其创建Book.aidl
文件。文件内容如下:
// Book.aidl
package com.example.ipcdemo.aidl;
parcelable Book;
到这里,一个基本的AIDL
接口就写好了。然后,我们需要执行AndroidStudio
的Build->Clean Project
,IDE
会自动帮我们生成对应的Java文件。这个自动生成的文件名字就叫IBookManager.java
,和AIDL
文件的名字保持一致。在这个文件中有一个静态内部类,名字叫Stub
,它继承了Binder
类,并实现了IBookManager
接口。实际上,我们后续使用的都是这个Stub
类的实例。关于这一过程的细节,可以参考这篇博客:
深入理解AIDL(先挖个坑,有空就填)
小提示:
关于aidl
的文件结构,这里给出一个建议。通过AndroidStudio
的New->AIDL
方式创建aidl
文件后,IDE
会自动为我们生成一个aidl文件夹
,它和java文件夹
是同级的。在这个文件夹中,存在和Java文件夹相同的主包结构
。因此,我们可以在这个文件夹下再创建一个名为aidl
的文件夹,然后将所有aidl
文件都放到这个文件夹中。此外,我们还需要在Java文件夹
中也创建一个名为aidl
的文件夹,然后将所有AIDL
接口会使用到的自定义类都放到这个文件夹中。这样,aidl文件
和java文件
的结构就保持一致了。同时,这也方便了将aidl
移植到其他应用中。直接将这两个包全部复制过去即可,减少了出错的可能。在上面的例子中,最终的文件结构如下:
基本流程
服务器端
首先,需要提供一个Service
,用于监听客户端的绑定请求。其次,我们需要创建自己的AIDL
接口,并在Service
中实例化这个接口对应的Stub
对象。最后,在Service
的onBind
方法中,我们需要将这个AIDL
实例对象作为Binder
返回。
客户端
首先,我们需要绑定远程服务端,并将绑定后返回的IBinder
对象转化为AIDL
接口。通过这个对象,我们就可以跨进程调用服务端的方法了。
一个典型的例子
服务器端:
public class ServerService extends Service {
private static final String TAG="ServerService";
private CopyOnWriteArrayList<Book> bookList=new CopyOnWriteArrayList<>();
private IBinder binder=new IBookManager.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return bookList;
}
@Override
public void addBook(Book book) throws RemoteException {
bookList.add(book);
}
};
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
//初始化数据
bookList.add(new Book(1,"小王子"));
bookList.add(new Book(2,"长尾理论"));
}
}
可以看到,我们在Service
中实例化了一个AIDL
接口对应的对象,在这里实际上是通过IBookManager.Stub
来进行实例化的。IBookManager
和IBookManager.Stub
都是IDE
根据aidl
文件自动生成的。在匿名内部类中,我们重写了接口中的两个方法。在onBind
方法中,我们将这个AIDL
对象作为Binder
返回。
此外,我们实例化了一个CopyOnWriteArrayList
对象,用于存储Book
数据。这里之所以不使用ArrayList
,是因为AIDL
中的方法运行在服务器端的Binder
线程池中,多个客户端可以并发请求,这就带来了线程同步的问题。使用CopyOnWriteArrayList
,支持并发读,就解决了线程同步的问题。但是上文提到过,AIDL
只支持ArrayList
,那为什么这里可以使用CopyOnWriteArrayList
呢?原因在于,虽然这里使用的是CopyOnWriteArrayList
,但是AIDL
会按照List
的规则去读取数据,最后返回给客户端的就是一个ArrayList
。同样的,我们也可以使用ConcurrentHashMap
,道理是一样的。
之后,我们为这个Service
指定私有进程名称,让它运行在一个独立的进程中。
<service
android:name=".ServerService"
android:process=":remote">
</service>
客户端:
public class ClientActivity extends AppCompatActivity {
private static final String TAG="ClientActivity";
private IBookManager bookManager;
private ServiceConnection serviceConnection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookManager=IBookManager.Stub.asInterface(service);
try {
List<Book> bookList=bookManager.getBookList();
Log.i(TAG,bookList.toString());
bookManager.addBook(new Book(3,"野火集"));
List<Book> newBookList=bookManager.getBookList();
Log.i(TAG,"添加新的书:"+newBookList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
Intent intent=new Intent(this,ServerService.class);
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);//解绑
}
}
可以看到,首先要绑定远程服务端。在onServiceConnnected
方法中,我们将返回的IBinder
对象转化为对应的AIDL
接口。通过这个接口,我们就可以调用服务端的方法了。在onDestroy
方法中,我们则进行了解绑操作。
private IBookManager bookManager;
bookManager=IBookManager.Stub.asInterface(service);
需要注意的是,服务器端的方法运行在Binder线程池
中,我们在客户端调用服务器端方法后,客户端的线程就被阻塞了,直到服务器端的方法执行完毕才会恢复。如果服务器端的方法中执行了耗时操作,而客户端是在UI线程
发起的请求,应用就可能出现ANR
问题。因此,正确的做法是在客户端通过异步请求的方式调用服务器端的方法。
运行结果:
.../com.example.ipcdemo I/ClientActivity: [bookId:1 bookName:小王子, bookId:2 bookName:长尾理论]
.../com.example.ipcdemo I/ClientActivity: 添加新的书:[bookId:1 bookName:王子, bookId:2 bookName:长尾理论, bookId:3 bookName:野火集]
实现观察者模式
上面提供了一个典型的AIDL
例子,通过这种方式我们可以向服务器端添加Book
,也可以查询Book列表
。但是这都是客户端的主动操作,很多时候我们需要服务端主动提醒客户端Book列表
是否改变。考虑这种场景,我们希望IBookManager
在添加新的Book
后,就主动通知每个客户端,而不需要客户端自己去查询数据是否改变。显然,这就构成了一个典型的观察者模式
,IBookManager
成了发布者
,而客户端们则成了订阅者
。关于这个模式的详细讲解可以参考这篇博客:
观察者模式(先挖个坑,有空就填)
为了实现观察者模式,我们需要提供一个新的AIDL
接口,之所以不选用普通接口是因为AIDL
中不支持普通接口。每个客户端都向IBookManger
注册自己的接口对象,IBookManager
则会将每个客户端对应的接口对象保存下来。当Book列表
数据改变后,IBookManager
就遍历自己的接口列表,依次执行这些接口中的回调方法,这也就起到了通知客户端的作用。具体实现如下,新增的主要代码前面都添加了//Add
注释:
新增的AIDL接口:
// IOnBookAddListener.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;
interface IOnBookAddListener {
void onBookAdd(in Book book);
}
新增的AIDL
接口很简单,就是用来监听IBookManager
中Book列表
数据是否改变的监听器。
修改后的原AIDL接口
// IBookManager.aidl
package com.example.ipcdemo.aidl;
import com.example.ipcdemo.aidl.Book;
//Add
import com.example.ipcdemo.aidl.IOnBookAddListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
//Add
void registerListener(IOnBookAddListener listener);
void unRegisterListener(IOnBookAddListener listener);
}
可以看到,修改后的IBookManager
接口中新增了两个方法,一个用于注册监听器,一个用于取消注册。由于这两个方法的参数是AIDL接口
,因此不需要指定参数的输入/输出类型(in、out、inout)
。但是需要注意的是,新增的AIDL接口
依旧需要显式地导入进来。
import com.example.ipcdemo.aidl.IOnBookAddListener;
修改后的服务器端:
public class ServerService extends Service {
private static final String TAG="ServerService";
private CopyOnWriteArrayList<Book> bookList=new CopyOnWriteArrayList<>();
//Add:存储监听器的数据结构
private RemoteCallbackList<IOnBookAddListener> listenerList=new RemoteCallbackList<>();
private IBinder binder=new IBookManager.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return bookList;
}
@Override
public void addBook(Book book) throws RemoteException {
bookList.add(book);
}
//Add
@Override
public void registerListener(IOnBookAddListener listener) throws RemoteException {
listenerList.register(listener);
}
@Override
public void unRegisterListener(IOnBookAddListener listener) throws RemoteException {
listenerList.unregister(listener);
}
};
//Add:添加书籍
public void addBook(Book book) throws RemoteException {
bookList.add(book);
int listenerNum=listenerList.beginBroadcast();
for(int i=0;i<listenerNum;i++){
IOnBookAddListener listener=listenerList.getBroadcastItem(i);
listener.onBookAdd(book);
}
listenerList.finishBroadcast();
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
@Override
public void onCreate() {
super.onCreate();
//初始化数据
bookList.add(new Book(1,"小王子"));
bookList.add(new Book(2,"长尾理论"));
//Add
new Thread(){
@Override
public void run() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
addBook(new Book(4,"创新者的窘境"));
} catch (RemoteException e) {
e.printStackTrace();
}
}
}.start();
}
}
可以看到,我们在客户端创建了一个RemoteCallbackList
对象来保存客户端的监听器。
private RemoteCallbackList<IOnBookAddListener> listenerList=new RemoteCallbackList<>();
RemoteCallbackList
是AIDL
中专门用来存储AIDL接口
以及删除跨进程listener
的数据结构。之所以不使用ArrayList
存储AIDL接口
,是因为listener
对象在通过跨进程传输时,实际上经过了一个序列化和反序列化的过程。这意味着在服务器端得到的listener
对象只是和客户端的listener
对象内容相同,两者本质上依旧是不同的对象。因此在客户端进行解注册操作时,服务器端会因为找不到与客户端使用的listener
相同的对象而解注册失败。而RemoteCallbackList
内部进行了特殊处理,让我们得以正常地移除客户端的listener
。关于RemoteCallbackList
的原理解析,可以参考下文的RemoteCallbackList解析
。
在IBookManager.Stub
中,我们实现了新增的两个方法,分别用于注册listener
和取消注册listener
。此外,我们新增了一个addBook
方法。这个方法用于向IBookManager
中添加一个Book
对象。在这个方法中,我们逐次调用了每个listener
的回调方法,这就起到了通知客户端的作用。需要注意RemoteCallbackList
的使用方式,它和ArrayList
的使用方式完全不同。
进行遍历前,先要调用beginBroadcast
方法,这个方法会返回RemoteCallbackList
中的元素数量。然后在for
循环中,通过getBroadcastItem
方法逐次获取存储的listener
。最后,还要调用finishBroadcast
方法,这才算是构成了一个完整的遍历过程。
int listenerNum=listenerList.beginBroadcast();
IOnBookAddListener listener=listenerList.getBroadcastItem(i);
listenerList.finishBroadcast();
最后,我们在Service
的onCreate
方法中开启了一个匿名的线程。这个线程的作用是在3秒后向IBookManager
中添加一个Book
对象,主要用于测试是否成功实现了观察者模式。
修改后的客户端:
public class ClientActivity extends AppCompatActivity {
private static final String TAG="ClientActivity";
private static final int MSG_ON_BOOK_ADD=1;
private IBookManager bookManager;
//Add
private Handler handler=new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what){
case MSG_ON_BOOK_ADD:
Log.i(TAG,msg.obj.toString());
break;
default:
break;
}
}
};
//Add
private IOnBookAddListener listener=new IOnBookAddListener.Stub(){
@Override
public void onBookAdd(Book book) throws RemoteException {
Message message=new Message();
message.what=MSG_ON_BOOK_ADD;
message.obj=book;
handler.sendMessage(message);
}
};
private ServiceConnection serviceConnection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookManager=IBookManager.Stub.asInterface(service);
try {
List<Book> bookList=bookManager.getBookList();
Log.i(TAG,bookList.toString());
bookManager.addBook(new Book(3,"野火集"));
List<Book> newBookList=bookManager.getBookList();
Log.i(TAG,"添加新的书:"+newBookList.toString());
//Add:注册监听器
bookManager.registerListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
Intent intent=new Intent(this,ServerService.class);
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
unbindService(serviceConnection);//解绑
//Add:取消注册监听器
try {
bookManager.unRegisterListener(listener);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
可以看到,我们通过实例化IOnBookAddListener.Stub
获得了一个AIDL
接口对象,并在onServiceConnected
方法中进行了注册。在onDestroy
方法中,我们又将listener
从IBookManager
中取消注册。
bookManager.registerListener(listener);
bookManager.unRegisterListener(listener);
需要注意的是,IOnBookAddListener
中的回调方法是在客户端的Binder线程池
中执行的,因此不能在这个方法里直接操作客户端UI
。在本例中,我们提供了一个Handler
对象,并在IOnBookAddListener
的回调方法中通过Handler
发送Message
,借助Handler
切换到UI
线程执行后续操作。
运行结果:
05-05 01:31:43.707 15028-15028/com.example.ipcdemo I/ClientActivity: [bookId:1 bookName:小王子, bookId:2 bookName:长尾理论]
05-05 01:31:43.707 15028-15028/com.example.ipcdemo I/ClientActivity: 添加新的书:[bookId:1 bookName:小王子, bookId:2 bookName:长尾理论, bookId:3 bookName:野火集]
05-05 01:31:46.667 15028-15028/com.example.ipcdemo I/ClientActivity: bookId:4 bookName:创新者的窘境
RemoteCallBackList解析
RemoteCallbackList
的内部维持着一个Map
结构,具体来说是一个ArrayMap
,如下所示:
ArrayMap<IBinder,Callback> mCallbacks=new ArrayMap<IBinder,Callback>();
可以看到,这个Map
的key
是IBinder
对象,value
则是Callback
对象。我们来看一下key
和value
是怎么获得的:
IBinder binder = callback.asBinder();
Callback cb = new Callback(callback, cookie);
这样一来,RemoteCallbackList
的工作原理就很清晰了。虽然经过序列化和反序列化后,客户端注册时和解除注册时的listener
在服务器端并不是同一个对象,但是它们底层的Binder
却是同一个对象。通过这个Binder
对象,就可以从RemoteCallbackList
内部的Map
中获得Callback
对象,也就间接获得了listener
对象。在Map
中移除这个Callback
对象,就达到了取消注册listener
的作用。
最佳实践
重连机制
我们要知道,在实际使用中Binder
是有可能死亡的,一般是由于服务器端进程的突然停止造成的。为了避免这种情况的发生,我们应该为Binder
设置重连机制。一旦Binder
重新死亡,就需要重新绑定服务器端。下面介绍两种方法。
一种方法是为Binder
对象设置死亡代理
,具体代码如下所示:
private IBinder.DeathRecipient deathRecipient=new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if(bookManager!=null){
return;
}
bookManager.asBinder().unlinkToDeath(deathRecipient,0);
bookManager=null;
//重新绑定服务
Intent intent=new Intent(ClientActivity.this,ServerService.class);
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
}
};
private ServiceConnection serviceConnection=new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bookManager=IBookManager.Stub.asInterface(service);
try {
//设置死亡代理
service.linkToDeath(deathRecipient,0);
.........
} catch (RemoteException e) {
e.printStackTrace();
}
}
.......
};
可以看到,我们先创建了一个IBinder.DeathRecipient
对象,并在它的回调方法binderDied
中进行了重连操作。此外,我们在ServiceConnection
的onServiceConnected
方法中为IBookManager
设置了这个死亡代理
。linkToDeath
方法的第二个参数是一个标识,一般传入0
即可。
service.linkToDeath(deathRecipient,0);
需要注意的是,在对这种方式进行测试时,未能获得理想的效果,暂时先留个坑在这里。
另一种方法是在ServiceConnection
的onServiceDisconnected
方法中执行重连操作,具体代码如下所示:
private ServiceConnection serviceConnection=new ServiceConnection() {
......
@Override
public void onServiceDisconnected(ComponentName name) {
//重连操作
Intent intent=new Intent(ClientActivity.this,ServerService.class);
bindService(intent,serviceConnection,BIND_AUTO_CREATE);
}
};
以上两种方式的区别在于,第一种方式的binderDied
方法在Binder
线程池中调用,而第二种方式的onServiceConnnected
方法则在UI
线程中调用。
权限验证
作为服务器端,自然需要具备权限验证功能,否则任何应用都可以调用我们的远程服务,这显然不是我们想要的。一般来说,在Service
的onBind
方法中进行权限验证就可以了。接下来介绍一种简单的验证方式,即通过permission
进行权限验证。
首先,我们需要在服务端自定义权限,如下所示:
<permission
android:name="com.example.ipcdemo.CONNECT_TO_SERVICE"
android:protectionLevel="normal">
</permission>
可以看到,我们将permission
的级别设置为normal
。关于permission
的详细解析,可以看一看这篇博客:
在服务器端的onBind
方法中,我们先进行权限判断。如果客户端拥有相应权限,我们才返回Binder
对象,否则返回null
。具体代码如下所示:
@Override
public IBinder onBind(Intent intent) {
int permissionEnd=checkCallingOrSelfPermission("com.example.ipcdemo.CONNECT_TO_SERVICE");
if(permissionEnd==PackageManager.PERMISSION_DENIED){
return null;
}
return binder;
}
需要注意的是,如果Service
的onBind
方法返回null
,则ServiceConnection
的onServiceConnected
方法将不会被调用。
需要绑定服务器端的客户端,只要在自己的AndroidManifest
文件中声明对应的权限即可,如下所示:
<uses-permission android:name="com.example.ipcdemo.CONNECT_TO_SERVICE"></uses-permission>
Binder线程池
通过前面的例子,可以发现我们的AIDL是建立在一个Service基础上的。如果仅仅采用这种模式,当我们的业务需求较多时,就需要单独创建很多个Service。对于一个大型项目而言,这显然是不现实的。因此,需要引入线程池的概念。即通过一个统一的Service向外提供AIDL服务,并针对不同的业务请求返回不同的Binder,这就解决了Service数量过多的问题。关于线程池的具体使用,请参考这篇博客:
Binder线程池(按照惯例,先挖坑,慢慢填)
小结
通过上文的讲解,我们基本就了解AIDL
的常规使用了。此外,我们还需要记住,在客户端调用服务器端的方法时,这些方法运行在服务器端的Binder线程池
中,客户端的线程则会陷入阻塞状态。因此,如果是在客户端的UI线程
中进行远程调用,就要注意使用异步的方式,避免出现ANR
。而服务器端的方法,本来就运行在线程池中,所以我们应该避免在服务器端的方法中再开启线程。同样的,从服务器端远程调用客户端中的回调方法时,这些方法也运行在客户端的Binder
线程池中。
项目demo下载地址
下面给出上述例子的demo
下载地址:IPCDemo