Android进阶

IPC机制之AIDL的使用与原理

2017-04-19  本文已影响395人  小鱼爱记录

AIDL概述

AIDL是一个用于快速创建Binder的工具,没有AIDL文件也可以自己写Binder文件。

Binder的工作机制

各种IPC方式的优缺点和适用场景

需求目标

  1. 客户端向服务端添加一本书,获取图书列表
  2. 服务提供注册和解除注册监听新书服务,并且每5秒添加一本书
  3. 客户端注册监听新书服务,同时将新书打印出来

服务端

  1. 首先,创建一个Service用来监听客户端的连接请求
  2. 然后,创建一个AIDL文件,将暴露给客户端的接口在这个AIDL文件中声明
  3. 最后,在Service中实现这个AIDL接口即可
package com.soubu.ipcdemo.aidl;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.support.annotation.Nullable;

import com.soubu.ipcdemo.LogUtil;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * 作者:余天然 on 2017/4/19 上午10:45
 */
public class BookManagerService extends Service {

    private AtomicBoolean isServiceDestoryed = new AtomicBoolean(false);

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

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

    private Binder binder = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            SystemClock.sleep(5000);
            return books;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            books.add(book);
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
            listeners.register(listener);
            LogUtil.print("listener:" + listener);
        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
            listeners.unregister(listener);
            LogUtil.print("listener:" + listener);
        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        books.add(new Book(1, "Androird"));
        books.add(new Book(2, "ios"));
        new Thread(new ServiceWorker()).start();
    }

    private class ServiceWorker implements Runnable {

        @Override
        public void run() {
            while (!isServiceDestoryed.get()) {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int bookId = books.size() + 1;
                Book newBook = new Book(bookId, "newBook#" + bookId);
                try {
                    onNewBookArrived(newBook);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private void onNewBookArrived(Book newBook) throws RemoteException {
        books.add(newBook);
        final int N = listeners.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener listener = listeners.getBroadcastItem(i);
            if (listener != null) {
                listener.onNewBookArriced(newBook);
            }
        }
        listeners.finishBroadcast();
    }

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

}

客户端

  1. 首先,绑定服务端的Service,绑定成功后,将服务端返回的Binder对象转成AIDL接口所属的类型
  2. 接着就可以调用AIDL中的方法
package com.soubu.ipcdemo.aidl;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.TextView;

import com.soubu.ipcdemo.LogUtil;
import com.soubu.ipcdemo.R;

import java.util.List;

/**
 * 作者:余天然 on 2017/4/19 上午10:51
 */
public class BookManagerActivity extends AppCompatActivity {

    private static final int MESSAGE_NEW_BOOK_ARRIVED = 1;

    private IBookManager bookManager;

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MESSAGE_NEW_BOOK_ARRIVED:
                    LogUtil.print("received new book :" + msg.obj);
                    break;
                default:
                    super.handleMessage(msg);
            }
        }
    };

    private IOnNewBookArrivedListener onNewBookArrivedListener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArriced(Book newBook) throws RemoteException {
            handler.obtainMessage(MESSAGE_NEW_BOOK_ARRIVED, newBook).sendToTarget();
        }
    };

    private IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
        @Override
        public void binderDied() {
            if (bookManager == null) {
                return;
            }
            bookManager.asBinder().unlinkToDeath(deathRecipient, 0);
            bookManager = null;
            bindBookManagerService();
        }
    };

    private void bindBookManagerService() {
        Intent intent = new Intent(this, BookManagerService.class);
        bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            bookManager = IBookManager.Stub.asInterface(service);
            try {
                service.linkToDeath(deathRecipient, 0);
                bookManager.registerListener(onNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = (TextView) findViewById(R.id.tv_send);
        tv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    invocateRemoteMethod();
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        });
        bindBookManagerService();
    }

    private void invocateRemoteMethod() throws RemoteException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                if (bookManager != null) {
                    List<Book> list = null;
                    try {
                        list = bookManager.getBookList();
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    LogUtil.print("query book list :" + list.getClass().getCanonicalName());
                    LogUtil.print("query book list :" + list.toString());
                }

            }
        }).start();

    }

    @Override
    protected void onDestroy() {
        if (bookManager != null && bookManager.asBinder().isBinderAlive()) {
            try {
                bookManager.unregisterListener(onNewBookArrivedListener);
                LogUtil.print("unregister listener:" + onNewBookArrivedListener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        unbindService(connection);
        super.onDestroy();
    }
}

AIDL使用的注意点

  1. AIDL支持的数据类型:基本数据类型、String和CharSequence、ArrayList、HashMap、Parcelable以及AIDL;
  2. 某些类即使和AIDL文件在同一个包中也要显式import进来;
  3. AIDL中除了基本数据类,其他类型的参数都要标上方向:in、out或者inout;
  4. AIDL接口中支持方法,不支持声明静态变量;
  5. 为了方便AIDL的开发,建议把所有和AIDL相关的类和文件全部放入同一个包中,这样做的好处是,当客户端是另一个应用的时候,可以直接把整个包复制到客户端工程中。
  6. AIDL方法是在服务端的Binder线程池中执行的,因此当多个客户端同时连接的时候,会存在多个线程同时访问的情形,所以要在AIDL方法中处理线程同步。
  7. RemoteCallbackList是系统专门提供的用于删除跨进程Listener的接口。RemoteCallbackList是一个泛型,支持管理任意的AIDL接口,因为所有的AIDL接口都继承自IInterface接口。
  8. 客户端调用远程服务方法时,因为远程方法运行在服务端的binder线程池中,同时客户端线程会被挂起,所以如果该方法过于耗时,而客户端又是UI线程,会导致ANR,所以当确认该远程方法是耗时操作时,应避免客户端在UI线程中调用该方法。同理,当服务器调用客户端的listener方法时,该方法也运行在客户端的binder线程池中,所以如果该方法也是耗时操作,请确认运行在服务端的非UI线程中。另外,因为客户端的回调listener运行在binder线程池中,所以更新UI需要用到handler。
  9. 客户端通过IBinder.DeathRecipient来监听Binder死亡,也可以在onServiceDisconnected中监听并重连服务端。区别在于前者是在binder线程池中,访问UI需要用Handler,后者则是UI线程。
  10. AIDL可通过自定义权限在onBind或者onTransact中进行权限验证。

测试

我们可以看到Service是在Binder_2线程,每隔5秒,客户端确实会收到服务端的通知。


当我们退出客户端时,确实会解除绑定。


AIDL实际生成的代码

我们创建的AIDL文件如下:


Book.aidl

package com.soubu.ipcdemo.aidl;

parcelable Book;

IBookManager.aidl

package com.soubu.ipcdemo.aidl;

import com.soubu.ipcdemo.aidl.Book;
import com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener;

interface IBookManager {
    List<Book> getBookList();
    void addBook(in Book book);
    void registerListener(IOnNewBookArrivedListener listener);
    void unregisterListener(IOnNewBookArrivedListener listener);
}

IOnNewBookArrivedListener.aidl

package com.soubu.ipcdemo.aidl;

import com.soubu.ipcdemo.aidl.Book;

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

然后,AIDL帮我们生成的代码如下:


IBookManager.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.soubu.ipcdemo.aidl;

import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import com.soubu.ipcdemo.aidl.Book;
import com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener;
import java.util.ArrayList;
import java.util.List;

public interface IBookManager extends IInterface {
    List<Book> getBookList() throws RemoteException;

    void addBook(Book var1) throws RemoteException;

    void registerListener(IOnNewBookArrivedListener var1) throws RemoteException;

    void unregisterListener(IOnNewBookArrivedListener var1) throws RemoteException;

    public abstract static class Stub extends Binder implements IBookManager {
        private static final String DESCRIPTOR = "com.soubu.ipcdemo.aidl.IBookManager";
        static final int TRANSACTION_getBookList = 1;
        static final int TRANSACTION_addBook = 2;
        static final int TRANSACTION_registerListener = 3;
        static final int TRANSACTION_unregisterListener = 4;

        public Stub() {
            this.attachInterface(this, "com.soubu.ipcdemo.aidl.IBookManager");
        }

        public static IBookManager asInterface(IBinder obj) {
            if(obj == null) {
                return null;
            } else {
                IInterface iin = obj.queryLocalInterface("com.soubu.ipcdemo.aidl.IBookManager");
                return (IBookManager)(iin != null && iin instanceof IBookManager?(IBookManager)iin:new IBookManager.Stub.Proxy(obj));
            }
        }

        public IBinder asBinder() {
            return this;
        }

        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            IOnNewBookArrivedListener _arg0;
            switch(code) {
            case 1:
                data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
                List _arg02 = this.getBookList();
                reply.writeNoException();
                reply.writeTypedList(_arg02);
                return true;
            case 2:
                data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
                Book _arg01;
                if(0 != data.readInt()) {
                    _arg01 = (Book)Book.CREATOR.createFromParcel(data);
                } else {
                    _arg01 = null;
                }

                this.addBook(_arg01);
                reply.writeNoException();
                return true;
            case 3:
                data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
                _arg0 = com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
                this.registerListener(_arg0);
                reply.writeNoException();
                return true;
            case 4:
                data.enforceInterface("com.soubu.ipcdemo.aidl.IBookManager");
                _arg0 = com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener.Stub.asInterface(data.readStrongBinder());
                this.unregisterListener(_arg0);
                reply.writeNoException();
                return true;
            case 1598968902:
                reply.writeString("com.soubu.ipcdemo.aidl.IBookManager");
                return true;
            default:
                return super.onTransact(code, data, reply, flags);
            }
        }

        private static class Proxy implements IBookManager {
            private IBinder mRemote;

            Proxy(IBinder remote) {
                this.mRemote = remote;
            }

            public IBinder asBinder() {
                return this.mRemote;
            }

            public String getInterfaceDescriptor() {
                return "com.soubu.ipcdemo.aidl.IBookManager";
            }

            public List<Book> getBookList() throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                ArrayList _result;
                try {
                    _data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
                    this.mRemote.transact(1, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(Book.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

                return _result;
            }

            public void addBook(Book book) throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                try {
                    _data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
                    if(book != null) {
                        _data.writeInt(1);
                        book.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }

                    this.mRemote.transact(2, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

            }

            public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                try {
                    _data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
                    _data.writeStrongBinder(listener != null?listener.asBinder():null);
                    this.mRemote.transact(3, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

            }

            public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                try {
                    _data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IBookManager");
                    _data.writeStrongBinder(listener != null?listener.asBinder():null);
                    this.mRemote.transact(4, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

            }
        }
    }
}

IOnNewBookArrivedListener.class

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package com.soubu.ipcdemo.aidl;

import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
import com.soubu.ipcdemo.aidl.Book;

public interface IOnNewBookArrivedListener extends IInterface {
    void onNewBookArriced(Book var1) throws RemoteException;

    public abstract static class Stub extends Binder implements IOnNewBookArrivedListener {
        private static final String DESCRIPTOR = "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener";
        static final int TRANSACTION_onNewBookArriced = 1;

        public Stub() {
            this.attachInterface(this, "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
        }

        public static IOnNewBookArrivedListener asInterface(IBinder obj) {
            if(obj == null) {
                return null;
            } else {
                IInterface iin = obj.queryLocalInterface("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
                return (IOnNewBookArrivedListener)(iin != null && iin instanceof IOnNewBookArrivedListener?(IOnNewBookArrivedListener)iin:new IOnNewBookArrivedListener.Stub.Proxy(obj));
            }
        }

        public IBinder asBinder() {
            return this;
        }

        public boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
            switch(code) {
            case 1:
                data.enforceInterface("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
                Book _arg0;
                if(0 != data.readInt()) {
                    _arg0 = (Book)Book.CREATOR.createFromParcel(data);
                } else {
                    _arg0 = null;
                }

                this.onNewBookArriced(_arg0);
                reply.writeNoException();
                return true;
            case 1598968902:
                reply.writeString("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
                return true;
            default:
                return super.onTransact(code, data, reply, flags);
            }
        }

        private static class Proxy implements IOnNewBookArrivedListener {
            private IBinder mRemote;

            Proxy(IBinder remote) {
                this.mRemote = remote;
            }

            public IBinder asBinder() {
                return this.mRemote;
            }

            public String getInterfaceDescriptor() {
                return "com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener";
            }

            public void onNewBookArriced(Book newBook) throws RemoteException {
                Parcel _data = Parcel.obtain();
                Parcel _reply = Parcel.obtain();

                try {
                    _data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
                    if(newBook != null) {
                        _data.writeInt(1);
                        newBook.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }

                    this.mRemote.transact(1, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }

            }
        }
    }
}

如果你觉得自己很牛逼的话,其实也是可以自己手写Binder的。不过要想自己写,还是得先好好地学习下AIDL自动生成代码的具体逻辑。

其实我们查看这个根据AIDL自动生成的代码就会发现,就是在AIDL定义的接口之外,又增加了两个实现接口的类,Proxy/Stub。

这里呢,我们以IOnNewBookArrivedListener生成的代码为例,来分析下这种Proxy/Stub模式。

首先看看Proxy的实现,首先是将远程的Binder作为参数传入进来,再来看看 onNewBookArriced() 这个方法里面的下面几个步骤。首先是将newBook的参数写入到_data中去,同时在 远程binder 调用结束后,得到返回的 _reply ,在没有异常的情况下,返回_reply的result结果。(这里,onNewBookArriced()方法没有返回值,不过,前面的getBookList()是有返回值的,大家可以看看它的实现。)

_data.writeInterfaceToken("com.soubu.ipcdemo.aidl.IOnNewBookArrivedListener");
newBook.writeToParcel(_data, 0);
this.mRemote.transact(1, _data, _reply, 0);
_reply.readException();

这里可以粗略地将 Binder 看做远程服务抛出的句柄,通过这个句柄就可以执行相应的方法了。这里需要额外说明的是,写入和传输的数据都是 Parcelable,Android Framework 中提供的。

再来看看Stub里面的实现,Stub本身继承了Binder抽象类,本身将作为一个句柄,实现在服务端,但传递给客户端使用。同样也看看 onTransact里面的方法,首先是通过 (Book)Book.CREATOR.createFromParcel(data) 读取 _arg0 参数,在this.onNewBookArriced(_arg0)计算过后,将结果写入到reply中。

细心的读者,可以从上面的描述中,发现一些有意思的东西:

Proxy 是写入参数,读取值;
Stub 是读取参数,写入值。

正好是一对,那因此我们是不是可以做出这样的论断呢?Proxy 和 Stub 操作的是一份数据?恭喜你,答对了。

用户空间和内核空间是互相隔离的,客户端和服务端在同一用户空间,而 Binder Driver 在内核空间中,常见的方式是通过 copy_from_user 和 copy_to_user 两个系统调用来完成,但 Android Framework 考虑到这种方式涉及到两次内存拷贝,在嵌入式系统中不是很合适,于是 Binder Framework 通过 ioctl 系统调用,直接在内核态进行了相关的操作,节省了宝贵的空间。

可能大家也注意到 Proxy 这里是private权限的,外部是无法访问的,但这里是 Android 有意为之,抛出了 asInterface 方法,这样避免了对 Proxy可能的修改。

总结一下Proxy/Stub的具体流程,其实,客户端主要就是调用了Proxy,而服务端主要就是实现了Stub:

我们需要注意到如果客户端和服务端在同一个进程下,那么此时就没有跨进程通讯了,asInterface()将返回Stub对象本身,否则才返回Stub.Proxy对象。

上面的分析都是基于代码细节的,可能对Proxy/Stub没有一个整体性的把握的话,很难吃透。这里再举一个生活点的栗子。

自动售货机的故事

夏天一到,天气也变得炎热,地铁旁边的自动售货机开始有更多的人关顾了。那么售货机是怎么工作的了?通过对自动售货机的分析,可以对Binder的Proxy/Stub 模式更好地理解。

和我们打交道得是售货机,而不是背后的零售商,道理很简单,零售商的位置是固定的,也就意味着有很大的交通成本,而和售货机打交道就轻松很多,毕竟售货机就在身边。因而从客户端的角度上看,只需要知道售货机即可。

再从零售商的角度来看,要和为数众多的售货机打交道也是不容易的事情,需要大量的维护和更新成本,如果将其交由另一个中介公司,就能够省心不少。零售商只关心这个中介公司,按时发货,检查营收,这就是所有它应该完成的工作。

如上图所示,在 Binder Framework 中也采用了类似的结构,

Proxy=售货机
Stub=中介公司

在这样的设计下,客户端只需要和 Proxy 打交道,服务端只需要和 Stub 打交道,条理清晰很多。这种模式又被称为 Proxy / Stub 模式,这种模式也值得我们在日后的开发中好好借鉴一下。

参考目录:

  1. Android Binder 全解析(3) -- AIDL原理剖析
  2. 《Android开发艺术探索》笔记(二)——IPC进程间通信
  3. 从IBinder接口学习Proxy-Stub设计模式
上一篇下一篇

猜你喜欢

热点阅读