解锁进程间通信的各种姿势(1)AIDL下篇
上一篇实现了一个简单的客户端调用远程服务的Demo,这一篇来记录下关于AIDL的其他内容:
本篇内容包含如下:
- 新需求:当远程服务有了新书之后主动告诉客户端
- Binder死亡处理
- 权限验证
如果文中有不正确的地方还望指出,不要误人误己,感谢。学习自《Android开发艺术探索》
实现新需求
既然想监听服务端的新书情况,就要注册监听到服务端,当服务端有新书添加就会告诉客户端,这是一个典型的观察者模式,实现步骤如下:
Setp1 定义AIDL接口:
因为AIDL中不能使用普通接口,所以只能定义AIDL接口。创建aidl接口代码如下:
// IOnNewBookArrivedListener.aidl
package com.thc.binderdemo;
// Declare any non-default types here with import statements
import com.thc.binderdemo.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook);
}
Setp2 在原AIDL接口上增加注册和反注册的两个方法,代码如下:
// IBookManager.aidl
package com.thc.binderdemo;
// Declare any non-default types here with import statements
import com.thc.binderdemo.Book;
import com.thc.binderdemo.IOnNewBookArrivedListener;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
Setp3 修改服务端Service:
AIDL接口变动之后,在远程Service中的实现也要有相应的改变代码如下:
public class MyRemoteService extends Service {
private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);
/**
* AIDL中能够使用的List只有ArrayList,但是这里使用了CopyOnWriteArrayList(不继承ArraryList),这里为什么能够工作呢?
* <p>
* 因为 AIDL 中所支持的是抽象的List,而List只是一个接口,因此虽然服务端返回的是CopyOnWriteArrayList,但是再Binder中会按照List的规范去访问数据并最终形成一个新的ArrayList返回给客户端。
* <p>
* 和CopyOnWriteArrayList相似的还有ConcurrentHashMap
*/
CopyOnWriteArrayList<Book> copyOnWriteArrayList = new CopyOnWriteArrayList<>();
/**
* 观察者集合
*/
RemoteCallbackList<IOnNewBookArrivedListener> listenerList = new RemoteCallbackList<>();
// CopyOnWriteArrayList<IOnNewBookArrivedListener> listenerList = new CopyOnWriteArrayList<>();
public MyRemoteService() {
}
@Override
public void onDestroy() {
mIsServiceDestoryed.set(true);
super.onDestroy();
}
@Override
public void onCreate() {
super.onCreate();
//在Service创建后先添加两本书
copyOnWriteArrayList.add(new Book("三国演义", 0));
copyOnWriteArrayList.add(new Book("水浒传", 1));
new Thread(new Runnable() {
@Override
public void run() {
while (!mIsServiceDestoryed.get()) {
SystemClock.sleep(3000);
int bookId = copyOnWriteArrayList.size() + 1;
Book newBook = new Book("newBook#" + bookId, bookId);
copyOnWriteArrayList.add(newBook);
try {
onNewBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}).start();
}
/**
* 遍历观察者集合,告知用户有新书到了
*/
private void onNewBookArrived(Book newBook) throws RemoteException {
// for (int i = 0; i < listenerList.size(); i++) {
// IOnNewBookArrivedListener listener = listenerList.get(i);
// listener.onNewBookArrived(newBook);
// }
final int N = listenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = listenerList.getBroadcastItem(i);
if (l != null) {
l.onNewBookArrived(newBook);
}
}
listenerList.finishBroadcast();
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
private Binder mBinder = new IBookManager.Stub() {
@Override
public List<Book> getBookList() throws RemoteException {
return copyOnWriteArrayList;
}
@Override
public void addBook(Book book) throws RemoteException {
copyOnWriteArrayList.add(book);
}
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
listenerList.register(listener);
Log.e("result", "注册后的监听数量:" + listenerList.beginBroadcast());
listenerList.finishBroadcast();
// if (!listenerList.contains(listener)) {
// listenerList.register(listener);
// } else {
// Log.d("result", "already exists.");
// }
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
listenerList.unregister(listener);
Log.e("result", "解注册后的监听数量:" + listenerList.beginBroadcast());
listenerList.finishBroadcast();
// if (listenerList.contains(listener)) {
// listenerList.remove(listener);
// } else {
// Log.d("result", "not found,can not unregister");
// }
// Log.d("result", "unregisterListener,current size:" + listenerList.size());
}
};
}
改动说明:
- 我们返回给客户端的Binder,增加了registerListener和unregisterListener两个方法,这个是与修改IBookManager.aidl文件之后对应的
- 在Service的onCreate方法中起了一个线程,每隔3s就自动添加一本新书,然后遍历观察者集合告诉客户端们新书的到来
重要内容说明:
观察者集合这里我们用的是RemoteCallbackList,为什么不能用CopyOnWriteArrayList呢(可以看到我已经给注释掉了)?原因如下:
注册的观察者客户端在解注册的时候会解除不了。
因为在多进程应用中,Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,会产生两个全新的对象,因为对象跨进程传输的本质是反序列化的过程,这也是为什么AIDL的自定义对象都必须实现Parcelable接口的原因。
解决上述问题:
虽然跨进程传输的同一个对象会在服务端生成不同的对象,但是新生成的对象有一个共同点,就是它们的底层的Binder对象是同一个,利用这个特性就可以实现上面解注册的功能。 当客户端解注册的时候,我们只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删掉即可,这就是RemoteCallbackList为我们做的。
-
RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,它的工作原理是内部有一个Map结构专门用来保存AIDL回调,这个Map的key是IBinder类型,value是Callback类型。
-
RemoteCallbackList在客户端进程终止后,它能够自动移除客户端所注册的listener。
-
RemoteCallbackList内部自动实现了线程同步的功能,使用它来注册和解注册时,不需要做额外的线程同步工作
-
使用RemoteCallbackList需要注意:它的beginBroadcast和finishBroadcast必须要配对使用。示例如上述代码。
Binder死亡处理
Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的Binder连接断裂(称之为Binder死亡),会导致我们的远程调用失败。 在服务意外停止后,我们需要重新连接服务,有两种方法:
-
Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath,可以给Binder设置一个死亡代理,当Binder死亡时,就会收到通知,这个时候就可以重新发起连接请求,步骤如下:
-
声明一个DeathRecipient对象。DeatchRecipient是一个接口,内部只有一个binderDied方法,需要实现这个方法,当Binder死亡的时候系统会回调binderDied方法,然后就移除之前绑定的binder代理并重新绑定服务端进程服务:
private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() { @Override public void binderDied() { if(iBookManager==null){ return; } iBookManager.asBinder().unlinkToDeath(deathRecipient,0); iBookManager = null; //TODO 重新绑定 reBind(); } };
-
在客户端绑定远程服务成功后,给Binder设置死亡代理
@Override public void onServiceConnected(ComponentName name, IBinder binder) { iBookManager = IBookManager.Stub.asInterface(binder); try { binder.linkToDeath(deathRecipient,0); } catch (RemoteException e) { e.printStackTrace(); } try { List<Book> bookList = iBookManager.getBookList(); Log.e("result", bookList.get(0).getBookName() + "--" + bookList.get(1).getBookName()); iBookManager.registerListener(onNewBookArrivedListener); } catch (RemoteException e) { e.printStackTrace(); } }
-
-
在onServiceDisconnected中重连远程服务
-
这两种方式的区别在于:onServiceDisconnected在客户端UI线程中被回调;而binderDied在客户端的Binder线程池中被回调,不能访问UI。
权限验证
给远程服务加上权限验证,这样就不是任何人都能调用了。常用的有两种方法:
方法1: 在onBind中验证,验证不通过就直接返回null,这样验证失败的客户端直接无法绑定服务,验证方法有很多,比如自定义Permission。具体如下:
-
在AndroidManifest.xml
<!--声明权限--> <uses-permission android:name="com.thc.remotetest" android:protectionLevel="normal"/> <!--自定义权限--> <permission android:name="com.thc.remotetest" android:protectionLevel="normal"/>
-
在onBind方法中:
private static final String PERMISSION = "com.thc.remotetest"; @Override public IBinder onBind(Intent intent) { /** * 权限验证方式一: * * 如果没有在AndroidManifest.xml中添加自定义的权限就返回null */ int value = checkCallingOrSelfPermission(PERMISSION); if (value == PackageManager.PERMISSION_DENIED) { return null; } return mBinder; }
方法2: 在服务端的onTransact方法中进行权限验证,如果验证失败就直接返回false,这样服务端就不会执行AIDL中的方法,从而达到保护服务端的效果,同样也可以采用自定义权限的方法,还可以通过采用Uid和Pid来做验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid,通过这两个参数可以做一些验证操作,如下:
/**
* 权限验证方式二:
*
* 采用Uid和Pid来做 验证,通过getCallingUid和getCallingPid可以拿到客户端所属应用的Uid和Pid
*
* 通过这两个参数可以做一些验证工作,比如验证包名,如下:
*/
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
String packageName = null;
String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
if(packages != null && packages.length>0){
packageName = packages[0];
}
if(!packageName.startsWith("com.thc")){
return false;
}
return super.onTransact(code, data, reply, flags);
}