本文将会介绍AIDL通信。
本系列的其它文章:
Android跨进程通信-IPC初探(一)
Android跨进程通信-IPC初探(二) - 使用Messenger
回顾一下在IPC初探(一)中的示例,服务端为客户端提供了两个功能:
addStudent()
: 在客户端为服务端的List添加新的数据getStudentList()
:在客户端获取服务端的List数据。现在我们提出了新需求:
addStudent()
添加一条新的数据后,需要再次查询才能获取最新的数据。这意味着客户端必须主动向服务端发起查询,才能确定什么时候List更新了数据。设计解决方案
IOnNewStudentAddedListener
,用于服务端通知客户端。由于跨进程,这个接口使用AIDL,由系统自动生成。具体实现由客户端提供,而服务端在完成Student添加后,将会调用这个接口。IStudentManager
添加注册和注销方法。这两个方法在由服务端实现,提供给客户端调用。IOnNewStudentAddedListener
的具体实现,并通过注册/注销方法,通知服务端添加/删除这个接口对象。实现:
回调接口IOnNewStudentAddedListener
:
//IOnNewStudentAddedListener.aidl
package com.dou.ipcsimple;
import com.dou.ipcsimple.Student;
interface IOnNewStudentAddedListener {
void onNewStudentAdded(in Student student);
}
在IStudentManager
添加两个新的方法,用于注册和注销观察者。
package com.dou.ipcsimple;
import com.dou.ipcsimple.Student;
import com.dou.ipcsimple.OnNewStudentAddedListener;
interface IStudentManager {
void addStudent(in Student student);
List getStudentList();
//新增的接口,用于注册和注销观察者
void registerListener(OnNewStudentAddedListener listener);
void unregisterListener(OnNewStudentAddedListener listener);
}
服务端StudentManagerService
修改:
在Binder中,提供注册/注销两个方法的具体实现:
private CopyOnWriteArrayList mListeners = new CopyOnWriteArrayList<>();
private Binder binder = new IStudentManager.Stub() {
@Override
public void addStudent(Student student) throws RemoteException {
students.add(student);
}
@Override
public List getStudentList() throws RemoteException {
return students;
}
//注册
@Override
public void registerListener(IOnStudentAddedListener listener) throws RemoteException {
if (mListeners.contains(listener)) {
mListeners.add(listener);
} else {
Log.d(TAG,"already exists.");
}
Log.d(TAG,"registerListener size:"+ mListeners.size());
}
//注销
@Override
public void unregisterListener(IOnStudentAddedListener listener) throws RemoteException {
if (mListeners.contains(listener)) {
mListeners.remove(listener);
Log.d(TAG,"unregister listener succeed.");
} else {
Log.w(TAG,"Warning:listener not found, cannot unregister.");
}
Log.d(TAG,"unregisterListener size:"+ mListeners.size());
}
};
此外,我们做一个模拟测试,在服务端每5秒添加一个新的Student:
private AtomicBoolean mIsServiceDestroyed = new AtomicBoolean(false);
private class AddStudentWork implements Runnable {
@Override
public void run() {
while (!mIsServiceDestroyed.get()){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int studentID = students.size() + 1;
Student student = new Student(studentID, "newStu_" + studentID);
try {
noticeStudentAdded(student);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
//挨个儿通知各位观察者
private void noticeStudentAdded(Student student) throws RemoteException {
students.add(student);
Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size());
for (int i = 0; i < mListeners.size(); i++) {
IOnStudentAddedListener listener = mListeners.get(i);
listener.onStudentAdded(student);
}
}
@Override
public void onDestroy() {
mIsServiceDestroyed.set(true);
super.onDestroy();
}
客户端修改:
IOnNewStudentAddedListener
的具体实现,以及这个接口对象的创建:
//这里提供了IOnNewStudentAddedListener的匿名实现,这个mOnStudentAdded将作为接口对象注册/注销到远程服务端。
private IOnStudentAddedListener mOnStudentAdded = new IOnStudentAddedListener.Stub() {
@Override
public void onStudentAdded(Student student) throws RemoteException {
//这里我们给handler发送消息,后续操作将由handler处理。
handler.obtainMessage(MESSAGE_NEW_STUDENT_ADDED, student).sendToTarget();
}
};
//在handler中处理消息,可以更新UI线程。
private Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MESSAGE_NEW_STUDENT_ADDED:
Log.d(TAG, "receive new student:" + msg.obj);
break;
default:
super.handleMessage(msg);
}
}
};
在合适的地方注册/注销 mOnStudentAdded
:
private IStudentManager remoteStudentManager;
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
bindService = true;
IStudentManager studentManager = IStudentManager.Stub.asInterface(service);
try {
...
...
//注册
studentManager.registerListener(mOnStudentAdded);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
remoteStudentManager = null;
Log.d(TAG, "binder died.");
}
};
@Override
protected void onDestroy() {
if (remoteStudentManager != null && remoteStudentManager.asBinder().isBinderAlive()) {
Log.i(TAG, "unregister listener:" + mOnStudentAdded);
try {
//注销
remoteStudentManager.unregisterListener(mOnStudentAdded);
} catch (RemoteException e) {
e.printStackTrace();
}
}
if (bindService){
unbindService(mServiceConnection);
}
super.onDestroy();
}
运行测试:
客户端:能观察到正常的运行结果:
04-23 09:03:14.146 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry]]
04-23 09:03:14.149 21025-21025/com.dou.ipcsimple D/IPCSimple: Client Request students:[[stuId:1001, name:Tom], [stuId:1002, name:Jerry], [stuId:1003, name:Jack]]
04-23 09:03:19.141 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:4, name:newStu_4]
04-23 09:03:24.143 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:5, name:newStu_5]
04-23 09:03:29.146 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:6, name:newStu_6]
04-23 09:03:34.149 21025-21025/com.dou.ipcsimple D/IPCSimple: receive new student:[stuId:7, name:newStu_7]
服务端:能正常接受注册。但当我们点击后退键,退出这个Activity时,onDestroy
调用的注销方法出了BUG:
04-23 09:05:44.276 22645-22662/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1
04-23 09:05:45.646 22645-22658/com.dou.ipcsimple:remote W/StudentService: Warning:listener not found, cannot unregister.
04-23 09:05:45.647 22645-22658/com.dou.ipcsimple:remote D/StudentService: unregisterListener size:1
注销失败:反序列化—-每一个生成的对象都是崭新的。
mListeners.contains(listener)
返回了false
,注销的listener
与注册的listener
并不是同一个对象。 解决方案:使用RemoteCallbackList
.
RemoteCallbackList
内部使用ArrayMap
来保存所有的AIDL回调。对于本例中的3个listener对象(客户端1个,服务端2个),它们的低层Binder都是同一个。利用RemoteCallbackList
这个特性,就可以修复上面注销的BUG。使用RemoteCallbackList
替代CopyOnWriteArrayList
, 然后修改注册/注销方法的实现:
private RemoteCallbackList mListeners = new RemoteCallbackList<>();
// 注册
mListeners.register(listener);
// 注销
mListeners.unregister(listener);
以及,修改通知方法(请注意遍历方式):
```
private void noticeStudentAdded(Student student) throws RemoteException {
students.add(student);
/** 旧版本
Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + mListeners.size());
for (int i = 0; i < mListeners.size(); i++) {
IOnStudentAddedListener listener = mListeners.get(i);
listener.onStudentAdded(student);
}
//*/
//RemoteCallbackList not-is a List...
final int COUNT = mListeners.beginBroadcast();
Log.d(TAG, "noticeStudentAdded notify . listeners counts:" + COUNT);
for (int i = 0; i < COUNT; i++) {
IOnStudentAddedListener listener = mListeners.getBroadcastItem(i);
if (null != listener) {
listener.onStudentAdded(student);
}
}
mListeners.finishBroadcast();
}
```
好奇的看了一下getBroadcastItem()
,内部用数组来支持这种索引遍历方式:
```
public E getBroadcastItem(int index) {
return ((Callback)mActiveBroadcast[index]).mCallback;
}
```
修复之后的运行结果:当退出Activity时,注销成功:
04-23 10:17:46.187 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:1
04-23 10:17:47.369 14105-14105/com.dou.ipcsimple:remote W/StudentService: Service onDestroy
04-23 10:17:51.189 14105-14130/com.dou.ipcsimple:remote D/StudentService: noticeStudentAdded notify . listeners counts:0
所有的修改都可以在Github的完整示例代码中找打。被注释的则是旧版本代码,也就是导致注销失败的代码。
我们可以在Service的getStudentList
方法中添加延时,模拟耗时过长的方法:
public List getStudentList() throws RemoteException {
SystemClock.sleep(5000);
return students;
}
onServiceConnected
与 onServiceDisconnected
也是运行在UI线程中,所以也可能会导致ANR。上面的运行结果就是证明。
解决阻塞/ANR:
当服务端的某个方法是长耗时的,那么客户端就不要在UI线程里来调用它。当然在onServiceConnected
与 onServiceDisconnected
方法中也不要调用这个长耗时方法。
同理,我们也不可以在服务端主线程中调用客户端的耗时方法。例如,服务端的noticeStudentAdded
会调用客户端的回调接口方法 onStudentAdded
。如果onStudentAdded
耗时较多,那么请保证服务端的noticeStudentAdded
运行在非UI线程中,否则将导致服务端无法相应。
onStudentAdded
方法运行在客户端的Binder线程池中,所以不能在方法里面去访问UI相关的内容。如果要访问UI,请使用Handler切换到UI线程。Binder意外死亡之后的重新连接:
为Binder设置死亡代理:linkToDeath
和 unlinkToDeath
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if (remoteStudentManager == null) {
return;
}
remoteStudentManager.asBinder().unlinkToDeath(mDeathRecipient, 0);
remoteStudentManager = null;
//重新绑定远程服务
Intent intent = new Intent(MainActivity.this, StudentManagerService.class);
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
};
然后在客户端绑定远程服务成功后,给binder设置死亡代理:
service.linkToDeath(mDeathRecipient,0);
在onServiceDisconnected
中重新连接远程服务。
两种方法可以随便选一种来用。它们的区别在于:
onServiceDisconnected
在客户端的UI线程中被回调。AIDL中权限验证有两种方法:
1. 在服务端的onBind
方法中验证,如果验证不通过就返回null,验证失败的客户端就无法绑定服务。这里我们使用permission验证方式。
首先在manifest中声明所需要的权限:
<permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE"
android:protectionLevel="normal"/>
为onBind
方法添加权限验证:
public IBinder onBind(Intent intent) {
Log.d("StudentManagerService", "Service onBind");
int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
Log.e("StudentManagerService", "Service onBind :" + null);
return null;
}
return binder;
}
此时运行,客户端就无法连接到远程服务。查看Logcat,可以发现如下信息:
com.dou.ipcsimple:remote E/StudentManagerService: Service onBind :null
在manifest中注册该权限:
```
```
再次运行,客户端就可以连接上远程服务了。
2. 在服务端的onTransact
方法中进行权限验证,如果验证失败则返回false。
除了permission验证,还可以使用Uid和Pid来验证,通过getCallingUid
和getCallingPid
可以拿到客户端所属应用的Uid和Pid。
```
//StudentManagerService
private Binder binder = new IStudentManager.Stub() {
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
int check = checkCallingOrSelfPermission("com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE");
if (check == PackageManager.PERMISSION_DENIED) {
return false;
}
String pkgName = null;
String[] pkgs = getPackageManager().getPackagesForUid(getCallingUid());
if (null != pkgs && pkgs.length > 0) {
pkgName = pkgs[0];
}
if (!pkgName.startsWith("com.dou")) {
return false;
}
return super.onTransact(code, data, reply, flags);
}
...
...
...
};
```
显然,这个服务只有声明使用权限"com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE"
,而且包名以com.dou
开头的客户端,才能成功连接上远程服务端。
Binder通信机制说到这里就暂时告一段落,下一篇将介绍另一种IPC机制,Content Privider。
本篇文章中,完整的代码可以从Github获取:IPCSimple。