安卓开发之IPC机制详解(附AIDL实例)

IPC(Inter-Process Communication),意为进程间通信或者跨进程通信,是指两个进程之间进行数据交换的过程。前面在学习Handler机制时提到过线程与进程的概念,在安卓中一个进程可以简单理解为一个程序或应用,而线程是CPU调度的最小单元,一个进程可以包含多个线程,也可以只有一个线程即主(UI)线程。

1. 多进程模式

既然是跨进程通信,必然要在多进程模式下进行。

使用多进程的情况分为两种:

  • 由于某些原因,应用自身需要采用多进程模式来实现。可能原因有:应用中某些模块因特殊原因要运行在单独进程中;为加大一个应用可使用的内存,需要通过多进程来获取多份内存空间
  • 当前应用需要向其它应用获取数据,例如通过系统提供的ContentProvider去查询数据时,其实也是一种进程间通信

安卓中开启多进程模式的方式

  • 通过JNI在native层fork一个新的进程,我们常见的应用反附加调试就用的是这种方法,应用启动时自己先fork一个进程附加自己
  • 给四大组件指定android:process属性,进程名的命名规则:
    • 1)默认:没有指定该属性则运行在默认进程,其进程名就是包名
    • 2)完整命名的进程:例如android:process="com.example.myapplication.remote,表示该进程属于全局进程,其他有着相同ShareUID和签名的应用可以通过ShareUID方式和它跑在同一个进程中,在这种情况下,无论它们是否跑在同一进程,它们均可共享data目录,组件信息,如果跑在同一进程中,还可共享内存数据,看起来就像是一个应用的两个部分
    • 3)以":“开头的进程:例如android:process=”:remote",进程名包名+":remote"=com.example.myapplication:remote,表示该进程属于当前应用的私有进程,其他进程的组件不能和它跑在同一进程中

多进程模式的运行机制

所有运行在不同进程的四大组件,只要它们之间需要通过内存来共享数据,都会共享失败。这是因为Android为每个应用/进程分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间,这会导致在不同的虚拟机中访问同一个类的对象会产生多份副本。举例来说,假如两个进程中都存在同一个类,并且这两个类是互不干扰的,在其中一个进程中修改类变量的值只会影响当前进程,对其他进程不会造成任何影响。

使用多进程会造成的问题:

  • 静态变量和单例模式失效:Android为每个应用/进程分配了一个独立的虚拟机,不同的虚拟机在内存分配上有不同的地址空间
  • 线程同步机制失效:原因同上
  • SharedPreference的可靠性下降:SharedPreferences不支持两个进程同时进行写操作,否则会导致数据丢失,这是因为SP的底层是通过读/写XML文件来实现的,并发写甚至并发读都有可能出现问题
  • Application会多次创建:Android会为新的进程分配独立的虚拟机,相当于系统又把这个应用重新启动了一次,那么自然就会创建新的Application

2. Binder机制

2.1 Android新增Binder机制的原因

我们知道任何一个操作系统都有对应的IPC机制,例如:

  • Windows:通过剪切板、管道等进行进程间通信
  • Linux:通过命名空间、共享内容、信号量等进行进程间通信
  • Android:没有完全继承Linux,取而代之的是Binder机制,并通过Binder机制实现了例如 Bundle、文件共享、AIDL、Messager、Socket、ContentProvider等IPC方式,这些方式本质上都是通过Binder机制来实现的只不过是封装方式不同。Android另开炉灶新增Binder机制肯定是有原因的,主要有以下几点:
  1. 传输效率高、可操作性强:传输效率主要影响因素是内存拷贝的次数,拷贝次数越少,传输速率越高。几种数据传输方式比较:
    在这里插入图片描述
    对于消息队列、Socket和管道来说,数据先要从发送方的缓存区拷贝到内核开辟的缓存区中,再从内核缓存区拷贝到接收方的缓存区,一共两次拷贝,图片来自Android - Binder驱动
    在这里插入图片描述
    而对于Binder来说,数据从发送方的缓存区拷贝到内核的缓存区,而接收方的缓存区与内核的缓存区是映射到同一块物理地址的,只需要一次拷贝:
    在这里插入图片描述
    共享内存虽然在传输时没有拷贝数据,但其控制机制复杂,综合来看Binder的传输效率是最好的。

  2. 实现C/S架构方便:Linux的IPC方式中只有Socket支持C/S的通信模式,而Socket主要用于网络间的通信且传输效率低、开销大。Binder基于C/S 架构设计 ,Server端与Client端相对独立,稳定性较好。
    如下图所示,Binder框架定义了四个角色:Server,Client,ServiceManager和Binder驱动,其中Server、Client、ServiceManager运行于用户空间,Binder驱动运行于内核空间,Binder框架如下图所示:
    在这里插入图片描述
    这里先解释下上面说的用户空间和内核空间。我们知道在Android中每一个进程/应用都是独立的,只能运行在自己进程所拥有的虚拟地址空间,且都由两部分组成,一部分是用户空间,另一部分是内核空间,对于用户空间来说,不同进程/应用之间彼此是不能共享的,而内核空间却是可共享的。Client进程与Server进程通信利用进程间可共享的内核内存空间来完成底层通信工作,Client端与Server端进程跟内核空间的驱动进行交互往往采用ioctl(专门用于设备输入输出操作的系统调用)等方法。这样做一方面可以限制其他进程访问自己的虚拟地址空间,安全性高,另一方面内核共享也有助于系统维护和并发操作,节省空间:
    在这里插入图片描述

  3. 安全性高:传统Linux IPC的接收方无法获得对方进程可靠的UID/PID,从而无法鉴别对方身份,而Binder机制为每个进程分配了UID/PID且在Binder通信时会根据UID/PID进行有效性检测

2.2 Binder机制流程简介

刚上来就分析具体的流程确实比较困难,这里我们通过一个例子了解Binder运行机制。

首先介绍下Binder框架定义的几个角色:

  • Server&Client:服务器&客户端。在Binder驱动和ServiceManager提供的基础设施上,进行Client和Server之间的通信,这里Server、Client角色不是固定的,例如它们和ServiceManager通信时(通信基于Binder机制)角色就不相同,我们下面会说到
  • Binder驱动:与硬件设备没有关系,只不过其工作方式与设备驱动程序是一样的,它负责进程之间Binder通信的建立,Binder在进程之间的传递,Binder引用计数管理,数据包在进程之间的传递和交互等一系列底层支持
  • ServiceManager:服务管理者,将Binder名字转换为Client中对该Binder的引用,使得Client可以通过Binder名字获得Server中Binder实体的引用。流程如下图所示:
    在这里插入图片描述
    ServiceManager、 Server、Client三者之间的相互通信就是基于Binder机制的:
    • 1.注册服务:Server进程要先注册Service到ServiceManager。该过程中Server是客户端,ServiceManager是服务端
    • 2.获取服务:Client进程使用某个Service前,要先从ServiceManager中获取相应的Service,通常是Service引用的代理对象(假如Server、Client不处于同一进程的话)。该过程中Client是客户端,ServiceManager 是服务端
    • 3.使用服务:Client根据得到的Service信息建立与Service所在的Server进程通信的线路,然后就可以直接与Service交互。该过程中Client是客户端,Server是服务端

图中也看到Client、Server、ServiceManager之间的交互都是虚线表示,是由于它们彼此都不是直接交互的,而是都通过与Binder驱动进行交互从而实现IPC通信方式。这里可能会有人问为什么Client不直接和Server通信而要加个ServiceManager呢?其实这样做一方面便于管理service和Client的请求;另一方面在应用程序开发时,只需为Client建立到Server的连接,就可花很少时间和精力去实现Server相应功能。举个例子,你(client)毕业了想向你们班所有同学(Server)写信,考虑两种情况:一是你们有个在线通讯录文档(ServiceManager),同学们毕业前都主动把自己联系地址“注册”了上去,这样你寄信的时候查一下保存的通讯录(ServiceManager)对应名字的地址就好;二是没有这个通讯录(ServiceManager),你写信要一个个打电话询问然后寄出去,显而易见第一种方式简单一些。当然这个例子不太恰当,Android既然这样做那么肯定是有好处的。

这里参考详细说说Binder通信原理与机制给的Binder跨进程通信的例子以理清上面的流程:

假如Client进程调用Server进程的computer对象的add方法:

  • 1.注册服务:Server进程向ServiceManager注册,告诉ServiceManager我是Server,我有computer对象,我能做add这个事
  • 2.获取服务:Client进程向ServiceManager查询,我要调用Server进程的computer对象的add方法,这个过程会经过Binder驱动,当向ServiceManager查询完毕后,Binder驱动就发挥作用了,它不会直接将computer对象返回给Client进程,而是将computer对象转换成了computerProxy代理对象,并将这个代理对象返回给了Client进程,因此Client进程拿到的实际上是一个代理对象computerProxy,并且这个代理对象也有add方法,但是这个add方法只是对参数进行一些包装而已
    -3.使用服务:当Client进程要调用add方法,就把这个消息发送给Binder驱动:“我要调用Server进程的computer对象的add方法(其实调用的是代理对象的add方法,与此同时代理对象的方法会将Client传递的参数打包成Parcel序列化对象)”,代理对象将Parcel发送给内核中的Binder驱动,Binder驱动发现调用的是computerProxy代理对象的add方法,它知道Client进程实际上是要调用computer对象的add方法,这时Binder驱动就去通知Server进程,调用你的computer对象的add方法并将结果给我,然后Server进程就读取Binder驱动中的请求数据,如果是发送给自己的,就解析Parcel对象,调用了computer对象的add方法并将计算结果发送给了Binder驱动,Binder驱动再返回给Client进程。从始至终Client进程都以为自己调用的是真实的computer对象的add方法,其实它只是调用了代理对象的add方法而已,不过好在Client最终还是拿到了计算结果。这里和前面问题一样,为什么不直接去调用而是要加个代理?这是因为通过引入代理对象的方式来间接访问目标对象可以减少直接访问目标对象给系统带来的不必要复杂性,并且通过代理对象可以对原有的业务增强。再举个不恰当的例子,你想购买国外某公司旗下的某款产品,假如没有淘宝啊之类的第三方代理,你自己打电话过去说我想买你们公司这款产品,那中间快递运输、关税、售后等等之类的都得你自己去解决,那还不如让代理去做这些“底层”的东西,你只需要提出需求给代理然后收货就好

再简单一点看的话,之前在学习bindservice启动Service的时候,自定义的Service(就看做是服务端吧)在onBind()方法中就要返回一个包含了“服务端”业务调用的Binder对象,在“客户端”角度来说,这个返回的就是一个Binder引用,通过这个Binder引用,“客户端”就可以获取“服务端”提供的服务或者数据。这里也可以看到,“服务端”想要实现被“跨进程”访问,就必须继承Binder类。
在这里插入图片描述
MainActivity中:
在这里插入图片描述在这里插入图片描述
上面还提到了代理就简单了解一下即可,详细可参考代理模式以及在Android中的使用 ,代理模式Proxy就是给某个对象提供一个代理对象,并由代理对象控制对原对象的访问。UML图如下所示:
在这里插入图片描述

  • 公共接口角色:定义了Real Subject和Proxy的共同接口,这样在任何可以使用Real Subject的地方都可以使用Proxy,例如公司和代理都有sale销售方法
  • Real Subject(国外公司) :定义了Proxy所代表的Real Subject,并定义实现具体业务逻辑的实现
  • Proxy(第三方代理) :持有Real Subject的引用,控制和限制Real Subject的实现,可在任何时候操作目标对象;提供一个与Real Subject相同的接口,可在任何时候替代目标对象;并且还可以拥有自己的处理方法(预处理和善后)

2.3 Binder 工作原理

在这里插入图片描述

  • Server端:在服务端创建好一个Binder对象后,内部就会开启一个线程用于接收Binder驱动发送的消息,收到消息后调用onTranscat方法,并按照参数执行不同的服务端代码
  • Binder驱动:在服务端成功Binder对象后,Binder驱动也会创建一个mRemote对象(也是Binder类),客户端可借助它调用transcat()即可向服务端发送消息
  • Client端:客户端要想访问Binder的远程服务,就必须获取远程服务的Binder对象在Binder驱动层对应的mRemote引用。当获取到mRemote对象的引用后,就可以调用相应Binder对象的暴露给客户端的方法

具体可参考:

3.Android中的IPC方式

前面提到过,Bundle、AIDL、Socket等一些IPC方式实际都是通过Binder来实现,只不过封装方式不同,这里简单了解下Android中的几种IPC也就是跨进程通信的方式,详细的解释可以参考《Android开发艺术探索》:

3.1 Bundle

Bundle在之前学习Activity通信的时候学过,它可以在Activity、Service和Receiver之间通过Intent.putExtra()传递Bundle数据。举个例子:

    //MainActivity中
    Intent intent = new Intent(MainActivity.this,SecondActivity.class);
    Bundle data = new Bundle();
    data.putString("name","lisi");//将数据放入bundle
    data.putInt("age",27);
    data.putBoolean("isfemale",true);
    intent.putExtras(data);

    //在SecondActivity中,将传递的数据取出
    Bundle data = getIntent().getExtras();//从bundle中取出数据
    String name = data.getString(MainActivity.NAME_KEY);
    int age = data.getInt(MainActivity.AGE_KEY);
    boolean isfemale = data.getBoolean(MainActivity.IS_FEMALE_KEY);
    Log.d(LOG_TAG,"name = "+name);
    Log.d(LOG_TAG,"age = "+age);
    Log.d(LOG_TAG,"isfemale = "+isfemale);

Bundle也可以传递序列化对象:

    //存入数据
    Person person = new Person();//Person是一个JavaBean,实现了Serializable接口
    person.setName("lisi");//设置相应的属性值
    person.setAge(27);
    person.setIsfemale(true);
    mIntent.putExtra(PERSON_KEY,person);

    //取出数据
    Bundle data = intent.getExtras();
    Person person = (Person) data.getSerializable(MainActivity.PERSON_KEY);
    Log.d(LOG_TAG,"name = "+person.getName());
    Log.d(LOG_TAG,"age = "+person.getAge());
    Log.d(LOG_TAG,"isfemale = "+person.isfemale());

其原理是实现了Parcelable接口,它可方便的在不同的进程中传输,另外要注意Bundle不支持的数据类型无法在进程中被传递,例如进程A进行计算后的结果想要传递给进程B,但是结果却不是Bundle所支持的数据类型,这种情况下可以将在A进程进行的计算过程转移到B进程中的一个Service里去做,避免了进程间的通信问题。

3.2 文件共享

两个进程通过读/写同一个文件来交换数据,例如A进程把数据写入文件,B进程通过读取这个文件来获取数据,这种方式适合对数据同步要求不高的进程之间的通信,并且要妥善处理并发读/写的问题。

3.3 Messager

Messenger 是一种轻量级的 IPC 方案,通过它可在不同进程中传递Message对象。

Messenger.send(Message);

它的底层实现是 AIDL ,它对 AIDL 进行了封装,更便于进行进程间通信。Messenger一次只处理一个请求,所以在服务端不用考虑线程同步的问题。工作原理图如下图所示:
在这里插入图片描述
Message和Messenger实现了Parcelable接口,所以可以在进程间进行传递。Message是我们所要传递信息的载体,Messenger提供了传递的渠道,Handler是最终的信息接受和处理中心,Handler本身是无法进行接受消息的,只不过在创建Messenger时,Messenger持有 Handler的对象 ,在 Messenger内部调用了Handler的handleMessage方法,让其去处理Message 。

实现方法如下,具体代码可参考IPC 之 Messenger 的使用

服务端

  • 创建一个Service用于提供服务
  • Service中创建一个Handler用于接收器客户端进程发来的数据
  • 利用Handler创建一个Messenger对象
  • 在Service的onBind()中返回Messenger对应的Binder对象

客户端

  • 通过bindService绑定服务端的Service
  • 通过绑定后返回的IBinder对象创建一个Messenger,进而可向服务器端进程发送Message数据。(完成单向通信)
  • 在客户端创建一个Handler并由此创建一个Messenger,并通过Message对象的replyTo字段传递给服务器端进程。服务端通过读取Message得到Messenger对象,进而向客户端进程传递数据。(完成双向通信)

3.4 ContentProvider

ContentProvider是Android提供的用来进行不同应用间数据共享的方式,例如我们之前获取通讯录/短信信息就是通过ContentProvider实现的,这里因为通讯录/短信也是一个应用,那我们的应用要想获取这些信息势必要跨进程/应用通信,底层就是通过Binder实现的。具体用法参考之前的文章安卓开发之内容提供器

使用时注意一下几点:

  • 除了onCreate()运行在UI线程中,其他的query()、update()、insert()、delete()和getType()都运行在Binder线程池中
  • CRUD四大操作存在多线程并发访问,要注意在方法内部要做好线程同步
  • 一个SQLiteDatabase内部对数据库的操作有同步处理,但多个SQLiteDatabase之间无法同步

3.5 Socket

不仅可以跨进程、还可以跨设备通信。
原理参考之前的文章安卓开发网络编程学习之Socket
Demo参考Android:这是一份很详细的Socket使用攻略

3.6 AIDL

AIDL(Android接口定义语言):如果在一个进程中要调用另一个进程中对象的方法,可使用AIDL生成可序列化的参数,AIDL会生成一个服务端对象的代理类,通过它,客户端就会间接调用服务端对象的方法,对我们初学者来说,AIDL的作用就是可以在自己的APP里绑定一个其他APP的service,这样自己的APP就可以和其他APP交互。当然我们也可以自己写Binder代码去实现进程间通信,但是你会发现写的代码和AIDL的代码差不多,而且AIDL还帮我们自动生成了一些Binder相关代码不用我们自己再去写,大大简化了开发过程。大致流程可以参考前面Binder原理图。

3.6.1 AIDL语法

3.6.1.1 数据类型

AIDL支持的数据类型包括以下几种:

  • 基本数据类型:byte,int,long,float,double,boolean,char
  • String类型
  • CharSequence类型
  • ArrayList、HashMap且里面的每个元素都能被AIDL支持
  • 实现Parcelable接口的对象
  • 所有AIDL接口本身

要注意的是,AIDL除了基本数据类型,其它类型(例如我们自定义的实体类型)的参数必须标上方向:in、out或inout,用于表示在跨进程通信中数据的流向。

  • in:表示数据只能由客户端流向服务端。服务端将会接收到这个对象的完整数据,但在服务端修改它不会对客户端输入的对象产生影响。
  • out:表示数据只能由服务端流向客户端。服务端将会接收到这个对象的的空对象,但在服务端对接收到的空对象有任何修改之后客户端将会同步变动。
  • inout:表示数据可在服务端与客户端之间双向流通。服务端将会接收到客户端传来对象的完整信息,且客户端将会同步服务端对该对象的任何变动。
3.6.1.2 AIDL文件类型
  • 所有AIDL文件都是以.aidl作为后缀的
  • 根据用途区分,AIDL文件有两种,一种是用于定义接口,以供系统使用来完成跨进程通信;另一种是用于声明parceable对象,以供其他AIDL文件使用,并且这种AIDL文件中声明自定义的Parcelable对象时必须把java文件和自定义的AIDL文件显式的import进来,无论是否在同一包内

3.6.1 AIDL使用

服务端
首先看下目录结构:
在这里插入图片描述
在AndroidStudio中工程目录的Android视图下,右键new一个AIDL文件,默认将创建一个与java文件夹同级的aidl文件夹用于存放AIDL文件,这里命名为Book.aidl,可以将原有抽象方法basicTypes删除:

package com.demo.testaidl;
interface Book {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

新建一个同名的实体类Book实现Parcelable接口:

public class Book implements Parcelable {

    private String name;

    public Book(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "book name:" + name;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(this.name);
    }

    public void readFromParcel(Parcel dest) {
        name = dest.readString();
    }

    protected Book(Parcel in) {
        this.name = in.readString();
    }

    public static final Creator<Book> CREATOR = new Creator<Book>() {
        @Override
        public Book createFromParcel(Parcel source) {
            return new Book(source);
        }

        @Override
        public Book[] newArray(int size) {
            return new Book[size];
        }
    };
}

新建AIDL文件,定义一个接口IBookManager,在这个接口里声明两个方法,分别用于添加Book数据和获取所有Book数据,因为AIDL是接口定义语言,所以不能在AIDL文件里对方法进行实现:

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

然后编译服务端代码,会自动生成一个 IBookManager的java类, 里面书写了Binder相关代码:
在这里插入图片描述
IBookManager代码如下:

public interface IBookManager extends android.os.IInterface
{
  /** Default implementation for IBookManager. */
  public static class Default implements com.demo.testaidl.IBookManager
  {
    @Override public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException
    {
      return null;
    }
    @Override public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException
    {
    }
    @Override
    public android.os.IBinder asBinder() {
      return null;
    }
  }
  /** Local-side IPC implementation stub class. */
  public static abstract class Stub extends android.os.Binder implements com.demo.testaidl.IBookManager
  {
    private static final java.lang.String DESCRIPTOR = "com.demo.testaidl.IBookManager";
    /** Construct the stub at attach it to the interface. */
    public Stub()
    {
      this.attachInterface(this, DESCRIPTOR);
    }
    /**
     * Cast an IBinder object into an com.demo.testaidl.IBookManager interface,
     * generating a proxy if needed.
     */
    public static com.demo.testaidl.IBookManager asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof com.demo.testaidl.IBookManager))) {
        return ((com.demo.testaidl.IBookManager)iin);
      }
      return new com.demo.testaidl.IBookManager.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_getBookList:
        {
          data.enforceInterface(descriptor);
          java.util.List<com.demo.testaidl.Book> _result = this.getBookList();
          reply.writeNoException();
          reply.writeTypedList(_result);
          return true;
        }
        case TRANSACTION_addBook:
        {
          data.enforceInterface(descriptor);
          com.demo.testaidl.Book _arg0;
          if ((0!=data.readInt())) {
            _arg0 = com.demo.testaidl.Book.CREATOR.createFromParcel(data);
          }
          else {
            _arg0 = null;
          }
          this.addBook(_arg0);
          reply.writeNoException();
          return true;
        }
        default:
        {
          return super.onTransact(code, data, reply, flags);
        }
      }
    }
    private static class Proxy implements com.demo.testaidl.IBookManager
    {
      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.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        java.util.List<com.demo.testaidl.Book> _result;
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          boolean _status = mRemote.transact(Stub.TRANSACTION_getBookList, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            return getDefaultImpl().getBookList();
          }
          _reply.readException();
          _result = _reply.createTypedArrayList(com.demo.testaidl.Book.CREATOR);
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
        return _result;
      }
      @Override public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException
      {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        try {
          _data.writeInterfaceToken(DESCRIPTOR);
          if ((book!=null)) {
            _data.writeInt(1);
            book.writeToParcel(_data, 0);
          }
          else {
            _data.writeInt(0);
          }
          boolean _status = mRemote.transact(Stub.TRANSACTION_addBook, _data, _reply, 0);
          if (!_status && getDefaultImpl() != null) {
            getDefaultImpl().addBook(book);
            return;
          }
          _reply.readException();
        }
        finally {
          _reply.recycle();
          _data.recycle();
        }
      }
      public static com.demo.testaidl.IBookManager sDefaultImpl;
    }
    static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
    static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    public static boolean setDefaultImpl(com.demo.testaidl.IBookManager impl) {
      if (Stub.Proxy.sDefaultImpl == null && impl != null) {
        Stub.Proxy.sDefaultImpl = impl;
        return true;
      }
      return false;
    }
    public static com.demo.testaidl.IBookManager getDefaultImpl() {
      return Stub.Proxy.sDefaultImpl;
    }
  }
  public java.util.List<com.demo.testaidl.Book> getBookList() throws android.os.RemoteException;
  public void addBook(com.demo.testaidl.Book book) throws android.os.RemoteException;
}

其中一些重要的类与变量:

  • DESCRIPTOR:Binder的唯一标识,一般用当前Binder的类名表示
  • asInterface():客户端调用,将服务端的返回的Binder对象,转换成客户端所需要的AIDL接口类型对象。返回对象分两种情况:若客户端和服务端位于同一进程,则直接返回Stub对象本身;否则返回的是系统封装后的Stub.proxy对象
  • asBinder():根据当前调用情况返回代理Proxy的Binder对象
  • onTransact():运行服务端的Binder线程池中,当客户端发起跨进程请求时,远程请求会通过系统底层封装后交由此方法来处理
  • Proxy类:服务器的本地代理,客户端通过这个类调用服务器的方法
    Proxy#getBookList:这个方法运行在客户端,当客户端远程调用此方法时,它的内部实现:首先创建该方法所需要的输入型Parcel对象 _data、 输出型Parcel对象_reply 和返回值对象List;然后把该方法的参数信息写入_data 中(如果有参数的话);接着调用transact 方法来发起RPC (远程过程调用)请求,同时当前线程挂起;然后服务端的onTransact方法会被调用,直到RPC过程返回后,当前线程继续执行,并从_reply 中取出RPC过程的返回结果;最后返回 _reply 中的数据。
    Proxy#addBook: 这个方法运行在客户端,它的执行过程和getBookList是一样的,addBook没有返回值,所以它不需要从_reply 中取出返回值。
  • transact():运行在客户端,当客户端发起远程请求的同时将当前线程挂起。之后调用服务端的onTransact()直到远程请求返回,当前线程才继续执行。

编译的时候可能会报错,Book类未被发现啥的,这是因为我们是在src/main/aidl文件夹下创建Book.java的,实际上这将因为找不到Book.java而报错,因为在Android Studio中使用Gradle构建项目时,默认是在src/main/java文件夹中查找java文件的,如果把Book.java放在src/main/aidl对应包名下,自然就会找不到这个文件了,所以需要修改app的build.gradle文件,在sourceSets下添加对应的源文件路径,即src/main/aidl:

 sourceSets {
        main {
            java.srcDirs = ["src/main/java", "src/main/aidl"]
        }
    }

在这里插入图片描述
接着创建一个Service,实现AIDL的接口函数并暴露AIDL接口,这里就叫做AIDLService吧:

public class AIDLService extends Service {

    private final String TAG = "Server";
    private List<Book> bookList;

    public AIDLService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        //因为AIDL方法是在服务端的Binder线程池中执行的,当有多个客户端同时连接时,可能存在多个线程同时访问mStuList对象的情况    
        //CopyOnWriteArrayList支持并发读写,可以保证线程安全
        bookList = new CopyOnWriteArrayList<>();
        initData();
    }


    private void initData() {
        Book book1 = new Book("Android第一行代码");
        Book book2 = new Book("Android开发艺术探索");
        Book book3 = new Book("Android群英传");
        bookList.add(book1);
        bookList.add(book2);
        bookList.add(book3);
    }

    private final IBookManager.Stub stub = new IBookManager.Stub() {

        @Override
        public List<Book> getBookList() throws RemoteException {
            return bookList;
        }

        @Override
        public void addBook(Book book) throws RemoteException {
            if (book != null) {
                bookList.add(book);
            } else {
                Log.e(TAG, "接收到了一个空对象 InOut");
            }
        }

    };

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

}

这里我们考虑单个应用多进程的情况,多应用的话之后也会提一下。AIDLService服务端另起一个进程,在AndroidManifest.xml配置文件中,声明android:process=":remote",即可创建一个新的进程实现单应用多进程,从而模拟进程间通信。

<service android:name=".AIDLService"
            android:process=":remote"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.demo.testaidl.AIDLService"/>
            </intent-filter>
</service>

客户端

客户端和服务端肯定是在不同的进程中,所以客户端要想通过AIDL与远程服务端通信,那么必须也要有服务端的这份AIDL代码。这里分为两种情况:

  • 服务端与客户端是两个独立应用,也就是多应用
    把服务端的aidl文件夹整个复制到客户端的与java文件夹同级的目录下,保持客户端和服务端的aidl文件夹的目录结构一致。这种情况下需要注意的是,如果前面的Book.java文件是放置src/main/java对应包名路径下,则在拷贝aidl文件夹到客户端的同时,也要将对应的Book.java一并拷贝到客户端相同的包名路径下。

  • 我们上面考虑的单应用多进程
    这种情况下因为客户端与服务端同属一个应用,两个进程都可以使用这份AIDL代码,则不需要拷贝。客户端进程即主进程,在MainActivity.java中绑定远程AIDLService,就可以向服务端进程remote发起请求了,修改MainActivity代码:

public class MainActivity extends AppCompatActivity implements View.OnClickListener{
    private Button bind,add,get,unbind;
    private IBookManager mIBookManager;

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //获取远程服务Binder的代理
            mIBookManager = IBookManager.Stub.asInterface(service);
            if(mIBookManager == null){
                return;
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        bind.setOnClickListener(this);
        add.setOnClickListener(this);
        get.setOnClickListener(this);
        unbind.setOnClickListener(this);
    }

    private void initViews() {
        bind = findViewById(R.id.bind);
        add = findViewById(R.id.add);
        get = findViewById(R.id.get);
        unbind = findViewById(R.id.unbind);
    }


    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.bind:
                bindService();
                break;
            case R.id.add:
                addBook();
                break;
            case R.id.get:
                getBookList();
                break;
            case R.id.unbind:
                unbindBookService();
                break;
            default:
                break;
        }
    }

    public void bindService() {
        Intent intent = new Intent();
        intent.setAction("com.demo.testaidl.AIDLService");
        intent.setPackage("com.demo.testaidl");
        startService(intent);
        bindService(intent, mConnection, BIND_AUTO_CREATE);
    }

    public void addBook() {
        try {
            mIBookManager.addBook(new Book("Android安全权威指南"));
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    public void getBookList() {
        try {
            List<Book> books = mIBookManager.getBookList();
            Log.e("Client", "books:" + books.toString());
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindBookService();
    }

    private void unbindBookService() {
        unbindService(mConnection);
        mIBookManager = null;
    }

}

修改布局文件:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/bind"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="绑定服务"/>
    <Button
        android:id="@+id/add"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="添加数据"/>
    <Button
        android:id="@+id/get"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="获取数据"/>
    <Button
        android:id="@+id/unbind"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="解绑服务"/>
</LinearLayout>

效果如下图所示:
在这里插入图片描述
先点击绑定服务,然后获取数据:
在这里插入图片描述
再点击添加数据后,点击获取数据:
在这里插入图片描述
使用完后解绑服务即可。

4.Binder线程池

如果我们的应用有很多模块,而每一个模块都需要和服务端通讯,那么我们也要为每一个模块创建特定的aidl文件,那么服务端service也会产生很多个,显然,如果aidl接口变多,那么service也会跟着变多,那么这样的用户体验就会非常不好。《Android 开发艺术探索》中给出了一个Binder连接池的概念,即利用一个Binder连接池来管理所有Binder,服务端只需要管理这个Binder连接池即可,这样就能实现一个service管理多个Binder,为不同的模块返回不同的Binder,以实现进程间通讯。简单来说Binder线程池将每个模块的Binder请求统一转发到一个远程Service中去执行,从而避免重复创建Service。

具体步骤如下:每个模块创建自己的AIDL接口并实现此接口,然后向服务端提供自己的唯一标识和其对应的Binder对象。服务端只需要一个Service,服务器提供一个queryBinder接口,它会根据业务模块的特征来返回相应的Binder对像,不同的业务模块拿到所需的Binder对象后就可进行远程方法的调用。

详细代码可参考: Android IPC机制(四):细说Binder连接池

本文还参考了以下文章,如有理解错误还请指出:

猜你喜欢

转载自blog.csdn.net/weixin_42011443/article/details/107912431