Android IPC 之Binder应用

前言

IPC 系列文章:
建议按顺序阅读。

Android IPC 之Service 还可以这么理解
Android IPC 之Binder基础
Android IPC 之Binder应用
Android IPC 之AIDL应用(上)
Android IPC 之AIDL应用(下)
Android IPC 之Messenger 原理及应用
Android IPC 之服务端回调
Android IPC 之获取服务(IBinder)
Android Binder 原理换个姿势就顿悟了(图文版)

上篇文章分析了Binder作为IPC中的一种在Android里发挥着重要的作用,本篇将从代码的角度分析如何使用Binder进行进程间通信。
通过本篇文章,你将了解到:

1、IPC 基础
2、IBinder/Binder 简介
3、编写跨进程的Service
4、Binder 通信Demo
5、AIDL的引入

1、IPC 基础

网上\color{Red}{绝大部分}文章在分析了Binder之后立即抛出AIDL概念,然后一堆代码,初看让人比较迷惑,再看也无法完全理解AIDL存在的意义。这里套用一个比较俗的说法:存在即合理(一个东西存在有它存在的理由)。同样适用于程序设计,为什么要用AIDL?它不会凭空想当然的出现,一定是它解决了某些问题。接下来我们一步步分析,自然过渡到使用AIDL。

同一进程里的访问

在Java的世界里,一切都是对象,拿到对象后就可以访问对象的属性和方法。
对象存在于内存的某个区域,怎样拿到对象呢?答案是:引用。

    class Student {
        private int age;
        private String name;
    }
    private Student getStudent() {
        //student 是引用
        //该引用指向了堆里面的Student对象
        Student student = new Student();
        return student;
    }

只要拿到了Student引用,就可以操作Student对象。

不同进程里的访问

同一进程里的访问是我们所熟悉的方式,那么不同进程间的访问呢?
假若Student对象在进程B里构造,而想在进程A里使用它。通过上篇文章可知无法直接引用Student,然而可以借助于Binder。


image.png

由图可知:

1、进程A调用Binder提供的接口
2、Binder调用进程B的接口
3、通过Binder的连接,A就可以调用B

Binder(驱动)对于上层来说是透明的,这么看起来就像是A直接调用了B的接口。

2、IBinder/Binder 简介

既然Binder机制要为上层提供接口,那么就需要将接口/类暴露出来,比较重要的接口/类是:IBinder/Binder。
IBinder.java:接口


    public interface IBinder {
        ...
        //code : 要执行动作的标示
        //data : 从客户端往服务端传递的序列化后的数据,不能为空
        //reply : 从服务端返回的序列化后的数据,可能为空
        //附加操作标记:0-->表示阻塞等待该方法调用结束 1-->表示执行该方法后立即返回
        public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags)
                throws RemoteException {
            ...
        }
    }

Binder.java 抽象类
Binder实现了IBinder:

    public class Binder implements android.os.IBinder {
        ...
        //code : 要执行动作的标示
        //data : 从客户端往服务端传递的序列化后的数据,不能为空
        //reply : 从服务端返回的序列化后的数据,可能为空
        //附加操作标记:0-->表示阻塞等待该方法调用结束 1-->表示执行该方法后立即返回
        protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply,
                                     int flags) throws RemoteException {
            ...
        }
        ...
    }

可以看出,transact(xx)和onTransact(xx)参数很像,类似于layout(xx)-->onLayout(xx)、draw(xx)-->onDraw(xx) 调用方式,猜测transact(xx)里最终调用了onTransact(xx)。
整理出两者有如下联系:

1、进程B实现了Binder里的onTransact(xx)方法,并挂出IBiner接口,告诉外界可以调用这个接口来获取B提供的服务。
2、进程A获取了IBinder接口,并调用transact(xx),传递消息给B。
3、通过Binder驱动的中转,找到该IBinder是进程B放出来的,于是调用onTransact(xx)。
至此,进程A就可以发送消息给进程B了。

大致流程如下:


image.png

3、编写跨进程的Service

上面问题的关键是:进程A如何获取进程B提供的IBinder接口?
明显的这势必又是一次IPC过程,恰好可以借助四大组件之一的Service来完成。
在AndroidMenifest.xml里声明Service的时候:

<service android:name=".MyService"></service>

默认表示该Service与当前App运行在同一进程里。
若要想Service运行在单独的进程里,可增加配置如下:

<service android:name=".MyService" android:process=".hello"></service>
或者
<service android:name=".MyService" android:process=":hello"></service>

其中:

  • .hello表示Service运行在名为.hello的进程里
  • :hello表示Service运行在名为当前进程名+hello的进程里,举个例子当前进程名为:com.example.myapplication,那么Service运行的进程名为:com.example.myapplication:hello

配置好如上信息之后,开启(显示开启/绑定开启)Service之后,Service就运行在单独的进程里了:


image.png

4、Binder 通信Demo

基本的东西准备好了,接着来看看具体的业务。

编写Server端业务

1、声明接口
既然进程B要提供给外界服务,那么最好声明一个接口:

public interface IMyServer{
    //提供给外界调用
    public void say(String word);
}

2、定义Binder子类
为了获取Binder驱动发过来的消息,需要声明一个Binder类。

public class MyServerBinder extends Binder {

    //匿名内部类实现接口
    IMyServer iMyServer = new IMyServer() {
        @Override
        public void say(String word) {
            //为方便起见,仅仅打印客户端传递过来的消息
            Log.d("IPC", word + " in process:" + SystemUitl.getAppName(null));
        }
    };

    @Override
    protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
        //从序列化后的内容里读取对应的字段值
        String word = data.readString();
        //收到Client发过来的消息
        iMyServer.say(word);

        //回复消息给Client
        String replyWord = "I'm fine, and you?";
        Log.d("IPC", replyWord + " in process:" + SystemUitl.getAppName(null));
        reply.writeString(replyWord);
        return true;
    }
}

继承自Binder,并重写onTransact(xx),该方法里获取了来自客户端(进程A)的消息,将消息提取出来并交给业务接口IMyServer调用。
同时将消息回传给客户端。
SystemUitl.getAppName(null)工具为获取当前运行的进程名。
3、编写Service
Binder准备好之后,需要将Binder传递给客户端,传递的方法是通过Service传递。

public class MyService extends Service {

    private MyServerBinder myServerBinder = new MyServerBinder();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        //返回IBinder引用给调用者
        return myServerBinder;
    }
}

在Service里构造了MyServerBinder对象,并在onBind(xx)里将引用传递出去。
当客户端绑定这个Service的时候就能拿到该IBinder引用(注意:此处客户端拿到的引用与服务端不是同一个引用,后续会分析此流程)。

编写Client端业务

此时,Server端已经编写完毕,接着来看客户端如何获取IBinder引用。
1、实现ServiceConnection接口
首先实现一个ServiceConnection接口,用以当绑定关系确立后回调该类里的方法:

    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //service 为 Server传递过来的IBinder引用

            //构造序列化对象
            Parcel data = Parcel.obtain();
            Parcel reply = Parcel.obtain();
            //写入String
            String word = "hello server, how are you ?";
            Log.d("IPC", word + " in process:" + SystemUitl.getAppName(null));
            data.writeString(word);
            try {
                //传递消息给Server
                service.transact(2, data, reply, 0);

                //收到Server的回复消息
                String responseWord = reply.readString();
                Log.d("IPC", responseWord + " in process:" + SystemUitl.getAppName(null));
            } catch (Exception e) {

            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

在onServiceConnected(xx)拿到IBinder引用:service。
先构造待发送的数据,然后调用IBinder transact(xx)发送数据给服务端。
2、绑定服务端的Service
客户端通过绑定服务端的Service来建立绑定关系。

    private void bindService() {
        Intent intent = new Intent(MainActivity.this, MyService.class);
        bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
    }

用图来梳理以上流程:


image.png

最后来看看通信结果:


image.png

从日志结果看:Client与Server分别打印了两条日志,说明通信成功了。

5、AIDL的引入

以上是借助Binder来进行两个进程间简单通信,功能是实现了,但是你可能发现了一些端倪:
1、编写冗余
在onTransact(xx)里只调用了say(xx)一个方法,如果需要调用另一个方法,那么就要对code进行区分:

    @Override
    protected boolean onTransact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException {
        switch (code) {
            case 1:
                iMyServer.say(data.readString());
                break;
            case 2:
                iMyServer.say1(data.readInt());
                break;
            case 3:
                iMyServer.say2(data.readFloat());
                break;
        }
        return true;
    }

同样的,在客户端发送消息的时候也需要区分code。Server端提供的业务只有几个接口还好,若是十几个接口甚至更多,书写case不仅是个体力活,还容易出错。
2、序列化数据
在客户端发送数据前,先将数据序列化,在服务端接收数据处理前,先将数据反序列化。这个过程也是个重复的体力活,实际上双方都不关心具体的序列化细节,只知道丢个参数进去,弄个参数出来即可。

3、面向对象
客户端先要调用transact(xx),服务端在onTransact(xx)里调用say(xx)方法。
这么看起来有点绕,实际上比较好的方式是客户端"直接"调用服务端的say(xx)方法:

image.png

如上,Client看起来直接调用了Server的接口,就像是Client拿到了Server对象引用然后操作之,和在同一进程里的操作一样,符合面向对象操作的习惯,大大降低了违和感。

说了这么多直接使用Binder编码的缺点,那么是否有一种方法能解决上面的问题呢?


image.png

没错就是AIDL。

1.gif

接下来的文章将着重分析AIDL原理及其使用。

本文基于Android 10.0。

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营学习Android

1、Android各种Context的前世今生
2、Android DecorView 必知必会
3、Window/WindowManager 不可不知之事
4、View Measure/Layout/Draw 真明白了
5、Android事件分发全套服务
6、Android invalidate/postInvalidate/requestLayout 彻底厘清
7、Android Window 如何确定大小/onMeasure()多次执行原因
8、Android事件驱动Handler-Message-Looper解析
9、Android 键盘一招搞定
10、Android 各种坐标彻底明了
11、Android Activity/Window/View 的background
12、Android Activity创建到View的显示过
13、Android IPC 系列
14、Android 存储系列
15、Java 并发系列不再疑惑
16、Java 线程池系列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容