2020-02-13-Android跨进程通信Binder

传统Linux的IPC通信

传统的IPC通信,由于不同进程间的隔离,用户空间的数据是不能共享的,需要通过内核空间实现数据交换。比如进程A和进程B想要通信,首先进程A将数据通过copy_from_user拷贝到内核空间,然后进程B通过copy_to_user从内核空间拷贝到用户空间。
一次通信需要两次数据拷贝。


IPC.jpg

Binder通信的好处

BInder通信在内核空间开辟了两块缓存区,一块区域负责数据接收,一块负责数据发送,两块内存区域的地址建立了映射关系,同时在用户空间B建立了一块区域映射到数据接收缓存区。这样当用户空间A通过copy_from_user将数据拷贝到内核空间的时候,用户空间B就接收到了同样的数据。
一次通信只需要一次数据拷贝。

IBinder对象实现数据传输

在本地服务中使用Binder比较简单,只是通过重写Binder的transact()和onTransact()方法实现数据传输。

  1. 服务端定义一个Binder的子类,在onBind()方法中返回这个Binder对象。
public class BinderService extends Service {

    public static final String INTERFACE_NAME = "IReporter";
    public static final int CODE_REPORT = 1;
    private static final String TAG = "BinderService";
    private Reporter mReporter = null;

    public BinderService() {
        mReporter = new Reporter();
    }

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

    public final class Reporter extends Binder {

        @Override
        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
            switch (code) {
                case CODE_REPORT:
                    data.enforceInterface(INTERFACE_NAME);
                    String key = data.readString();
                    int value = data.readInt();
                    int result = report(key, value);
                    reply.writeInterfaceToken(INTERFACE_NAME);
                    reply.writeInt(result);
                    return true;
            }
            return super.onTransact(code, data, reply, flags);
        }

        public int report(String key, int values) {
            Log.d(TAG, "report key=" + key + " value=" + values);
            return 0;
        }
    }
}
  1. 客户端通过bindService()获取IBinder对象,由IBinder.transact()调用服务端的onTransact()方法,实现数据传输。
    实际上传输的数据是Parcel对象,创建Parcel对象的时候,不是通过new方法调用构造函数,而是使用了Parcel.obtain(),这里应该用到了对象的缓存池,类似Message的obtain()方法。Parcel的具体实现大部分是在native层,这里不深入,只是记得最后要调用recycle方法进行对象的回收。
public class BinderClient {

    private static final String TAG = "BinderClient";
    private IBinder mReporterBinder = null;

    public void bindReporter(Context context) {
        Intent intent = new Intent(context, BinderService.class);
        context.bindService(intent, new BinderConnection(), Context.BIND_AUTO_CREATE);
    }

    public void report(String key, int value) {
        if (mReporterBinder != null) {
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            data.writeInterfaceToken(BinderService.INTERFACE_NAME);
            data.writeString(key);
            data.writeInt(value);
            try {
                mReporterBinder.transact(BinderService.CODE_REPORT, data, reply, 0);
                reply.enforceInterface(BinderService.INTERFACE_NAME);
                int result = reply.readInt();
                Log.d(TAG, "report result=" + result);
            } catch (RemoteException e) {
                Log.e(TAG, "Fail to transact e=" + e);
            } finally {
                data.recycle();
                reply.recycle();
            }
        }
    }

    private class BinderConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected ComponentName=" + name);
            mReporterBinder = service;
        }

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

这里调用的transact()有两种方式,flag=0和flag=FLAG_ONEWAY。默认情况下,flag=0,客户端线程会阻塞直到服务端返回。

AIDL实现数据传输

Android Interface Definition Language (AIDL)是Android对进程间通信的封装。
Android Studio提供了自动生成aidl接口的方法,只需要新建一个aidl文件,编译项目后就得到一个同名的Java文件。
比如新建一个IReportInterface.aidl文件。

interface IReportInterface {
    int report(String key, int values);
}

编译后得到IReportInterface.java,里面有个内部静态抽象类继承了Binder,为我们做好了Parcel数据打包的过程,不需要再手动编码实现。
这里先不看IReportInterface的具体内容。先看下服务端和客户端的实现代码。
服务端的Binder类改成了继承IReportInterface.Stub,不需要再重写onTransact方法,因为aidl已经自动生成了代码。

public class BinderService extends Service {

    private static final String TAG = "BinderService";
    private Reporter mReporter = null;

    public BinderService() {
        mReporter = new Reporter();
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return mReporter;
    }

    public final class Reporter extends IReportInterface.Stub {
        public int report(String key, int values) {
            Log.d(TAG, "report key=" + key + " value=" + values);
            return 0;
        }
    }
}

客户端也不再需要通过transact方法传输数据,而是通过IBinder对象直接获得Service的实例,直接调用report方法。

public class BinderClient {

    private static final String TAG = "BinderClient";
    private IReportInterface mReporter = null;

    public void bindReporter(Context context) {
        Intent intent = new Intent(context, BinderService.class);
        context.bindService(intent, new BinderConnection(), Context.BIND_AUTO_CREATE);
    }

    public void report(String key, int value) {
        Log.d(TAG, "report key = " + key + " value = " + value);
        if (mReporter != null) {
            try {
                mReporter.report(key, value);
            } catch (RemoteException e) {
                Log.e(TAG, "Fail report e=" + e);
            }
        }
    }

    private class BinderConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "onServiceConnected ComponentName=" + name);
            mReporter = IReportInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            //
        }
    }
}

最后再来看下自动生成的代码,是怎么实现数据传输的。
有三个内部类Default,Stub和Proxy。


Binder (1).jpg

先看下Stub.asInterface()方法,它的作用是把一个IBinder对象转化成IInterface对象。有三种返回结果,如果IBinder为空,返回null;如果能获取到本地的IInterface对象,返回本地IInterface;兜底方案是新构建一个Proxy对象返回。
实际上如果客户端和服务端在同一个进程,asInterface()方法返回的就是服务端创建的Stub对象,是不需要调用onTransact()方法对Parcel数据进行打包和解包操作的,因为不需要跨进程。

/**
         * Cast an IBinder object into an com.one.binder.IReportInterface interface,
         * generating a proxy if needed.
         */
        public static com.one.binder.IReportInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.one.binder.IReportInterface))) {
                return ((com.one.binder.IReportInterface) iin);
            }
            return new com.one.binder.IReportInterface.Stub.Proxy(obj);
        }

只有获取不到Stub对象,才会构造一个Proxy类,重写report()方法,对发送的数据进行打包。

            @Override
            public int report(java.lang.String key, int values) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeString(key);
                    _data.writeInt(values);
                    boolean _status = mRemote.transact(Stub.TRANSACTION_report, _data, _reply, 0);
                    if (!_status && getDefaultImpl() != null) {
                        return getDefaultImpl().report(key, values);
                    }
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }

然后在服务端通过onTransact()对数据进行解包,这就是为什么客户端和服务端都需要保存一份AIDL文件。因为它负责数据的打包和解包,双方必须一模一样。

        @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_report: {
                    data.enforceInterface(descriptor);
                    java.lang.String _arg0;
                    _arg0 = data.readString();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.report(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
                default: {
                    return super.onTransact(code, data, reply, flags);
                }
            }
        }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342