Android 中的进程通信:AIDL

Android 中的进程

我们知道,每个 Android 程序都是运行在独立的进程当中的,默认进程名称为该程序的包名,通过 adb shell ps 可以查看当前设备中正在运行的进程:

但同时 Android 还为我们提供了 android:process 属性,可以帮我们将同一个程序的不同组件运行在不同的进程当中,之所以要这样做,是因为 Android 系统对于每个应用所占内存是有限制的,进程占用内存越大,往往容易被系统回收,让占用内存大的组件运行在不同的进程当中,可以减少主进程所占的内存,避免被回收(反正网上是这么说的)。

这样一来,就不可避免的产生进程间通信的问题,去面试的时候,进程间通信也会经常被问到,主要有哪些方式?

大致上,Android 系统中的进程通信方法可以分为以下几类:

  • 基于 Binder:
    • AIDL
    • Messenger
    • ContentProvider
  • 文件:不同进程针对同一文件的读写
  • 网络:Socket
  • Bundle :四大组件之间在 Intent 中传递 Bundle 来完成数据传递

关于什么是 Binder,可以看这里 Android Binder机制浅析

AIDL

官网上这样形容 AIDL :

您可以利用它定义发送端与接收端使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。 

并且重点提示:

只有允许不同应用的发送端用 IPC 方式访问服务,并且想要在服务中处理多线程时,才有必要使用 AIDL。 如果您不需要执行跨越不同应用的并发 IPC,就应该通过实现一个 Binder 创建接口;或者,如果您想执行 IPC,但根本不需要处理多线程,则使用 Messenger 类来实现接口。

说明了运用 AIDL 的两个必要条件:

  • 跨进程通信
  • 需要处理多线程

那开始使用 AIDL 吧。

接收端

创建 aidl 接口文件

我们创建两个应用,分别模拟 Server 和 Client,先编辑 Server 端的 AIDL 文件

我们直接在当前 module 上右击,New - AIDL - AIDL File,输入文件名:MyAIDLTest,会发现 AndroidStudio 会自动在该 module 下创建一个 aidl 文件夹,并且会创建一个该工程包名相同的包,刚才创建的 AIDL 文件(MyAIDLTest.aidl)就在这个包当中。

interface IMyAIDLTest {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);
}

系统会默认创建一个方法,通过注释可以看到该方法只是为了向我们演示 AIDL 可以用作参数的数据类型,无用,删之。

我们创建自己的方法:

package com.lixyz.aidltestserver;

interface IMyAIDLTest {
    String getName();
}

这里注意一下包名,先记住就行

然后我们 Rebuild 一下工程,然后我们可以发现在 generatedJava 文件夹当中自动生成了相同的和 AIDL 相同的包名,包名下有一个和 AIDL 文件名相同的 .java 文件。该文件由系统自动生成,和 R 文件一样,我们不要去编辑它。

创建 Service

和正常创建服务的方法大致相同,唯一的区别我们之前 onBind 方法返回的是我们自己创建的继承自 Binder 的对象,而这次返回的是实现 IMyAIDLTest.Stub 接口的对象:

public class AIDLService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AIDLBinder();
    }

    class AIDLBinder extends IMyAIDLTest.Stub {

        @Override
        public String getName() throws RemoteException {
            return "模拟返回的数据";
        }
    }
}

接下来,在清单文件当中注册该 Server:

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

这样写是有问题的,但是我们先这样写,后面遇到问题时再改过来,正好说明一下常见问题

至此,接收端的工作已经完成了,开始写发送端的。

发送端

创建 AIDL 文件

在发送端创建的 aidl 文件内容要和接收端一样。

package com.lixyz.aidlclient;

// Declare any non-default types here with import statements

interface IMyAIDLTest {
    String getName();
}

然后我们在 Activity 当中绑定服务:

        bind.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                bindService(intent, conn, BIND_AUTO_CREATE);
            }
        });

        private ServiceConnection conn = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName name, IBinder service) {
                IMyAIDLTest aidlTest = IMyAIDLTest.Stub.asInterface(service);
                try {
                    Log.d(TAG, aidlTest.getName());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }

            @Override
            public void onServiceDisconnected(ComponentName name) {

            }
    };  

然后我们运行接收端,在运行发送端,点击按钮,进行绑定,看是否会打印 Log,答案肯定不会,会抛出 IllegalArgumentException,因为我们并没有指定 Intent 要访问的目的地。

但是我们要绑定的是其他 APP 的服务,该怎么写呢?

这样写行不行?

        bind.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                intent.setAction("com.lixyz.aidltestserver.AIDLService");
                intent.setPackage("com.lixyz.aidltestserver");
                bindService(intent, conn, BIND_AUTO_CREATE);
            }
        });

还是绑定失败,因为:

规则一:发送端 aidl 接口文件的包名必须和接收端 aidl 文件包名相同

我们将接收端和发送端的包名修改一下,统一为 com.lixyz.aidl,再执行,发现还是没有绑定成功,是因为:

规则二:接收端服务需要保证该服务可以对外交互,也就是说,android:exported 属性需要为 true

规则三:Android 5.0开始,无法使用隐式 Intent 绑定服务,需要设置 Service 所在接收端的包名

这次再绑定,发现 Log 打印,绑定成功!

工作过程

在文章的一开始,就说明了 AIDL 是基于 Binder 实现的,Binder 是什么这里就不展开讲了,因为要展开讲就太长了。在这里你可以将其理解为一个 虚拟设备

在我们绑定本地服务的时候,系统调用本地服务的 onBind 方法,该方法返回与服务进行交互的 IBinder 接口对象。要接收 IBinder,发送端必须创建一个 ServiceConnection 实例,并将其传递给 bindService()ServiceConnection 包括一个回调方法,系统通过调用它来传递 IBinder

在 AIDL 当中,ServiceConnection 同样也需要接受 IBinder,这个 IBinder 是如何来的呢?

以我们刚刚创建的 IMyAIDLTest 为例:

可以看到在系统自动生成的同名 Java 文件当中,主要由两部分组成:

  • 静态抽象类 - Stub
  • 我们自己定义的方法

分析 Stub 类,主要由以下方法构成:

  • 构造方法:Stub
  • 静态方法 asInterface
  • asBinder 方法
  • onTransact 方法
  • 静态内部类 Proxy

Stub 类和 Proxy 代理类都实现了我们创建的接口,在代理类 Proxy 中又实现了我们自己定义的方法。

发送端中,我们通过 IMyAIDLTest.Stub.asInterface(service) 来获取我们定义的 AIDL 接口,一步一步来,看 asInterface 方法:

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

这个方法会判断我们传入的参数是否是本地接口,如果不是,则会调用代理类将其包装成一个代理类对象

接下来我们会调用我们自己添加的 getName 方法,调用的就是代理类当中的 getName 方法,最终会调用 transact 方法,系统会帮我们找到接收端的 AIDL 接口,其 onTransact() 会接收 Client 传递过来的参数,然后在 switch 方法中找到我们要调用的方法,将方法的返回值传递过来,再返回,这样就完成了从发送端到接收端,再从接收端到发送端的一个过程。

如何寻找到接收端的呢?这是 Android Binder 架构的一个知识点,我也迷迷糊糊的讲不清楚,等我搞清楚了再写

Parcel 和传递对象

默认情况下,AIDL 支持传递的数据类型包括:

  • Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)
  • String
  • CharSequence
  • List,但 List 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或我们自己声明的可打包类型。
  • Map,但 Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或我们自己声明的可打包类型。

什么是 我们自己声明的可打包类型 呢?

还记得系统为我们自动生成的 java 文件吗?

在代理类的方法中,_data_reply 都是 android.os.Parcel 类型的,通过查看 API 我们可以看到 Parcel 是我们通过 IBinder 传递消息的容器,支持序列化和反序列化。

官方文档中明确通过 IPC 是可以传递对象的,只需要实现按照以下步骤:

  • 自定义类实现 Parcelable 接口
  • 自定义类实现 writeToParcel 方法
  • 自定义类添加一个名为 CREATOR 的静态字段
  • 创建自定义类的 .aidl 文件

前三步可以通过插件自动完成,高效还安全
但是需要注意的是,插件生成的代码有时候会不包含 readFromParcel 方法,需要我们手动添加,否则会编译不通过
在手动添加时,一定要保证 writeToParcel 和 readFromParcel 方法读写的顺序是一一对应的

所以我们只需要以下几步,就可以在通过 AIDL 传递对象了:

  • 我们自定义的类实现 Parcelable 接口
  • 实现 writeToParcel,它会获取对象的当前状态并将其写入 Parcel
  • 为您的类添加一个名为 CREATOR 的静态字段,这个字段是一个实现 Parcelable.Creator 接口的对象。
  • 最后,创建一个声明可打包类的 .aidl 文件

例如:
自定义 Person 类,并实现 Parcelable 接口,添加 writeToParcelreadFromParcel 方法,添加 CREATOR 字段。

public class Person implements Parcelable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


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

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

    public void readFromParcel(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

    public Person() {
    }

    protected Person(Parcel in) {
        this.name = in.readString();
        this.age = in.readInt();
    }

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

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

创建 Person 的 aidl 文件,这个文件的作用是声明 Personparcelable 的,可以用于 AIDL 传输。

package com.lixyz.bean;

parcelable Person;

修改之前的 AIDL 接口文件:

package com.lixyz.aidl;

// 我们自定义的对象,需要使用 import 在此声明,否则会编译不通过
import com.lixyz.bean.Person;

interface IMyAIDLTest {
    Person getPerson();
}

同样修改 Service 方法:

public class AIDLService extends Service {

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

    @Override
    public void onCreate() {
        super.onCreate();
    }

    class MyBinder extends AIDLTest.Stub {

        @Override
        public Person getPerson() throws RemoteException {
            return new Person("张三", 20);
        }
    }
}

发送端方面依葫芦画瓢,调用就可以了。

需要注意的是,我们创建的自定义类,和声明该类为 parcelable 的 aidl 接口,包的名称必须相同,否则编译会不通过,可以看到我在上面都是新建了一个 com.lixyz.bean 包。

我们接着往下走,之前 Server 端提供的 getPerson 方法,我们新增一个 addPerson 方法,参数为一个 Person 对象:

interface AIDLTest {
    Person getPerson();
    void addPerson(Person person);
}

然后你就会发现,编译出错,为什么呢?官网中这样说:

所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout

也就是说,所有我们自定义的对象作为参数,都必须标记数据的走向:

  • in:表示数据只能从发送端流向接收端
  • out:表示数据只能从接收端流向发送端
  • inout:双向都可以传输

Java 自带的数据类型,只能是 in

但是切记,in、out、inout 只是 对象参数的流向

  • in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动
  • out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动
  • inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

关于in、out 和 inout 的解释,是 copy 自这里:AIDL中的in,out,inout

官网中说,您应该将方向限定为真正需要的方向,因为编组参数的开销极大。具体因为啥,我也不知道,我猜是因为对对象的序列化和反序列化这个过程本身对于内存或者处理器的消耗大吧。

AIDL 中的安全问题

我们知道 AIDL 是用来进行进程之间的通信的,也就是说,你无法预料和你通信的进程是本地进程还是远程进程,事实上,不同的进程连接,也会存在差异,还记得我们分析工具为我们自动生成的那个文件吗?在那个文件里是不是有一个判断,如果是本地线程,直接调用接口,如果是远程,就返回代理类。

官方文档是这样说的:

  • 来自本地进程的调用在发起调用的同一线程内执行。如果该线程是您的主 UI 线程,则该线程继续在 AIDL 接口中执行。如果该线程是其他线程,则其便是在服务中执行您的代码的线程。 因此,只有在本地线程访问服务时,您才能完全控制哪些线程在服务中执行(但如果真是这种情况,您根本不应该使用 AIDL,而是应该通过实现 Binder 类创建接口)。
  • 来自远程进程的调用分派自平台在您的自有进程内部维护的线程池。 您必须为来自未知线程的多次并发传入调用做好准备。 换言之,AIDL 接口的实现必须是完全线程安全实现。
  • oneway 关键字用于修改远程调用的行为。使用该关键字时,远程调用不会阻塞;它只是发送事务数据并立即返回。接口的实现最终接收此调用时,是以正常远程调用形式将其作为来自 Bind

所以我们之前写的代码实际上都是不规范的,因为我们并没有考虑到线程安全问题。那么,我们还需要对接收端的相关代码进行一下修改,上面说了,如果是其他线程,那么那个线程就是在服务中执行相关代码的线程(这样也就产生了多线程问题)

所以我们需要保证相关代码的线程安全问题:

    class MyBinder extends AIDLTest.Stub {
        @Override
        public Person getPerson() throws RemoteException {
            synchronized (this) {
                return new Person("张三", 10);
            }
        }

        @Override
        public Person editPerson(Person person) throws RemoteException {
            synchronized (this) {
                Log.d("TTT", "服务端收到的" + person.getName() + " ||| " + person.getAge());
                person.setName("王五");
                person.setAge(30);
                Log.d("TTT", "服务端修改后的: " + person.getName() + " ||| " + person.getAge());
                return person;
            }
        }
    }

默认情况下,整个调用过程是同步进行的,所以当接收端需要进行大量耗时操作的时候,假如我们在 Activity 的主线程当中发起调用,则会很容易引发 ANR,所以如果我们确定整个调用过程在几毫秒内无法完成,那么尽量把调用放在子线程当中进行。

AIDL 权限问题

假如接收端的 Service 定义了某项权限,也就是说,添加了 android:permission 属性,那么发送端也必须拥有该权限,否则将会出现异常。

接收端:

    <permission
        android:name="com.lixyz.aidl.permission"
        android:protectionLevel="normal" />
    <uses-permission android:name="com.lixyz.aidl.permission" />

        <service
            android:name=".AIDLService"
            android:exported="true"
            android:permission="com.lixyz.aidl.permission">
            <intent-filter>
                <action android:name="com.lixyz.aidltestserver.MyService" />
            </intent-filter>
        </service>    

发送端:

    <uses-permission android:name="com.lixyz.aidl.permission" />
Copyright© 2020-2022 li-xyz 冀ICP备2022001112号-1