在了解了AIDL的流程及基本原理之后,我们还需要对AIDL有进一步的了解。
在上一篇的例子的基础之上,我们考虑另一种情况:
假设有一种需求:用户不想时不时地去查询图书列表了,太累了,于是,他去问图书馆,“当有新书时能不能把书的信息告诉我呢?”。
大家应该明白了,这就是一种典型的观察者模式,每个感兴趣的用户都观察新书,当新书到的时候,图书馆就通知每一个对这本书感兴趣的用户,这种模式在实际开发中用得很多,下面我们就来模拟这种情形。
首先,我们需要提供一个AIDL接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能,当然用户也可以随时取消这种提醒。之所以选择AIDL接口而不是普通接口,是因为AIDL中无法使用普通接口。这里我们创建一个IOnNewBookArrivedListener.aidl文件,我们所期望的情况是:当服务端有新书到来时,就会通知每一个己经申请提醒功能的用户。从程序上来说就是调用所有IOnNewBookArivedListener对象中的onNewBookArived方法,并把新书的对象通过参数传递给客户端,内容如下所示。
// IOnNewBookArrivedListener.aidl
package com.liuguilin.ipcsample;
import com.liuguilin.ipcsample.Book;
interface IOnNewBookArrivedListener {
void onNewBookArrived(in Book newBook);
}
除了要新增加一个AIDL外,还需要在原有的接口中添加两个新的方法
// IBookManager.aidl
package com.liuguilin.ipcsample;
import com.liuguilin.ipcsample.Book;
import com.liuguilin.ipcsample.IOnNewBookArrivedListener.aidl;
interface IBookManager {
List<Book>getBookList();
void addBook(in Book book);
void registerListener(IOnNewBookArrivedListener listener);
void unregisterListener(IOnNewBookArrivedListener listener);
}
接下来,我们的服务端也是要稍微的修改一下,每隔5s向感兴趣的用户提醒
public class BookManagerService extends Service {
private static final String TAG = BookManagerService.class.getSimpleName();
private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);
private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();
private Binder mBinder = new IBookManager.Stub(){
@Override
public List<Book> getBookList() throws RemoteException {
return mBookList;
}
@Override
public void addBook(Book book) throws RemoteException {
mBookList.add(book);
}
@Override
public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
mListenerList.register(listener);
}
@Override
public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
mListenerList.unregister(listener);
}
};
@Override
public void onCreate() {
super.onCreate();
mBookList.add(new Book(1,"Android"));
mBookList.add(new Book(2,"Ios"));
new Thread(new ServiceWorker()).start();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public void onDestroy() {
super.onDestroy();
}
private class ServiceWorker implements Runnable{
@Override
public void run() {
while (!mIsServiceDestoryed.get()){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
int bookId = mBookList.size()+1;
Book newBook = new Book(bookId,"new book#" + bookId);
try {
onNewBookArrived(newBook);
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
}
private void onNewBookArrived(Book book) throws RemoteException{
mBookList.add(book);
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
if(l != null){
l.onNewBookArrived(book);
}
}
mListenerList.finishBroadcast();
}
}
最后,我们还需要修改一下客户端的代码,主要是两方面,首先是客户端要注册IOnNewBookArrivedListener到远程的服务器,这样当有新书时服务端才能通知客户端,同时在我们的Activity的onDestory方法里面去取消绑定,,另一方面,当有新书的时候,服务端会会带哦客户端的Binder线程池中执行,因此,为了便于进行UI操作,我们需要有一个Hnadler可以将其切换到客户端的主线程去执行,这个原理在Binder中已经做了分析了,这里不多说,把代码贴上:
public class BookManagerActivity extends AppCompatActivity {
private static final String TAG = BookManagerActivity.class.getSimpleName();
private static final int MESSAGE_NEW_BOOK_ARRIVED = 0x100;
private IBookManager mRemoteBookManager;
private Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
switch (msg.what){
case MESSAGE_NEW_BOOK_ARRIVED:
Log.d(TAG,"receive the book:"+msg.obj);
break;
default:
break;
}
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_bookmanager);
Intent intent = new Intent(this, BookManagerService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
super.onDestroy();
if(mRemoteBookManager!=null && mRemoteBookManager.asBinder().isBinderAlive()){
try {
Log.d(TAG,"unregister listener:" + mOnNew);
mRemoteBookManager.unregisterListener(mOnNew);
} catch (RemoteException e) {
e.printStackTrace();
}
}
unbindService(mConnection);
}
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
try {
mRemoteBookManager = bookManager;
List<Book> list = bookManager.getBookList();
Log.i(TAG, "list string : " + list.toString());
Book newBook = new Book(3,"Android进阶");
bookManager.addBook(newBook);
bookManager.registerListener(mOnNew);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mRemoteBookManager = null;
Log.e(TAG,"binder died.");
}
};
private IOnNewBookArrivedListener mOnNew = new com.cmnit.ipc.IOnNewBookArrivedListener.Stub() {
@Override
public void onNewBookArrived(Book newBook) throws RemoteException {
mHandler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED,newBook).sendToTarget();
}
};
}
- 客户端注册IOnNewBookArrivedListener到远程服务端,这样当有新书时服务端才能通知当前客户端,同时我们要在Activity退出时解除这个注册;另一方面,当有新书时,服务端会回调客户端的IOnNewBookArrivedListener对象中的onNewBookArrived方法,但是这个方法是在客户端的Binder线程池中去执行的,因此便于UI操作,我们需要有一个Handler可以将其切换到主线程中去执行。
特别说明
客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间阻塞在这里,如果客户端线程是UI线程的话,就会导致ANR。
因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去访问远程方法。由于客户端的onServiceConnected和 onServiceDisconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法,这点要尤其注意。另外,由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,这个时候切记不要在服务端方法中开线程执行异步任务,除非你明确知道自己在干什么,否则不建议这么做。
同理,当远程服务需要调用客户端的listener中的方法时,被调用的方法也运行在Binder线程池中,只不过是客户端的Binder线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。比如针对BookManagerService的onNewBookArrived方法,在它内部调用了客户端的IOnNewBookArrivedListener中的onNewBookArrived方法,如果客户端的这个onNewBookArrived方法比较耗时的话,那么请确保BookManagerService中的onNewBookArrived运行在非UI线程中,否则将导致服务端无法响应。
private void onNewBookArrived(Book book) throws RemoteException{
mBookList.add(book);
final int N = mListenerList.beginBroadcast();
for (int i = 0; i < N; i++) {
IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
Log.d(TAG,"onNewBookArrived,notify listener:"+l);
if(l != null){
l.onNewBookArrived(book);
}
}
mListenerList.finishBroadcast();
}
另外,由于客户端的IOnNewBookArrivedListener中的onNewBookArrived方法运行在客户端的Binder线程池中,所以不能在里面进行UI更新。
断开重连机制
Binder是可能意外死亡的,这往往是由于服务端进程意外终止了,这是我们需要重新连接服务。有两种方法:
- 给Binder设置DeathRecipient监听
当Binder死亡时,我们会收到binderDied的回调,在binderDied方法中我们可以重连远程服务。
- 首先,声明一个Deathleciient对象、Deathleciient是一个接口,其内部只有一个方法binderDied,我们需要实现这个方法,当Binder死亡的时候,系统就会回调binderDied方法,然后我们就可以移除之前绑定的binder代理并重新绑定远程服务;
### BookManagerActivity.java
private IBinder.DeathRecipient mDeathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
if(mBookManager == null){
return;
}
mBookManager.asBinder().unlinkToDeath(mDeathRecipient,0);
mBookManager = null;
//这个可以重新绑定远程service
bindService();
}
};
- 在客户端绑定远程服务成功之后,给binder设置死亡代理;
### BookManagerActivity.java
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IBookManager bookManager = IBookManager.Stub.asInterface(service);
service.linkToDeath(mDeathRecipient,0);
...
}
- 在onServiceDisconnected中重连远程服务
区别在于,onServiceDisconnected在客户端的UI线程中被回调,而binderDied在服务端的Binder线程池中被回调。
权限验证
默认情况下,我们的远程服务任何人都可以接入,但这不是我们想看到的,所以我们必须加入权限验证功能,权限验证失败将无法调用服务中的方法。
- 在服务端的onBinder中进行验证
- 在服务端的onTransact方法中进行验证
首先需要在服务端的 AndroidMenifest 文件中声明所需权限:
<permission android:name="com.example.zs.ipcdemo.permission.ACCESS_BOOK_SERVIC" android:protectionLevel="normal" />
<uses-permission android:name="com.example.zs.ipcdemo.permission.ACCESS_BOOK_SERVIC" />
客户端 AndroidManifest.xml 中添加权限:
<uses-permission android:name="com.example.zs.ipcdemo.permission.ACCESS_BOOK_SERVIC" />
第一种方法:
public IBinder onBind(Intent t) {
// 远程调用的验证方式
int check = checkCallingPermission("com.example.zs.ipcdemo.permission.ACCESS_BOOK_SERVIC");
if (check == PackageManager.PERMISSION_DENIED) {
// 权限校验失败
return null;
}
return mBinder;
}
第二种方法:
// 权限校验
private boolean checkPermission(Context context, String permName, String pkgName) {
PackageManager pm = context.getPackageManager();
if (PackageManager.PERMISSION_GRANTED == pm.checkPermission(permName, pkgName)) {
return true;
} else {
return false;
}
}
@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 == null) {
return false;
}
boolean checkPermission = checkPermission(getApplication(),
"com.example.zs.ipcdemo.permission.ACCESS_BOOK_SERVIC", packageName);
return checkPermission && super.onTransact(code, data, reply, flags);
}