Binder机制之AIDL

写文章已经4周年了,虽然中间很多时候都在偷懒,但也写了100篇文章,虽然回过头看有些文章写得好像自己现在也很嫌弃。但是确实怎么长时间的偷懒并坚持着,让我感觉确实有点欣慰,虽然这个赞啊阅读量啊这些都不高,但是我相信坚持下去肯定会有收获,至少不去努力什么都不会有。

既然是4周年,那就趁着这时候,赏自己一篇文章。我看平时开发也没涉及到复杂的夸进程通信,但是我两年前是有看过Binder机制的,但是我没笔记,所以导致了,我现在要重新去看,我又会浪费很多时间,所以论写文章的重要性,看自己的写的文章更能恢复记忆。

既然基本全忘了,那就大不了重头再来一遍嘛。

一. AIDL的Demo

写个Demo用AIDL来实现夸进程通信,我创建两个module



aidldemo充当客户端,binderservicedemo充当服务端。

1. 定义AIDL


在main下创建个aidl文件夹,然后创建个AIDL文件。
这个东西有什么用呢?他就是个接口,有一定编程经验的人都知道,就是面向接口编程的一种感觉,这个我不知道怎么解释,就是引用的时候,我们是对接口去操作,而不是对实体类,一般开发都是这样。
相当于我客户端操作你服务端,就是通过这个接口去操作,你接口告诉我你有哪些行为,我去某个方法,具体的实现你内部自己做,我不管。这个接口就是这样一个效果,所以,两端都要持有这个接口的引用,这个aidl就要在两端都存在

包名和类名都要一样,因为是同一个对象。一个小技巧,我这是演示所以这样写,实际不会这样,不然你两个项目都有重复内容,可以单独抽出来,然后两个项目在gradle去引用。

interface IMyBook {
    //  定义一个Book接口,有个返回String的方法
    String getBookName(int count);

}

2. 服务端

写个服务端的代码

public class BookService extends Service {

    @Override
    public IBinder onBind(Intent intent) {
        Log.v("mmp", "service onBind");
        return new BookBinder();
    }

    protected class BookBinder extends IMyBook.Stub{
        @Override
        public String getBookName(int count) throws RemoteException {
            // todo 处理逻辑
            if(count < 18){
                return "Study";
            }

            return "YouKnow";
        }
    }


}

就很简单,这里实现接口,然后详细写getBookName方法的实现,客户端绑定这个Service的时候会调用onBind方法,就返回一个IMyBook的实体对象。
在manifest中注册服务

        <service android:name="com.kylim.binderservicedemo.BookService"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.BookService" />
            </intent-filter>
        </service>

3. 客户端

public class MainActivity extends Activity {

    private IMyBook book;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn = findViewById(R.id.btn);
        initService();
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                try {
                    String result = book.getBookName(19);
                    Log.v("mmp", "跨进程获取结果:"+result);
                } catch (Exception e) {
                    e.printStackTrace();
                    Log.v("mmp", "跨进程获取结果是失败:"+e.getMessage());
                }
            }
        });
    }

    private void initService(){
        Intent intent = new Intent("android.intent.action.BookService");
        intent.setPackage("com.kylim.binderservicedemo");
        bindService(intent, new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                Log.v("mmp", "onServiceConnected");
                // todo 绑定
                book = IMyBook.Stub.asInterface(service);
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {
                Log.v("mmp", "onServiceDisconnected");
                // todo 解绑
            }

        }, BIND_AUTO_CREATE);
    }

}

然后先运行服务的应用,再运行客户端的应用,点按钮就能实现夸进程通信了



可以看到是不同的进程。

二. AIDL的生成

上面Demo那就很简单,就是A进程和B进程都引用接口I,A通过I去和B通信,没什么好讲的。但是AIDL其实是Android提供的工具,是为了方便开发而用,它把整个过程抽象成一个接口,但是具体的他会在编译时生成java代码。
我之前写过一篇文章是手动编译https://www.jianshu.com/p/f811f36e2997,看里面的编译流程图


他是通过sdk的这个工具去操作的,那篇文章我没写,但是原理一样,敲命令去实现

我们可以看看生成的java代码

public interface IMyBook extends android.os.IInterface
{
    /** Local-side IPC implementation stub class. */
    public static abstract class Stub extends android.os.Binder implements com.kylim.aidldemo.IMyBook
    {
        private static final java.lang.String DESCRIPTOR = "com.kylim.aidldemo.IMyBook";
        /** Construct the stub at attach it to the interface. */
        public Stub()
        {
            this.attachInterface(this, DESCRIPTOR);
        }
        /**
         * Cast an IBinder object into an com.kylim.aidldemo.IMyBook interface,
         * generating a proxy if needed.
         */
        public static com.kylim.aidldemo.IMyBook asInterface(android.os.IBinder obj)
        {
            if ((obj==null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof com.kylim.aidldemo.IMyBook))) {
                return ((com.kylim.aidldemo.IMyBook)iin);
            }
            return new com.kylim.aidldemo.IMyBook.Stub.Proxy(obj);
        }
        @Override public android.os.IBinder asBinder()
        {
            return this;
        }
        @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
        {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code)
            {
                case INTERFACE_TRANSACTION:
                {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getBookName:
                {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    java.lang.String _result = this.getBookName(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                default:
                {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }
        private static class Proxy implements com.kylim.aidldemo.IMyBook
        {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote)
            {
                mRemote = remote;
            }
            @Override public android.os.IBinder asBinder()
            {
                return mRemote;
            }
            public java.lang.String getInterfaceDescriptor()
            {
                return DESCRIPTOR;
            }
            @Override public java.lang.String getBookName(int count) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(count);
                    mRemote.transact(Stub.TRANSACTION_getBookName, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
        static final int TRANSACTION_getBookName = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    }
    public java.lang.String getBookName(int count) throws android.os.RemoteException;
}

这个代码,你也可以不用aidl,可以手敲,就是说aidl帮我们生成,所以很方便。
可以看出,客户端是通过Stub的asInterface方法去获取到这个接口的

        public static com.kylim.aidldemo.IMyBook asInterface(android.os.IBinder obj)
        {
            if ((obj==null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin!=null)&&(iin instanceof com.kylim.aidldemo.IMyBook))) {
                return ((com.kylim.aidldemo.IMyBook)iin);
            }
            return new com.kylim.aidldemo.IMyBook.Stub.Proxy(obj);
        }

这里就有意思了,他先调用传进来的IBinder去查这个接口的一个标识:DESCRIPTOR。如果有的话就返回,否则创建一个Proxy。

一步步来,IBinder的实现类是Binder,看它的queryLocalInterface方法

    public IInterface queryLocalInterface(String descriptor) {
        if (mDescriptor != null && mDescriptor.equals(descriptor)) {
            return mOwner;
        }
        return null;
    }

这两个参数都是attachInterface方法来的

    public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) {
        mOwner = owner;
        mDescriptor = descriptor;
    }

这个方法是Stub的构造方法调用的

        public Stub()
        {
            this.attachInterface(this, DESCRIPTOR);
        }

看上面服务端BookService的代码,构造方法就是在那里new的时候调用的。
这说明什么,说明是同一个对象的时候,说明你在onServiceConnected方法中获取的IBinder是你在Service的onBind中return的对象的时候,这两个参数才对得上。但是我Demo里面这两个地方是同一个对象吗?不是,为什么,因为他们属于不同的进程,怎么去知道他们两个是不是同一个对象,那就需去看onBind方法怎么去调用到onServiceConnected这个回调的,这篇文章就先不讲,总之先简单了解不是同个对象。
既然不是同个对象,那就走return new com.kylim.aidldemo.IMyBook.Stub.Proxy(obj) 这行代码

        private static class Proxy implements com.kylim.aidldemo.IMyBook
        {
            private android.os.IBinder mRemote;
            Proxy(android.os.IBinder remote)
            {
                mRemote = remote;
            }

            @Override public java.lang.String getBookName(int count) throws android.os.RemoteException
            {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.lang.String _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(count);
                    mRemote.transact(Stub.TRANSACTION_getBookName, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readString();
                }
                finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

看得出,代码中onServiceConnected回调里面,IMyBook.Stub.asInterface(service)方法返回的是这个Proxy 对象,这是一个代理对象。
这又要涉及到代理模式了,简单的这样想,我进程A要操作进程B,但是他们之间的通信很复杂,我封装这一块内容,抽象当成,我进程A调用Proxy 代理,Proxy 做了一些列复杂操作,把消息传给进程B,我A不管你做什么操作, 反正有什么我就把这事交给你,剩下的你自己内部解决。就像你的领导给你需求,他管你怎么处理,他只管结果。

当按按钮时就调用到这个代理的getBookName方法。
嗯?难道要翻车?直接看源码,到这步卡住了,我保证没做过功课,一直跟着源码走,到这步有点卡住了,有点看不懂这里面做的是什么骚操作
但是不要慌,一点一点来分析。
首先这个obtain()和recycle()方法,就是一个复用池,复用这个Parcel对象,这是android代码里面常用的做法,Message也是这样玩的,看多代码就知道了,套路都一样。
还能看出_data相当于Request,_result相当于Response。Parcel,一看就是和序列化有关的,那核心的代码就是

mRemote.transact(Stub.TRANSACTION_getBookName, _data, _reply, 0);

看看Binder的transact方法

    public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply,
            int flags) throws RemoteException {
        if (false) Log.v("Binder", "Transact: " + code + " to " + this);

        if (data != null) {
            data.setDataPosition(0);
        }
        boolean r = onTransact(code, data, reply, flags);
        if (reply != null) {
            reply.setDataPosition(0);
        }
        return r;
    }

这个if (false)打印是什么操作,测试的没来得及删吗。接着往下看onTransact

    protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
            int flags) throws RemoteException {
        if (code == INTERFACE_TRANSACTION) {
            reply.writeString(getInterfaceDescriptor());
            return true;
        } else if (code == DUMP_TRANSACTION) {
            ParcelFileDescriptor fd = data.readFileDescriptor();
            String[] args = data.readStringArray();
            if (fd != null) {
                try {
                    dump(fd.getFileDescriptor(), args);
                } finally {
                    IoUtils.closeQuietly(fd);
                }
            }
            // Write the StrictMode header.
            if (reply != null) {
                reply.writeNoException();
            } else {
                StrictMode.clearGatheredViolations();
            }
            return true;
        } else if (code == SHELL_COMMAND_TRANSACTION) {
            ParcelFileDescriptor in = data.readFileDescriptor();
            ParcelFileDescriptor out = data.readFileDescriptor();
            ParcelFileDescriptor err = data.readFileDescriptor();
            String[] args = data.readStringArray();
            ShellCallback shellCallback = ShellCallback.CREATOR.createFromParcel(data);
            ResultReceiver resultReceiver = ResultReceiver.CREATOR.createFromParcel(data);
            try {
                if (out != null) {
                    shellCommand(in != null ? in.getFileDescriptor() : null,
                            out.getFileDescriptor(),
                            err != null ? err.getFileDescriptor() : out.getFileDescriptor(),
                            args, shellCallback, resultReceiver);
                }
            } finally {
                IoUtils.closeQuietly(in);
                IoUtils.closeQuietly(out);
                IoUtils.closeQuietly(err);
                // Write the StrictMode header.
                if (reply != null) {
                    reply.writeNoException();
                } else {
                    StrictMode.clearGatheredViolations();
                }
            }
            return true;
        }
        return false;
    }

看错了,不好意思,这里是调用onTransact没错,但是子类有重写这个方法,所以应该是先看Stub中的onTransact,因为onBind中传过来的是IMyBook的Stub,没看懂的可以自己多看几遍,确实有点绕。

        @Override public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException
        {
            java.lang.String descriptor = DESCRIPTOR;
            switch (code)
            {
                case INTERFACE_TRANSACTION:
                {
                    reply.writeString(descriptor);
                    return true;
                }
                case TRANSACTION_getBookName:
                {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    java.lang.String _result = this.getBookName(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }
                default:
                {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }

在getBookName中传入的code是TRANSACTION_getBookName,所以走这个判断

                case TRANSACTION_getBookName:
                {
                    data.enforceInterface(descriptor);
                    int _arg0;
                    _arg0 = data.readInt();
                    java.lang.String _result = this.getBookName(_arg0);
                    reply.writeNoException();
                    reply.writeString(_result);
                    return true;
                }

这个是服务端的对象,所以调用getBookName方法就是执行了服务端那边的getBookName方法。

是有点绕啊,Proxy对象是属于客户端进程的,但他内部的mRemote是属于服务端的对象。
他们之间的引用关系 客户端进程->Proxy->mRemote; mRemote->服务端进程

从源码可以看出,AIDL就是定义了通信的接口,真正的底层通信,还是Binder机制来实现的,所以“AIDL是多进程通信的方法”,这句话应该是错的把,真正做通信的应该是Binder,是Demo中onBind方法和onServiceConnected回调他们之间的联系。后面会写一篇Binder机制的源码分析。我是一面看源码一面写这篇文章的,如果有写得不对的地方,希望能指出,我再找个机会认真看一遍。