Android IPC 之Binder应用

1,872 阅读8分钟

前言

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