IPC初探(三) - 使用AIDL
本文将会介绍AIDL通信。
本系列的其它文章:
Android跨进程通信-IPC初探(二) - 使用Messenger
1. 示例的新需求
回顾一下在IPC初探(一)中的示例,服务端为客户端提供了两个功能:
addStudent()
: 在客户端为服务端的List添加新的数据getStudentList()
:在客户端获取服务端的List数据。
现在我们提出了新需求:
- 问题:当客户端调用
addStudent()
添加一条新的数据后,需要再次查询才能获取最新的数据。这意味着客户端必须主动向服务端发起查询,才能确定什么时候List更新了数据。 - 新需求:现在我们要改变这个流程,当服务端添加了新的数据时,会主动通知客户端。这就是观察者模式。
- 解决方案:常用的回调接口方式在这里行不通—-因为这是跨进程,需要提供AIDL接口,来达到跨进程的目的。
- 问题:当客户端调用
设计解决方案
- 设计一个回调接口
IOnNewStudentAddedListener
,用于服务端通知客户端。由于跨进程,这个接口使用AIDL,由系统自动生成。具体实现由客户端提供,而服务端在完成Student添加后,将会调用这个接口。 - 为
IStudentManager
添加注册和注销方法。这两个方法在由服务端实现,提供给客户端调用。 - 服务端需要提供注册和注销方法的具体实现,客户端获取到远程binder后,通过binder(在合适的地方)调用这两个方法。
- 客户端需要提供
IOnNewStudentAddedListener
的具体实现,并通过注册/注销方法,通知服务端添加/删除这个接口对象。 - 为了做模拟测试,我们在服务端添加了一个线程,它每5秒会添加一个新的Student,并通知客户端。
- 设计一个回调接口
实现:
回调接口
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<Student> getStudentList(); //新增的接口,用于注册和注销观察者 void registerListener(OnNewStudentAddedListener listener); void unregisterListener(OnNewStudentAddedListener listener); }
服务端
StudentManagerService
修改:在Binder中,提供注册/注销两个方法的具体实现:
private CopyOnWriteArrayList<IOnStudentAddedListener> mListeners = new CopyOnWriteArrayList<>(); private Binder binder = new IStudentManager.Stub() { @Override public void addStudent(Student student) throws RemoteException { students.add(student); } @Override public List<Student> 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(); }
- 完整的代码可以从Github获取IPCSimple
2. 添加回调接口之后的运行结果分析
运行测试:
客户端:能观察到正常的运行结果:
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
并不是同一个对象。
- 这里通过binder传递对象,而这个传递的本质是序列化与反序列化。所以,对客户端的listener分别通过注册和注销传递给服务端,反序列化得到的两个listener,是两个不同的对象—-因为每次序列化都会创建一个独立的对象。
解决方案:使用
RemoteCallbackList
.RemoteCallbackList
内部使用ArrayMap<IBinder,Callback>
来保存所有的AIDL回调。对于本例中的3个listener对象(客户端1个,服务端2个),它们的低层Binder都是同一个。利用RemoteCallbackList
这个特性,就可以修复上面注销的BUG。使用
RemoteCallbackList
替代CopyOnWriteArrayList<IOnStudentAddedListener>
, 然后修改注册/注销方法的实现:private RemoteCallbackList<IOnStudentAddedListener> 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的完整示例代码中找打。被注释的则是旧版本代码,也就是导致注销失败的代码。
- 原因探究:显然
3. 回顾Binder工作流程
我们再来看看Binder工作图解:
- Client在发起远程请求之后,会被挂起,此时如果服务端方法执行耗时过多,就会导致Client线程阻塞。如果这个Client线程是UI线程,就可能会导致ANR。
我们可以在Service的
getStudentList
方法中添加延时,模拟耗时过长的方法:public List<Student> getStudentList() throws RemoteException { SystemClock.sleep(5000); return students; }
然后运行,发现ANR:
onServiceConnected
与onServiceDisconnected
也是运行在UI线程中,所以也可能会导致ANR。上面的运行结果就是证明。
解决阻塞/ANR:
- 当客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中。同理,当远程服务端调用客户端的方法中,被调用的方法运行在客户端的Binder线程池中。
当服务端的某个方法是长耗时的,那么客户端就不要在UI线程里来调用它。当然在
onServiceConnected
与onServiceDisconnected
方法中也不要调用这个长耗时方法。- 应当把长耗时方法的调用放在非UI线程中。比如另起一个Thread来运行。
同理,我们也不可以在服务端主线程中调用客户端的耗时方法。例如,服务端的
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
中重新连接远程服务。两种方法可以随便选一种来用。它们的区别在于:
- Binder的死亡代理,在客户端Binder线程池中被回调,所以不能访问UI。
onServiceDisconnected
在客户端的UI线程中被回调。
4. 添加权限验证功能
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中注册该权限:
``` <uses-permission android:name="com.dou.ipcsimple.permission.ACCESS_STUDENT_SERVICE" /> ``` 再次运行,客户端就可以连接上远程服务了。
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
开头的客户端,才能成功连接上远程服务端。
4. 尾声
Binder通信机制说到这里就暂时告一段落,下一篇将介绍另一种IPC机制,Content Privider。
本篇文章中,完整的代码可以从Github获取:IPCSimple。