美文网首页需要使用Android技术知识Android开发经验谈
解锁进程间通信的各种姿势(1)AIDL下篇

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

作者: 落魄的安卓开发 | 来源:发表于2018-01-18 13:54 被阅读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为我们做的。

    • RemoteCallbackList是系统专门提供的用于删除跨进程listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,它的工作原理是内部有一个Map结构专门用来保存AIDL回调,这个Map的key是IBinder类型,value是Callback类型。

    • RemoteCallbackList在客户端进程终止后,它能够自动移除客户端所注册的listener。

    • RemoteCallbackList内部自动实现了线程同步的功能,使用它来注册和解注册时,不需要做额外的线程同步工作

    • 使用RemoteCallbackList需要注意:它的beginBroadcast和finishBroadcast必须要配对使用。示例如上述代码。

    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);
    }

    相关文章

      网友评论

        本文标题:解锁进程间通信的各种姿势(1)AIDL下篇

        本文链接:https://www.haomeiwen.com/subject/auqwoxtx.html