需要使用Android技术知识Android开发经验谈

解锁进程间通信的各种姿势(1)AIDL下篇

2018-01-18  本文已影响33人  落魄的安卓开发

上一篇实现了一个简单的客户端调用远程服务的Demo,这一篇来记录下关于AIDL的其他内容:

本篇内容包含如下:

  1. 新需求:当远程服务有了新书之后主动告诉客户端
  2. Binder死亡处理
  3. 权限验证

如果文中有不正确的地方还望指出,不要误人误己,感谢。学习自《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());
        }
    };
}

改动说明:

  1. 我们返回给客户端的Binder,增加了registerListener和unregisterListener两个方法,这个是与修改IBookManager.aidl文件之后对应的
  2. 在Service的onCreate方法中起了一个线程,每隔3s就自动添加一本新书,然后遍历观察者集合告诉客户端们新书的到来

重要内容说明:

观察者集合这里我们用的是RemoteCallbackList,为什么不能用CopyOnWriteArrayList呢(可以看到我已经给注释掉了)?原因如下:

注册的观察者客户端在解注册的时候会解除不了。

因为在多进程应用中,Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然我们在注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后,会产生两个全新的对象,因为对象跨进程传输的本质是反序列化的过程,这也是为什么AIDL的自定义对象都必须实现Parcelable接口的原因。

解决上述问题:

虽然跨进程传输的同一个对象会在服务端生成不同的对象,但是新生成的对象有一个共同点,就是它们的底层的Binder对象是同一个,利用这个特性就可以实现上面解注册的功能。 当客户端解注册的时候,我们只要遍历服务端所有的listener,找出那个和解注册listener具有相同Binder对象的服务端listener并把它删掉即可,这就是RemoteCallbackList为我们做的。

Binder死亡处理

Binder运行在服务端进程,如果服务端进程由于某种原因异常终止,这个时候我们到服务端的Binder连接断裂(称之为Binder死亡),会导致我们的远程调用失败。 在服务意外停止后,我们需要重新连接服务,有两种方法:

  1. Binder中提供了两个配对的方法linkToDeath和unlinkToDeath,通过linkToDeath,可以给Binder设置一个死亡代理,当Binder死亡时,就会收到通知,这个时候就可以重新发起连接请求,步骤如下:

    1. 声明一个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();
           }
       };
      
    2. 在客户端绑定远程服务成功后,给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();
           }
       }
      
  2. 在onServiceDisconnected中重连远程服务

  3. 这两种方式的区别在于:onServiceDisconnected在客户端UI线程中被回调;而binderDied在客户端的Binder线程池中被回调,不能访问UI。

权限验证

给远程服务加上权限验证,这样就不是任何人都能调用了。常用的有两种方法:

方法1: 在onBind中验证,验证不通过就直接返回null,这样验证失败的客户端直接无法绑定服务,验证方法有很多,比如自定义Permission。具体如下:

  1. 在AndroidManifest.xml

     <!--声明权限-->
     <uses-permission android:name="com.thc.remotetest"
         android:protectionLevel="normal"/>
     <!--自定义权限-->
     <permission android:name="com.thc.remotetest"
         android:protectionLevel="normal"/>
    
  2. 在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);
}
上一篇下一篇

猜你喜欢

热点阅读