- 上一篇中提到,在对我们客户端进行解绑监听的时候,并没有成功,服务端打印出not found listener的log,而我们客户端在进行绑定和解绑的时候传递的listener明明是同一个,为什么会出现这种情况呢?其实这是必然的,这种解注册的处理方式在我们平时的开发中经常使用到,但是放在多进程中却是失效的,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们解绑和绑定传递的是同一个对象,但是通过Binderde转换已经是两个全新的对象了。因为对象是不能跨进程直接传输的,对象的跨进程传输本质上就是反序列化的过程,这就是为什么AIDL中的自定义对象必须实现Parcelable接口的原因。
- 那到底如何进行解绑呢,下面介绍一下RemoteCallbackList
-
- RemoteCallbackList是系统专门提供的用户删除跨进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口
public interface IBookManager extends android.os.IInterface
- 它的工作原理很简单,在它的内部有一个Map结构专门用来保存所有的AIDL回调,这个map的key是IBinder类型,value是CallBack类型,如下
其中Callback中封装了真正的远程listener。当客户端注册listener的时候,它会把这个listener的信息存入mCallbacks中,其中key和value分别通过下面的方式获取/*package*/ ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();
IBinder binder = callback.asBinder(); try { Callback cb = new Callback(callback, cookie); binder.linkToDeath(cb, 0); mCallbacks.put(binder, cb); return true; } catch (RemoteException e) { return false; }
- RemoteCallbackList是系统专门提供的用户删除跨进程listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口
- 因此,虽然多次跨进程传输的对象在服务端会生成不同的对象,但是这些新生成的对象有一个共同点,那就是他们底层的Binder对象是同一个,利用这个特性,我们就可以实现上面无法实现的功能,当客户端解绑注册的时候,我们只要遍历服务端所有的listener,找出和那个解注册listener具有相同Binder对象的服务器端listener,并把它删掉即可。
- 同时,RemoteCallbackList还有一个很有用的功能,那就是当客户端进程终止后,他能自动移除客户端所注册的listener,另外RemoteCallbackList内部实现了线程同步的功能,所以在注册和解注册的时候,不需要做额外的线程同步工作。
- 下面,演示一下如何完成解注册
-
- 在BookService中,使用RemoteCallbackList代替CopyOnWriteArrayList,如下
private RemoteCallbackList<IONewBookArrivedListener> listenerList = new RemoteCallbackList<>();
- 修改注册和解注册接口的实现
@Override public void registerNewBookArrivedListener(IONewBookArrivedListener listener) throws RemoteException { listenerList.register(listener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Log.i(TAG, "listenerList size = " + listenerList.getRegisteredCallbackCount()); } } @Override public void unRegisterNewBookArrivedListener(IONewBookArrivedListener listener) throws RemoteException { listenerList.unregister(listener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { Log.i(TAG, "listenerList size = " + listenerList.getRegisteredCallbackCount()); } }
- 修改onNewBookArrived方法,当有新书时,通知所有已注册的listener
private void onNewBookArrived(Book book) throws RemoteException { bookList.add(book); int N = listenerList.beginBroadcast(); for (int i = 0; i < N; i++) { IONewBookArrivedListener arrivedListener = listenerList.getBroadcastItem(i); if (arrivedListener != null) { arrivedListener.onNewBookArrived(book); } } listenerList.finishBroadcast(); }
- 至此,BookService修改完毕,通过在注册和解注册时打log查看成功否,如下
很显然,使用RemoteCallbackList可以实现跨进程的解注册功能。03-11 11:16:44.486 31043-31073/com.happy.ipc.server I/BookService: listenerList size = 1 03-11 11:16:47.226 31043-31073/com.happy.ipc.server I/BookService: listenerList size = 0
- 在BookService中,使用RemoteCallbackList代替CopyOnWriteArrayList,如下
- 使用RemoteCallbackList,无法像操作List一样去操作它,虽然名字中带List,但它却不是一个List,遍历他的画,需要beginBroadcase和finishBroadcase配对使用。
- 至此,AIDL的基本用法基本介绍完了,有几点需要说明一下
-
- 客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端会被挂起,如果服务端比较耗时,会导致客户端长时间阻塞在这里,如果是客户端的UI线程的话,就会导致客户端出现ANR错误,因此,当我们知道远程方法是耗时的时候,不应该再在UI线程进行调用了。
- 客户端的onServiceConnected和onServiceDisconnected方法都运行在UI线程中,所以也不能在其里面直接调用服务端的耗时方法
- 服务端的方法本身就运行在Binder线程池中,因此不需要再在服务端另起线程执行操作了
- 同理,当远程服务需要调用客户端的listener中的方法时,被调用的方法运行在客户端的Binder线程池中,所以同样不可以在服务端中调用客户端的耗时方法。比如BookService中的onNewBookArrived方法,调用了客户端的onNewBookArrived方法,如果客户端的这个方法比较耗时的话,要确保BookService中的onNewBookArrived运行在非UI线程中,否则会导致服务端无法相应。
- 客户端的IOnNewBookArrivedListener中的onNewBookArrived方法运行在客户端的Binder线程池中,所以不能在此方法中进行UI相关操作,如果需要,借助Handler进行处理。
- Binder是可能意外死亡的,为了提高健壮性,我们需要在服务端进程意外终止的时候重新连接服务,两种方法
-
- 给Binder设置DeathRecipient监听,当Binder死亡时,我们会收到binderDied的回掉,可以重新连接服务,如下
private ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mBookManager = IBookManager.Stub.asInterface(service); try { service.linkToDeath(deathRecipient, 0); } catch (RemoteException e) { e.printStackTrace(); } try { mBookManager.registerNewBookArrivedListener(listener); } catch (RemoteException e) { e.printStackTrace(); } } @Override public void onServiceDisconnected(ComponentName name) { mBookManager = null; } }; private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { if (mBookManager != null) { mBookManager.asBinder().unlinkToDeath(deathRecipient, 0); mBookManager = null; } // 重新连接服务 } };
- 在onServiceDisconnected中重新连接远程服务
@Override public void onServiceDisconnected(ComponentName name) { mBookManager = null; // 重新连接服务 }
- 区别在于:onServiceDisconnected在客户端的UI线程中被回调,而binderDied在客户端的Binder线程中被回调,不能进行UI访问操作。
- 给Binder设置DeathRecipient监听,当Binder死亡时,我们会收到binderDied的回掉,可以重新连接服务,如下
- 最后,如何在AIDL中使用权限验证功能。默认情况下,我们的远程服务任何人都可以链接,但这应该不是我们愿意看到的,因此,我们必须给服务加入权限验证功能,验证失败,则无法调用服务中的方法。以下为两种常用的方法
-
- 在onBinder中进行验证,验证不通过直接返回null,这也验证失败的客户端就无法直接绑定付无,验证方式有很多种,比如使用permission验证,使用这种方式,首先在AndroidManifestz红声明所需的权限,比如 这样,一个应用来绑定我们的服务时,会验证这个应用的权限,达到想到的效果,此方法同样适用于Messenger中。如果我们自己内部的应用想绑定到我们的服务中,需要在AndroidManifest中声明permission即可,如下
@Override public IBinder onBind(Intent intent) { int check = checkCallingOrSelfPermission("com.happ.ipc.server.ACCESS_BOOK_SERVICE"); if (check == PackageManager.PERMISSION_DENIED) { Log.i(TAG, "permission denied"); return null; } Log.i(TAG, "permission access"); return mBinder; }
ps:如果服务端和客户端是两个工程,则在Service中无法验证客户端的权限,因为onBinde方法不是一个binder调用的,它运行在服务端的UI线程,因此在onBind中只能验证服务端的权限,这样就木有意义了,所以推荐使用第二种。<service android:name=".service.BookService" android:enabled="true" android:exported="true"></service> </application> <permission android:name="com.happ.ipc.server.ACCESS_BOOK_SERVICE" android:protectionLevel="normal" />
- 第二种方法,在服务端的onTransact方法中进行权限验证,如果验证失败直接返回false,这也服务端也不会执行AIDL中的方法,从而达到保护服务端的效果。具体的验证方式很多,可以采用permission验证,实现和第一种一样,还可以采用pid和uid来做验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的uid和pid,通过这种方式可以做一些验证工作,比如包名。下面既验证了permission,又验证了报名。一个应用如果想远程调用服务的方法,首先要使用我们刚才定义的权限,并且包名是"com.happy.ipc.client",否则就会调用失败
查看日志发现@Override public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { String packageName = null; int callingPid = getCallingPid(); int callingUid = getCallingUid(); Log.i(TAG, "callingPid = " + callingPid + ",callingUid = " + callingUid); String[] packagesForUid = BookService.this.getPackageManager().getPackagesForUid(callingUid); if (packagesForUid != null && packagesForUid.length > 0) { packageName = packagesForUid[0]; } Log.i(TAG, "packageName = " + packageName); if (TextUtils.isEmpty(packageName) || !"com.happy.ipc.client".equals(packageName)) { return false; } return super.onTransact(code, data, reply, flags); }
如果包名进行修改,比如03-11 11:48:09.516 28044-28057/com.happy.ipc.server I/BookService: listenerList size = 1 03-11 11:48:20.876 28044-28057/com.happy.ipc.server I/BookService: callingPid = 28469,callingUid = 11166 03-11 11:48:20.876 28044-28057/com.happy.ipc.server I/BookService: packageName = com.happy.ipc.client
这样,通过client点击时间获取书的列表和count的时候,打印如下log,成功实现权限验证if (TextUtils.isEmpty(packageName) || !"com.happy.ipc.client.test".equals(packageName)) { return false; }
03-11 11:52:02.196 3507-3507/com.happy.ipc.client I/MainActivity: bookList = [] 03-11 11:52:02.826 3507-3507/com.happy.ipc.client I/MainActivity: count = 0
- 上门介绍了AIDL权限验证的两种方法,此外还有其他方法比如在service中指定android:permissio属性等。
- 在onBinder中进行验证,验证不通过直接返回null,这也验证失败的客户端就无法直接绑定付无,验证方式有很多种,比如使用permission验证,使用这种方式,首先在AndroidManifestz红声明所需的权限,比如 这样,一个应用来绑定我们的服务时,会验证这个应用的权限,达到想到的效果,此方法同样适用于Messenger中。如果我们自己内部的应用想绑定到我们的服务中,需要在AndroidManifest中声明permission即可,如下
- 至此,IPC中的AIDL基本介绍完了。