Android 中的 IPC 方式(三) AIDL

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_29874521/article/details/82664017

上一篇文章我们介绍了 Messenger 如何来实进程间通信的方法,我们可以发现,Messenger 是以单行的方式处理客户端发来的消息的,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用 Messenger 就不太合适了。同时,Messenger 的作用主要是用来传递消息,很多时候我们可能需要跨进程调用服务端的方法,这个情形用 Messenger 就无法实现了,但是我们可以使用 AIDL 来实现跨进程的方法的调用。AIDL 也是 Messenger 底层的实现,因此 Messenger 本质上也是 AIDL,只不过系统为我们做了封装从而方便上层的调用而已。在前面我们也介绍了 Binder 的概念,相信读者对 Binder 也有了一定的了解,在 Binder 的基础上我们也更加容易的了解 AIDL。这里先介绍使用 AIDL 来实现进程间通信的流程,分为服务端和客户端两个方面:

(1)服务端

服务端首先要创建一个 Service,用来监听客户端的链接请求,然后创建一个 AIDL 文件,将暴露给客户端的接口在这个 AIDL 文件中说明,最后在 Service 中实现这个 AIDL 接口即可。

(2)客户端

客户端所做的事情稍微简单一些,首先要绑定服务端的 Service,绑定成功后,将服务端返回的 Binder 对象转成 AIDL 接口的类型,接着就可以调用 AIDL 中的方法了。

1. AIDL 接口的创建

首先看 AIDL 接口的创建,如下所示,我们创建了一个后缀名为 AIDL 的文件,在里面声明了一个接口和两个接口方法。

// IBookManager.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;

interface IBookManager {

     List<Book> getBookList();
     void addBook(in Book book);
 }

在 AIDL 文件中,并不是所有的数据类型都可以使用的,那么 AIDL 到底支持哪些数据类型呢?如下所示:

● 基本数据类型(int、long、char、double、boolean 等);

● String  和 CharSeqence;

● List:只支持 ArrayList,里面每个元素都必须能被 AIDL 支持;

● Map:只支持 HashMap,里面每个元素都必须能被 AIDL 支持,包括 key 和 value;

● Parcelable:所有实现了 Parcelable 接口的对象;

● AIDL:所有 AIDL 接口本身也可以在 AIDL 文件中使用。

以上六中类型就是 AIDL 所支持的所有类型,其中自定义的 Parcelable 对象和 AIDL 对象必须显示的 import 进来。

另一个需要注意的地方是,如果 AIDL 文件中共用到了自定义的 Parcelable 对象,那么必须新建一个和他同名的 AIDL 文件,在上面的 IBookManager.aidl 中,我们用到了 Book 这个类,所以,我们必须要创建 Book.aidl,然后在里面添加如下内容:

// Book.aidl
package com.demo.text.demotext.aidl;

parcelable Book;

我们需要注意,AIDL 中每个实现了 Parcelable 的类都需要按照上面的方式去创建响应的 AIDL 文件并声明那个类为 Parcelable。除此之外,AIDL 中除了基本数据类型,其他类型的参数必须标上方向:in、out 或者 inout,in 表示输入型参数,数据只能由客户端流向服务端,表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 表示输出型参数,数据只能由服务端流向客户端,表现为服务端将会接收到那个对象的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 表示输入输出型参数,数据可在服务端与客户端之间双向流通,表现为服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。我们根据实际需求去指定参数类型,不能一概用 out 或者 inout,因为这在底层实现是有开销的。最后,AIDL 接口中只支持方法,不支持声明静态常量,这一点区别于传统的接口。

为了方便 AIDL开发,建议把所有的和 AIDL 相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用时,我们可以直接把整个包复制到客户端工程中,对于本利来说,我要要把 com.demo.text.demotext.aidl 这个包和包中的文件原封不动的复制到客户端中。如果 AIDL 相关的文件位于不同的包中时,那么就需要把这些包一一复制到客户端工程中,这样操作起来比较麻烦而且容易出错。需要注意的是,AIDL 的包结构在服务端和客户端要保持一致,否则运行会出错,这是因为客户端要反序列化服务端中和 AIDL 接口相关的所有类,如果类的完整路径不一样的话,就无法成功反序列化,程序也就无法整成运行。

1. 远程服务端 Service 的实现

上面我们讲述了如何定义 AIDL 接口,接下来我们就需要实现这个接口了,我们先创建一个 Service,称为 BookManagerService,代码如下:

public class BookManagerSerrvice extends Service {

    private static final String TAG = "BMS";
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private Binder binder = 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 onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Andrroid"));
        mBookList.add(new Book(2, "IOS"));
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }
}

上面是一个服务端典型的 Service 实例,首先在 onCreate 中添加两本图书的信息,然后创建一个 Binde 对象并在 onBind 中返回它,这个对象继承自 IBookManager.Stub 并实现了它内部的 AIDL 方法。这个过程在前面就介绍过了,这里就不多说了。这里主要看 getBookList 和 addBook 这两个 AIDL 方法的实现,实现过程也不叫简单,注意这里采用了CopyOnWriteArrayList,这个 CopyOnWriteArrayList 支持并发读/写。在前面我们提到,AIDL 方法在服务端的 Binder 线程池中执行,因此多个客户端同时连接的时候,会存在多线程同时访问的情况,所以我们要在 AIDL 方法中处理线程同步,而我们这里直接采用 CopyOnWriteArrayList 来进行自动的线程同步。

前面我们提到 AIDL 中能够使用的 List 只有 ArrayList,但是我们这里却是用了 CopyOnWriteArrayList(它不是继承 ArrayList),为什么能够正常工作呢?这是因为 AIDL 中所支持的是抽象的 List,而 List 只是一个接口,因此虽然服务端返回的是 CopyOnWriteArrayList ,但是在 Binder 中会按照 List 的规范去访问数据并最终形成一个新的 ArrayList 传递给客户端。所以,我们在服务端采用 CopyOnWriteArrayList 是完全可以的。和此类似的还有 ConcurrentHashMap。

2.客户端的实现

客户端的实现就比较简单了,首先绑定远程服务,绑定成功后,将服务端的 Binder 转换成 AIDL 接口,然后通过这个接口就可以去调用服务端的远程方法了,代码如下:

        @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };


    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(mConnection);
    }

绑定成功后,会通过 bookManager 去调用 getBookList 的方法,然后打印出所获取的图书信息。需要注意的是,服务端的方法可能需要很久才能执行完毕,这个时候下面的代码可能会导致 ANR,这一点是值得注意的。后面会介绍这种情况。运行之后,log 如下:

09-13 15:24:56.269 16271-16271/com.demo.text.demotext I/MainActivity: query book list, list types:java.util.ArrayList
09-13 15:24:56.270 16271-16271/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS]]

可以发现,虽然我们在服务端返回的是 CopyOnWriteArrayList类型,但是客户端收到的仍然是 ArrayList 类型,这也证实了我们在前面做的分析。第二行 log 表明客户端成功得到了服务端的图书信息。

我们接着在调用一下另外一个方法 addBook,在客户端给服务端添加一本书,然后在获取一次。堪称秀是否能正常工作。还是上面的代码,客户端在连接后,修改如下:

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.addBook(new Book(3, "java"));
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

然后我们看下 log,很显然,我们成功添加了一本书。

09-13 15:33:22.103 17441-17441/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS], [bookId:3, bookName:java]]

现在考虑一种情况,假设有一种需求,用户不想去时不时的查询图书列表了,太累了,于是,他去问图书馆,“当有新书的时候能不能把书的信息告诉我呢?”。大家应该明白,这是一种典型的观察者模式,每个感兴趣的用户都观察新书,图书馆就知道每一个对图书感兴趣的用户,这种模式在开发中使用很多,下面我们就模拟这种情形。首先,我们需要提供一个 AIDL 接口,每个用户都要实现这个接口并且向图书馆申请新书的提醒,当然用户也可以随时取消这种提醒。之所以选择 AIDL 接口而不是普通接口,是因为 AIDL 中无法使用普通接口,这里我们创建一个 IOnNewBookArrivedListener.aidl 文件,我们所期望的情况是:当服务端有新书到来时,就会通知每一个已经申请提醒功能的用户。从程序上来说就是调用所有 IOnNewBookArrivedListener 对象中的 onNewBookArrived 方法,并把新书的对象通过参数传递给客户端,如下所示:

// IOnNewBookArrivedListener.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;

interface IOnNewBookArrivedListener {
    void onNewBookArrived(in Book newBook);
}

除了新建一个 AIDL 接口,我们还需要在原来的接口中添加两个新方法,一个用来监听,一个用来解除监听,代码如下:

// IBookManager.aidl
package com.demo.text.demotext.aidl;

import com.demo.text.demotext.aidl.Book;
import com.demo.text.demotext.aidl.IOnNewBookArrivedListener;

interface IBookManager {

     List<Book> getBookList();
     void addBook(in Book book);

     void registerListener(IOnNewBookArrivedListener listener);
     void unRegisterListener(IOnNewBookArrivedListener listener);

 }

接着,服务端的 Service 的实现也需要修改一下,主要是 Service 中 IBookManager.Stub 的实现,因为我们在 IBookManager 中新加了两个方法,所以 IBookManager.Stub 中也要实现这两个方法。同时,在 BookManagerService 中还开启一个线程,每隔 5s 就像图库中添加一本新书并通知感兴趣的用户,注意,我们这里使用的是 AtomicBoolean,而不是使用的boolean,至于为什么,读者可以自行百度,这里就不解释了。代码如下:

ublic class BookManagerService extends Service {

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(true);
    private static final String TAG = "BMS";
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();
    private CopyOnWriteArrayList<IOnNewBookArrivedListener> mListenerList = new CopyOnWriteArrayList<>();


    private Binder binder = 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 {
            if(!mListenerList.contains(listener)) {
                mListenerList.add(listener);
            } else {
                Log.i(TAG, "already exists.");
            }
            Log.i(TAG, "registerListener size:" + mListenerList.size());
        }

        @Override
        public void unRegisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            if(mListenerList.contains(listener)) {
                mListenerList.remove(listener);
                Log.i(TAG, "unregister listener succeed.");
            } else {
                Log.i(TAG, "not found,can not unregister");
            }
            Log.i(TAG, "unregisterListener current size:" + mListenerList.size());
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "Andrroid"));
        mBookList.add(new Book(2, "IOS"));
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (mIsServiceDestoryed.get()) {
                        Thread.sleep(5000);
                        onNewBookArrived(new Book(mBookList.size() + 1, "newBookName#" + (mBookList.size() + 1)));
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    private void onNewBookArrived(Book book){
        mBookList.add(book);
        for (int i = 0; i < mListenerList.size(); i++) {
            try {
                mListenerList.get(i).onNewBookArrived(book);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        mIsServiceDestoryed.set(false);
    }
}

最后,我们还需要修改一下客户端的代码,主要有两个方面,首先客户端要注册 IOnNewBookArrivedListener 到远程服务端,这样当有新书时服务端才能通知当前客户端,同时我们要在 Activity 退出时解除这个注册;另一方面,当有新书时,服务端会回调客户端的 IOnNewBookArrivedListener 对象中的 onNewBookArrived 方法,但是这个方法实在客户端的 Binder 线程池中执行的,因此,为了便于 UI 操作,我们需要一个 Handler 可以将其切换到客户端的主线程中去执行,这个原理在 Binder 中已经做了分析,这里就不多说了,客户端修改如下:

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindService(new Intent(this, BookManagerService.class), mConnection, Context.BIND_AUTO_CREATE);
    }

    private IBookManager mIBookManager;

    ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager bookManager = IBookManager.Stub.asInterface(service);
            mIBookManager = bookManager;
            try {
                Log.i(TAG, "query book list, list types:" + bookManager.getBookList().getClass().getCanonicalName());
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.addBook(new Book(3, "java"));
                Log.i(TAG, "query book list:" + bookManager.getBookList().toString());
                bookManager.registerListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            mOnNewBookArrivedListener = null;
        }
    };

    private IOnNewBookArrivedListener mOnNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            Message msg = new Message();
            msg.what = 1;
            msg.obj = newBook;
            handler.sendMessage(msg);
        }
    };

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if(msg.what == 1) {
                Log.i(TAG, "receive new book:" + msg.obj);
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if(mIBookManager != null && mIBookManager.asBinder().isBinderAlive()) {
            Log.i(TAG, "uunregister listener:" + mOnNewBookArrivedListener);
            try {
                mIBookManager.unRegisterListener(mOnNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(mConnection);
    }
}

运行程序,我们来看下结果,如下所示:

09-13 16:33:14.459 25712-25712/com.demo.text.demotext I/MainActivity: query book list, list types:java.util.ArrayList
09-13 16:33:14.459 25712-25712/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS]]
09-13 16:33:14.460 25712-25712/com.demo.text.demotext I/MainActivity: query book list:[[bookId:1, bookName:Andrroid], [bookId:2, bookName:IOS], [bookId:3, bookName:java]]
09-13 16:33:19.464 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:4, bookName:newBookName#4]
09-13 16:33:24.462 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:5, bookName:newBookName#5]
09-13 16:33:29.465 25712-25712/com.demo.text.demotext I/MainActivity: receive new book:[bookId:6, bookName:newBookName#6]

如果你以为到这里 AIDL 的介绍就结束了,那你就错了,之前说了,AIDL 远不止这么简单,目前还有一些难点使我们没有涉及到的,接下来我们继续研究。

从上面的代码可以看出,当 MainActivity 关闭时,我们会在 onDestory 中去解除已经注册服务端的 listener,这就相当于我们不想再接收图书馆的新书提醒了,所以我们可以随时结束这个提醒,按 Back 按键退出 MainActivity,下面看下 log:

09-13 16:33:33.457 25712-25712/com.demo.text.demotext I/MainActivity: uunregister listener:com.demo.text.demotext.MainActivity$5@df2ed17
09-13 16:33:33.457 25728-25740/com.demo.text.demotext:remote I/BMS: not found,can not unregister
09-13 16:33:33.457 25728-25740/com.demo.text.demotext:remote I/BMS: unregisterListener current size:1

从上面的 log 可以看出,程序没有像我们所预期的那样执行。在解除注册的过程中,服务端镜无法找到我们之前注册的那个 listener,在客户端我们注册和解除注册时,明明传递的是同一个 listener 啊!最终,服务端由于无法找到要接触的 listener 而宣告解除注册失败!这当然不是我们想要的结果,但仔细想想,好像这种方式确实无法解除注册。其实,这是必然的,这种解除注册的处理方式在日常开发中时常用到的,但是放到多进程中却无法奏效,因为 Binder 会把客户端传递过来的对象重新转换成一个新的对象。虽然我们在注册和解除注册过程中使用的是同一个客户端对象,但是通过 Binder 传递到服务器后,却产生了两个全新的对象。别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,这就是为什么 AIDL 中的自定义对象都必须要实现 Parcelable 接口的原因。不过没关系,接下来我们来解决这个问题。

RemoteCallbackList 是系统专门提供的用于删除跨进程 listener 的接口,RemoteCallBackList 是一个泛型,支持管理任意的 AIDL 接口,这点从它的声明就可以看出,因为所有的 AIDL 接口都继承自 IInterface 接口,

public class RemoteCallbackList<E extends IInterface> 

它的工作原理很简单,在它的内部有一个 Map 结构专门用来保存所有 AIDL 的回调,这个 Map 的 key 是 IBinder 类型,value 是 Callback 类型,如下所示:

ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<IBinder, Callback>();

其中,Callback 中封装了真正的远程 listener,当客户端注册 listener 的时候,它会把这个 listener 信息存入 mCallbacks 中,其中 key 和 value 分别通过下面的方式获得:

        IBinder key = listener.asBinder();
        Callback value = new Callback(listener, cookie);

到这里,我们应该明白了,虽然说多次跨进程传输客户端的同一个对象会在服务端生成不同的对象,但是这些新生成的对象有一个共同点,那就是它们底层的 Binder 对象是同一个,利用这个特性,就可以实现上面我们无法实现的功能。当客户端接触注册的时候,我们只要遍历服务端所有的 listener,找出那个和解注册 listener 具有相同 Binder 对象的服务端 listener 把它删除即可,这就是 RemoteCallbackList 为我们所做的事情。同时,RemoteCallbackList 还有一个很有用的功能,就是当客户端进程终止后,它能够自动移除客户端所注册的 listener。另外,RemoteCallbackList 内部自动实现了线程同步功能,所以我们使用它来注册和解除注册时,不需要做额外的线程同步工作。

RemoteCallbackList  使用起来很简单,我们要对 BookManagerService 做一些修改,首先创建一个 RemoteCallbackList  对象来替代 CopyOnWriteArrayList,如下所示:

    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<>();

然后修改 registerListener 和 unRegisterListener 这两个接口的实现,如下所示:

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.register(listener);
            Log.i(TAG, "registerListener size:" + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();
        }

        @Override
        public void unRegisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            boolean success = mListenerList.unregister(listener);
            if (success) {
                Log.i(TAG, "unregister success.");
            } else {
                Log.i(TAG, "not found, can not unregister.");
            }
            Log.i(TAG, "unregisterListener current size:" + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();
        }

接着修改 onNewBookArrived 方法,当有新书时,我们要通知所有已经注册的 listener,如下所示:

 private void onNewBookArrived(Book book){
        mBookList.add(book);
        for (int i = 0; i < mListenerList.beginBroadcast(); i++) {
            try {
                if(null !=  mListenerList.getBroadcastItem(i)) {
                    mListenerList.getBroadcastItem(i).onNewBookArrived(book);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        mListenerList.finishBroadcast();
    }

BookManagerService 的修改已经完毕了,我们接下来运行一下程序,看看是否完成我们想要的功能:

09-13 18:02:26.201 6696-6707/com.demo.text.demotext:remote I/BMS: unregister success.
09-13 18:02:26.201 6696-6707/com.demo.text.demotext:remote I/BMS: unregisterListener current size:0

使用 RemoteCallbackList 有一点需要注意,我们无法像操作 List 一样去操作它,尽管它的名字也带一个 List,但是它并不是一个 List。遍历 RemoteCallbackList ,必须要按照下面的方式进行,其中 beginBroadcast 和 finishBroadcast 必须要配对使用,哪怕我们仅仅是像获取 RemoteCallbackList 中的元素个数,这是必须注意的地方,否则将会报错:java.lang.IllegalStateException: beginBroadcast() called while already in a broadcast。

到这里,AIDL 的使用方法我们已经介绍完了,但是有几点好需要在说明一下,我们知道,客户端调用远程服务的方法,被调用的方法运行在服务端的 Binder 线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间阻塞在这里,而如果这个客户端线程是 UI 线程的话,就会导致客户端 ANR,这当然不是我们想看到的。因此,如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的 UI 线程中去访问远程方法发。由于客户端的 onServiceConnected 和 onServiceDisconnected 方法都运行在 UI 线程,所以不可以在它们里边直接调用服务端的耗时方法,这点需要注意。另外,由于服务端的方法本身就运行在服务端的 Binder 线程池中,所以服务端方法本身就可以执行大量耗时操作,这个时候切记不要在服务端方法中开启线程去进行异步操作,除非你明确知道自己在干什么,否则不建议这么做。

同理,当远程服务端调用客户端的 listener 中的方法时,被调用的方法运行在 Binder 线程池中,只不过是客户端的线程池。所以,我们同样不可以在客户端中调用客户端的耗时方法,比如针对 BookManagerService 的 OnNewBookArrived 方法,如下所示,在它内部调用了客户端的 IOnNewBookArrivedListener 的 onNewBookArrived 方法,如果客户端的 onNewBookArrived 方法比较耗时的话,那么请确保 BookManagerService 中的 onNewBookArrived 运行在非 UI 线程中,否则将导致服务端无响应。

    private void onNewBookArrived(Book book){
        mBookList.add(book);
        for (int i = 0; i < mListenerList.beginBroadcast(); i++) {
            try {
                if(null !=  mListenerList.getBroadcastItem(i)) {
                    mListenerList.getBroadcastItem(i).onNewBookArrived(book);
                }
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        mListenerList.finishBroadcast();
    }

另外,由于客户端的 IOnNewBookArrivedListener 中的 onNewBookArrived 方法运行在客户端的 Binder 线程池中,所以不能在它里面去访问 UI 相关的内容,如果要访问 UI,请使用 Handler 切换到 UI 线程。

为了程序的健壮性,我们还需要做一件事,Binder 是可能意外死亡的,这往往是由于服务端进程意外停止了,这时我们需要重新连接服务。有两种方法:

(1)是给 Binder 设置 DeathRecipient 监听,当 Binder 死亡时,我们会收到 binderDied 方法的回调,在 binderDied 方法中我们可以重新连接远程服务,具体在前面已经说过了。请查看 Binder 概念详解

(2)在 onServiceDisconnected 中重新远程服务。

上述两种方法我们可以随意采用一种来使用,它们的区别在于:onServiceDisconnected 在客户端的 UI 线程中被回调,而 binderDied 在客户端的 Binder 线程池中被回调。也就是说,在 binderDied 方法中我们不能访问 UI。这就是它们的区别。

2. 使用 AIDL 进行权限验证

经过上面的分析,我们已经对 AIDL 有了一个系统性的认识,但是还差最有后一步,如何在 AIDL 中使用权限验证功能,默认情况下,我们的远程服务任何客户端都可以连接,但这应该不是我们愿意看到的,所以必须给服务加入权限验证功能。权限验证失败则服务调用府服务中的方法,在 AIDL 中进行权限验证,这里介绍两助攻方法。

第一种方法,我们可以在 onBind 中进行验证,验证不通过直接返回 null,这样验证失败的客户端直接无法绑定服务。至于验证方式有很多种,比如使用 permission 验证,使用这种验证方式,我们需要现在 AndroidManifest 中声明权限,比如:

    <permission
        android:name="com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE"
        android:protectionLevel="normal" />

关于 permission 的定义这里就不做详细解释了,毕竟这里主要介绍的是 AIDL,定义了权限以后,就可以在 BookManagerService 的 onBind 方法中进行验证了。代码如下:

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        int check = checkCallingOrSelfPermission("com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE");
        if(check == PackageManager.PERMISSION_DENIED) {
            return null;
        }
        return binder;
    }

一个应用来绑定我们的服务时,会验证这个应用的权限,如果他没有使用这个权限,onBind 方法则会直接返回 null,最终结果就是这个应用无法绑定到我们的服务, 这样就达到了权限验证的效果,这种方法同样适用于 Messenger 中。

如果我们自己内部的应用想绑定到我们的服务中,

    <uses-permission android:name="com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE"/>

第二种方法,我们可以在服务端的 onTransact 方法中进行权限验证。如果验证失败就直接返回 false,这样服务端就不会终止执行 AIDL 中的方法从而达到保护服务端的效果。至于具体的验证方式有很多,可以采用 permission 验证,具体实现方式和第一种方法一样,还可以采用 Uid 和 Pid 来做验证。通过 getCallingUid 和 getCallingPid 可以拿到客户端所属应用的 Uid 和 Pid,通过这两个参数我们可以做一些工作,比如验证包名。在下面的代码中,即验证了 permission,又验证了包名。一个应用如果想远程调用服务中的方法,首先要使用我们的自定义权限“com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE”,其次包名必须以“com,demo”开始,否则调用服务端的方法会失败。

        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            int check = checkCallingOrSelfPermission("com.demo.text.demotext.permission.ACCESS_BOOK_SERVICE");
            if (check == PackageManager.PERMISSION_DENIED) {
                return false;
            }
            String packageName = null;
            String[] packages = getPackageManager().getPackagesForUid(getCallingUid());
            if (null != packages && packages.length > 0) {
                //循环判断,偷个懒
                packageName = packages[0];
            }
            if (!packageName.startsWith("com.demo")) {
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

上面介绍了两种 AIDL 中常用的权限验证方法,但是肯定还有其他的方法做权限验证,比如在 Service 指定 android:permission 属性等,这里就不一一进行介绍了。

猜你喜欢

转载自blog.csdn.net/sinat_29874521/article/details/82664017