天天看点

IPC机制---04 Android中的IPC通讯方式(D)

  • 上一篇中提到,在对我们客户端进行解绑监听的时候,并没有成功,服务端打印出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类型,如下
      /*package*/ ArrayMap<IBinder, Callback> mCallbacks
                  = new ArrayMap<IBinder, Callback>();
                 
      其中Callback中封装了真正的远程listener。当客户端注册listener的时候,它会把这个listener的信息存入mCallbacks中,其中key和value分别通过下面的方式获取
      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;
                  }
                 
  • 因此,虽然多次跨进程传输的对象在服务端会生成不同的对象,但是这些新生成的对象有一个共同点,那就是他们底层的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查看成功否,如下
      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
                 
      很显然,使用RemoteCallbackList可以实现跨进程的解注册功能。
  • 使用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访问操作。
  • 最后,如何在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;
          }
                 
      <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" />
                 
      ps:如果服务端和客户端是两个工程,则在Service中无法验证客户端的权限,因为onBinde方法不是一个binder调用的,它运行在服务端的UI线程,因此在onBind中只能验证服务端的权限,这样就木有意义了,所以推荐使用第二种。
    • 第二种方法,在服务端的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
                 
      如果包名进行修改,比如
      if (TextUtils.isEmpty(packageName) || !"com.happy.ipc.client.test".equals(packageName)) {
                      return false;
                  }
                 
      这样,通过client点击时间获取书的列表和count的时候,打印如下log,成功实现权限验证
      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属性等。
  • 至此,IPC中的AIDL基本介绍完了。