进程间通信的方式(2)

2020-07-23 16:04:22 浏览数 (2)

这篇讲第四种IPC方式--AIDL,内容较多所以单独成篇。

四、使用AIDL

Messenger是以串行的方式处理客户端发来的消息,如果大量的消息同时发送到服务端,服务端仍然只能一个个处理,如果有大量的并发请求,那么用Messenger就不太合适了。同时,Messenger的作用主要是为了传递消息,很多时候我们可能需要跨进程调用服务端的方法,这种情形用Messenger就无法做到了,但是我们可以使用AIDL来实现跨进程的方法调用。

AIDL是Messenger的底层实现,因此Messenger本质上也是AIDL,只不过系统为我们做了封装,从而方便上层的调用而已。前面介绍了Binder的概念,在Binder的基础上我们可以更加容易地理解AIDL。这里先介绍使用AIDL 来进行进程间通信的流程,分为服务端和客户端两个方面。

1、AIDL的使用细节,《Android开发艺术探索》中的介绍很旧了,最好参考这里:

https://blog.csdn.net/luoyanglizi/article/details/51980630

看过上面的文章,我们已经会使用AIDL了。

2、下面继续《艺术探索》中关于AIDL的进阶知识:

1)进程间的Listener

假设有一种需求:用户不想时不时地去查询图书列表了,太累了,于是,他去问图书馆,“当有新书时能不能把书的信息告诉我呢?”。大家应该明白了,这就是一种典型的观察者模式,每个感兴趣的用户都观察新书,当新书到的时候,图书馆就通知每一个对这本书感兴趣的用户,这种模式在实际开发中用得很多,下面我们就来模拟这种情形。

首先,我们需要提供一个AIDL接口,每个用户都需要实现这个接口并且向图书馆申请新书的提醒功能,当然用户也可以随时取消这种提醒。之所以选择AIDL接口而不是普通接口,是因为AIDL中无法使用普通接口。这里我们创建一个IOnNewBookArrivedListener.aidl文件,我们所期望的情况是:当服务端有新书到来时,就会通知每一个己经申请提醒功能的用户。从程序上来说就是调用所有IOnNewBookArivedListener对象中的onNewBookArived方法,并把新书的对象通过参数传递给客户端,内容如下所示。

代码语言:javascript复制
// IOnNewBookArrivedListener.aidl
package com.liuguilin.ipcsample;
import com.liuguilin.ipcsample.Book;

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

除了要新增加一个AIDL外,还需要在原有的接口中添加两个新的方法:

代码语言:javascript复制
// 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向感兴趣的用户提醒:

代码语言:javascript复制
public class BookManagerService extends Service {

    private static final String TAG = "BookManagerService";

    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<>();

    private AtomicBoolean mIsServiceDestoryed = new AtomicBoolean(false);

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

    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 {
            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,"remove listener succeed");
                }else {
                    Log.i(TAG,"not found, can not remove listener");
                }
            Log.i(TAG,"unregisterListener size:"   mListenerList.size());
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();

        mBookList.add(new Book(1,"Android"));
        mBookList.add(new Book(1,"IOS"));
        new Thread(new ServiceWorker()).start();
    }

    @Override
    public IBinder onBind(Intent intent) {

        return mBinder;
    }

    @Override
    public void onDestroy() {
        mIsServiceDestoryed.set(true);
        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);
        Log.i(TAG,"onNewBookArrived size:"   mListenerList.size());
        for (int i = 0; i < mListenerList.size(); i  ) {
            IOnNewBookArrivedListener listener = mListenerList.get(i);
            Log.i(TAG,"listener: "  listener);
            listener.onNewBookArrived(book);
        }
    }
}

修改一下客户端的代码,主要是两方面:

1、客户端要注册IOnNewBookArrivedListener到远程的服务器,这样当有新书时服务端才能通知客户端,同时在我们的Activity的onDestory方法里面去取消绑定; 2、另一方面,当有新书的时候,服务端会回调客户端的OnNewBookArrived方法,这个方法是在客户端的Binder线程池中执行,因此,为了便于进行UI操作,我们需要有一个Handler可以将其切换到客户端的主线程去执行(Binder方法都是在Bind线程池中执行的)。

把代码贴上:

代码语言:javascript复制
public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final int MESSAGE_WHAT_NEW_BOOK_ARRIVED = 1;
    private IBookManager iBookManager;
    private boolean isBookManagerServiceConnected;
    private ServiceConnection connection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initView();
    }
    private void initView() {
        //略
    }

    @Override
    public void onClick(View v) {
        //略
    }

    /**
     * 调用远程服务:getBookList
     */
    private void getBookList() {
        if (!isBookManagerServiceConnected
                || iBookManager == null) {
            bindBookManageService();
        }

        if (iBookManager == null) {
            return;
        }

        //如果是耗时操作,则要在子线程进行
        new Thread(new Runnable() {
            @Override
            public void run() {
                List<Book> booklist = null;
                try {
                    booklist = iBookManager.getBooklist();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                if (booklist != null) {
                    for (Book book : booklist) {
                        Log.i(TAG, "getBookList: "   book.toString());
                    }
                }
            }
        }).start();

    }

    /**
     * 调用远程服务:addBook
     */
    private void addBook() {
        if (!isBookManagerServiceConnected
                || iBookManager == null) {
            bindBookManageService();
        }

        if (iBookManager == null) {
            return;
        }

        Book book = new Book("《Android》");
        try {
            iBookManager.addBook(book);
            Log.i(TAG, "addBook: "   book.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 绑定服务
     */
    private void bindBookManageService() {

        if (isBookManagerServiceConnected) {
            Toast.makeText(this, "isBookManagerServiceConnected : true", Toast.LENGTH_SHORT).show();
            return;
        }

        Intent intent = new Intent();
//        intent.setAction("BOOK_MANAGE_SERVICE");
        intent.setPackage("simplenet.hfy.com.demoapplication2");

        connection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {

                iBookManager = IBookManager.Stub.asInterface(service);
                isBookManagerServiceConnected = true;

                try {
                    //注册新书到达监听
                    iBookManager.registerOnNewBookArrivedListener(onNewBookArrivedListener);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }

                Log.i(TAG, "onServiceConnected: ");
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.i(TAG, "onServiceDisconnected: ");
                isBookManagerServiceConnected = false;
            }
        };

        bindService(intent, connection, BIND_AUTO_CREATE);
    }


    IOnNewBookArrivedListener onNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {

        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {

            //由于Binder的方法是在Binder线程池运行,所以 为了便于UI操作,此方法切到主线程执行
            Message message = Message.obtain();
            message.what = MESSAGE_WHAT_NEW_BOOK_ARRIVED;
            message.obj = newBook;
            uiHandler.sendMessage(message);
        }
    };

    //UIHandler
    @SuppressLint("HandlerLeak")
    Handler uiHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_WHAT_NEW_BOOK_ARRIVED:

                    Book newBook = (Book) msg.obj;

                    Log.i(TAG, "handleMessage: newBook:"   newBook.toString());
                    Toast.makeText(MainActivity.this,
                            "handleMessage: newBook:"   newBook.toString(), Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (isBookManagerServiceConnected) {
            unbindService(connection);
            isBookManagerServiceConnected = false;

            try {
                //解注册,此处可能会有问题,因为onNewBookArrivedListener传到服务端就不是注册时传过去的那个对象了(反序列化的结果)
                iBookManager.unregisterOnNewBookArrivedListener(onNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }
    }
}onNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {

        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {

            //由于Binder的方法是在Binder线程池运行,所以 为了便于UI操作,此方法切到主线程执行
            Message message = Message.obtain();
            message.what = MESSAGE_WHAT_NEW_BOOK_ARRIVED;
            message.obj = newBook;
            uiHandler.sendMessage(message);
        }
    };

    //UIHandler
    @SuppressLint("HandlerLeak")
    Handler uiHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_WHAT_NEW_BOOK_ARRIVED:

                    Book newBook = (Book) msg.obj;

                    Log.i(TAG, "handleMessage: newBook:"   newBook.toString());
                    Toast.makeText(MainActivity.this,
                            "handleMessage: newBook:"   newBook.toString(), Toast.LENGTH_SHORT).show();
                    break;
                default:
                    break;
            }
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (isBookManagerServiceConnected) {
            unbindService(connection);
            isBookManagerServiceConnected = false;

            try {
                //解注册,此处可能会有问题,因为onNewBookArrivedListener传到服务端就不是注册时传过去的那个对象了(反序列化的结果)
                iBookManager.unregisterOnNewBookArrivedListener(onNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }

        }
    }
}

最后我们如果运行的话,不难发现,我们的每隔5s新书推送是成功的。

2)RemoteCallbackList

如果你以为AIDL就这样结束了,那你就错了,AIDL远不止这么简单,目前我们还有一些难点还没有涉及。

从上面的代码我们可以看出,当BookManagerActivity关闭时,我们会在onDestory中去接触已经注册的服务端的listener,这就相当于我们不想再接收图书馆的新书提醒,所以我们可以随时取消这个提醒服务。按Back键退出BookManagerActivity,发现如下日志:

代码语言:javascript复制
not found, can not remove listener

从上log可以看出,在解注册的过程中,服务端竟然无法找到我们之前注册的那个listener,在客户端我们注册和解注册时明明传的是同一个listener啊!其实,这是必然的,这种解注册的处理方式在日常开发过程中时常使用到,但是放到多进程中却无法奏效,因为Binder会把客户端传递过来的对象重新转化并生成一个新的对象。虽然在注册和解注册过程中使用的是同一个客户端对象,但是通过Binder传递到服务端后会产生两个全新的对象。别忘了对象是不能跨进程直接传输的,对象的跨进程传输本质上都是反序列化的过程,这就是为什么AIDL中的自定义对象都必须要实现Parcelable接口的原因。

那么我们要怎么做才能实现解注册的功能?答案是用RemoteCallbackList,这看起来很抽象,不过没关系,请看接下来的分析;

RemoteCallbackList是系统专门用来删除listener的接口,RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,这点从他的声明就可以看出:

代码语言:javascript复制
public class RemoteCallbackList<E extends IInterface>

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

代码语言:javascript复制
ArrayMap<IBinder,Callback> mCallbacks = new ArrayMap<IBinder,Callback>();

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

代码语言:javascript复制
IBinder key = listener.asBinder();
Callback value = new Callback(listener,cookie);

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

RemoteCallbackList使用起来很很简单,我们要对服务端BookManagerService做一些修改,首先我们创建一个RemoteCallbackList对象来替代之前的CopyonWriteArrayList

代码语言:javascript复制
private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();

然后修改registerListener 和unregisterListener这两个接口的实现:

代码语言:javascript复制
  @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.registener(listener);
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            mListenerList.unregistener(listener);
        }

怎么样,使用起来是不是很简单,接下来我们修改onNewBookArrived方法,当有新书的时候,我们就要通知所有已注册的listener:

代码语言:javascript复制
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.getBroadcast(i);
            if(i != null){
                l.onNewBookArrived(book);
            }
        }
    //注意,要和beginBroadCast()搭配使用
     mListenerList.finishBroadcast();
    }

好了,RemoteCallbackList的使用介绍完毕。

3)AIDL使用的几点说明,重要!

到这里,AIDL的基本使用方法已经介绍完了,但是有几点还需要再次说明一下。

我们知道,客户端调用远程服务的方法,被调用的方法运行在服务端的Binder线程池中,同时客户端线程会被挂起,这个时候如果服务端方法执行比较耗时,就会导致客户端线程长时间地阻塞在这里,而如果这个客户端线程是UI线程的话,就会导致客户端ANR,这当然不是我们想要看到的。因此,

1、如果我们明确知道某个远程方法是耗时的,那么就要避免在客户端的UI线程中去访问远程方法。由于客户端的onServiceConnected和 onServiceDisconnected方法都运行在UI线程中,所以也不可以在它们里面直接调用服务端的耗时方法,这点要尤其注意。

2、由于服务端的方法本身就运行在服务端的Binder线程池中,所以服务端方法本身就可以执行大量耗时操作,这个时候切记不要在服务端方法中开线程执行异步任务,除非你明确知道自己在干什么,否则不建议这么做。

下面我们稍微改造一下服务端getBookList,我们让他耗时:

代码语言:javascript复制
    @Override
        public List<Book> getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return mBookList;
        }

多来几下就ANR了!避免出现ANR很简单,把调用放在非UI线程即可。

3、同理,当远程服务端需要调用 客户端的listener中的方法试,被调用的方法也运行在Binder线程池中,只不过是客户端的线程池。所以,我们同样不可以在服务端中调用客户端的耗时方法。比如BookManagerService中的onNewBookArrived(Book book) ,内部调用了IOnNewBookArrivedListener中的OnNewBookArrived方法,如果客户端的这个方法是耗时的,那么要保证服务端的onNewBookArrived(Book book)运行在非UI线程中。否则导致服务端无响应。

代码语言:javascript复制
    private void onNewBookArrived(Book book) throws RemoteException{
        mBookList.add(book);
        Log.i(TAG,"onNewBookArrived size:"   mListenerList.size());
        for (int i = 0; i < mListenerList.size(); i  ) {
            IOnNewBookArrivedListener listener = mListenerList.get(i);
            Log.i(TAG,"listener: "  listener);
            listener.onNewBookArrived(book);
        }
    }

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

5、Binder可能意外死亡(可能服务端意外停止),需要重新连接,两种方法:

  1. 客户端绑定远程服务成功之后,给binder设置死亡代理,前面介绍Binder时已说明。(Binder线程池中)
  2. onServiceDisconnected中重连服务。(UI线程中)

6、给服务加权限验证功能,验证失败则无法调用服务

a、在onBinder中验证,验证不通过则返回null,客户端就无法绑定服务。比如可以使用permission验证:

服务端在AndroidMenifest中声明所需的权限:

代码语言:javascript复制
<pemission android:name="permission_aidl"/

然后在onBind中做权限验证:

代码语言:javascript复制
 @Override
    public IBinder onBind(Intent intent) {

        int check = checkCallingOrSelfPermission("permission_aidl");
        if (check == PackageManager.PERMISSION_DENIED)
        {
            return null;
        }
        String action = intent.getAction();
        Log.i(TAG, "onBind: action "   action);
        return new BookManagerBinder();
    }

客户端需要:(或者代码中设置)

代码语言:javascript复制
<uses-permission android:name="permission_aidl"/>

如果客户端没有使用这个权限,就无法绑定服务。

b、在onTransact中验证权限,如果验失败就返回false。这样服务端就不会执行AIDL中的方法了。

具体验证也可以使用permission,或者验证包名。

代码语言:javascript复制
        @Override
        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            String[] packagesForUid = getPackageManager().getPackagesForUid(getCallingUid());
            String packageName = null;
            if (packagesForUid != null && packagesForUid.length > 0) {
                packageName = packagesForUid[0];
            }
            if (!"com.hfy".equals(packageName))
            {
                return false;
            }
            return super.onTransact(code, data, reply, flags);
        }

例外,还可以验签的问题要注意。

0 人点赞