AIDL介绍

1,496 阅读7分钟

概览

什么是AIDL?

Android 接口定义语言(AIDL),你可以利用它定义客户端与服务均认可的编程接口,以便二者使用进程间通信(IPC) 进行互相通信。在 Android 中,一个进程通常无法访问另一个进程的内存。因此,为进行通信,进程需将其对象分解成可供操作系统理解的原语,并将其编组为可供您操作的对象。编写执行该编组操作的代码较为繁琐,因此 Android 会使用 AIDL 为您处理此问题。

内化理解:AIDL就是Android跨进程通信的一种方案,底层原理是Binder机制, 它是以接口定义的方式能让你把跨进程代码写的可读性更强。

什么场景需要AIDL?

前面已经提到了在跨进程中会使用到 AIDL,那具体到哪些场景呢?

同 App 不同进程

这种情况一般会是大型 App 做的,因为业务过多运行时内存会过大,所以把不同模块拆分开来。例如负责长连接的Service,可以是推送、后台音乐播放等业务。

不同 App 不同进程

App 之间的通信,也只能用跨进程方案。 例如 B app 依赖于 A app的某个功能,那么就需要跨进程进行通信。

sdk 独立进程

在做 sdk 提供给三方 app 时其实也是可以用独立进程的, 尤其是这个sdk运行占用内存特别大的时候。这个时候就要处理好 app 给 sdk 传参以及 sdk 给 app 的回参都需要走一遍跨进程通信方案。

AIDL的使用

设计AIDL接口时需注意以下几点:

  • 如果调用来自本进程的线程,那么此时被调用的函数还在该线程中。如果调用来此其他进程,那么这个线程是该其他进程内部维护的线程池中分配的,因此是一个未知的线程,如果要做ui操作需要注意切换到主线程。
  • oneway 关键字,可以让此方法在调用后直接返回,而不需要等到其他进程返回结果。对于一些无需做回调的方法是推荐加oneway关键字的。
  • 跨进程调用一定有客户端和服务端,一个进程既可以是服务端也可以是客户端,这是看具体业务设计的需求。AIDL接口也是必须要在客户端和服务端各自实现一摸一样的一份,当然如果你是在实现同 App 不同进程或许就不需要再声明多余的一份接口。

接下来我们设计一个 不同 App 不同进程 的AIDL案例,将 A app 填入的个人信息数据传给 B app 并显示到页面。代码基本会包括在本文。

1. 如何创建 .aidl 文件

我们先创建一个服务端的app,然后去创建一个通过 Android Studio 创建一个 aidl 文件,它会包含一个 aidl 的包名、接口声明和接口方法声明,我们在其基础上修改即可。我们先假设用户信息只有姓名和年龄,那么我们的 aidl 接口应该是这样的。

package com.example.service;

interface IUserInfo {
    void getUserInfo(String name, int age);
}

然后我们将项目编译构建一遍,此时在 build/intermediates/javac/debug/classes/com/example/service 目录下会生成同名的 .class 文件,仔细看能发现核心就是实现 Binder 的 Stub 抽象类,aidl 的设计讲白了就是实现 Binder 的模板,让你减少些重复代码。

我们先看下 aidl 接口的方法入参,这里有一些规则:

  • Java 中所有的基本类型都可以(如 int, long, char, boolean等)
  • String
  • CharSquence
  • List。List中的所有元素必须是以上列表中支持的数据类型,或者由 aidl 生成的其他接口或 Parcelable 类型。你可以选择将 List 用作泛型类。尽管生成的方法旨在使用 List 接口,但另一方实际接收的具体类始终是 ArrayList。
  • Map。Map 中的所有元素必须是以上列表中支持的数据类型,或者你所声明的由 aidl 生成的其他接口或 Parcelable 类型。不支持泛型 Map(如Map<String, Integer> 形式的 Map)。尽管生成的方法旨在使用 Map 接口, 但另一方实际接收的具体类始终是 HashMap。

自定义传参其实非常容易,具体看之后的 Parcelable 介绍。

2. 实现接口

我们实现由 aidl 生成的 stub 抽象类。

private final IUserInfo.Stub stub = new IUserInfo.Stub() {
    @Override
    public void getUserInfo(String name, int age) throws RemoteException{
        // 在异步线程中
        Log.d("UserService", "UserInfo -> name=" + name + "; age=" + age);
    }
};

但绝对要注意的是此时 getUserInfo 方法是在异步线程中。

3. 向客户端公开接口

服务端使用 Service 作为 Binder 的连接纽带。

public class UserService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }
    private final IUserInfo.Stub stub = new IUserInfo.Stub() {
        @Override
        public void getUserInfo(String name, int age) throws RemoteException {
            // 在异步线程中...
            Log.d("UserService", "UserInfo -> name=" + name + "; age=" + age);
        }
    };
}

我们还需要将这个 Service 在 AndroidManifest.xml 中声明下。

<service android:name="com.example.service.UserService"
    android:exported="true">
    <intent-filter>
        <action android:name="com.example.service"></action>
        <category android:name="android.intent.category.DEFAULT"></category>
    </intent-filter>
</service>

在客户端的调用也比较简单,就是常规的 bindService 方法隐式绑定一个 Service。

val intent = Intent();
// 填入包名和类全名
intent.component = ComponentName("com.example.interprocess", "com.example.service.UserService")
bindService(intent, connection, Context.BIND_AUTO_CREATE);

接下来看看 ServiceConnection 的实现:

private val connection = object: ServiceConnection{
    override fun onServiceDisconnected(className: ComponentName?) {
        mService = null;
    }
    override fun onServiceConnected(className: ComponentName?, service: IBinder?) {
    		// 如果是同进程则直接获取 stub 的实现类,否则通过代理类进行远程调用,后续的Binder文章可以解释。
        mService = IUserInfo.Stub.asInterface(service);
    }
}

到此为止通道已经建立好了。可以尝试从 A app 提交用户信息到 B app 试试。

我们看到客户端也有 aidl 文件的副本所生成的 IUserInfo 的实例 mService,不过这个 mService 实际上是个远程代理,在使用方看起来就会像是调用一个普通的 api 方法。

 mService?.getUserInfo(name, age);

至此 aidl 成功跨进程通信,使用起来非常简单,但其底层的 Binder 可是非常精妙的。最后我们看下跨进程传递对象。

4. 传递对象

实际应用中数据结构一定是复杂的,支持跨进程传递对象显得尤其重要,如本文的例子,可能 UserInfo 不仅仅只是 name, age,后续可能还会增加 sex, height 等其他属性,不可能每次都去修改接口参数。支持跨进程传递的对象一定要支持 Parcelable 协议的类。

  • 让类实现 Parcelable 接口。
  • 实现 writeToParcel, 它会获取对象的当前状态并将其写入 Parcel。
  • 为你的类添加 CREATOR 的静态字段,该字段是实现 Parcelable.Creator 接口的对象。
  • 最后,创建声明 Parcelable 类的 .aidl 文件。

声明实现 Parcelable 接口的类:

public final class User implements Parcelable {
    private String name;
    private int age;
    private String sex;
    private String height;
    protected User(Parcel in) {
        readFromParcel(in);
    }

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

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

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

    @Override
    public void writeToParcel(Parcel out, int i) {
    		// 需要按序写入
        out.writeString(this.name);
        out.writeInt(this.age);
        out.writeString(this.sex);
        out.writeString(this.height);
    }

    public void readFromParcel(Parcel in){
    		// 需要按序读出
        this.name = in.readString();
        this.age = in.readInt();
        this.sex = in.readString();
        this.height = in.readString();
    }
}

这个实现 Parcelable 的对象类也是两端各有一个。同样的声明 Parcelable 类的 .aidl 文件也需要两端都有。

package com.example.service;
parcelable User;

值得注意的是 User.aidl 文件和 User.java 文件的包名需要一致。

最后再调整下客户端和服务端原先的 IUserInfo.aidl 文件:

package com.example.service;
import com.example.service.User;

interface IUserInfo {
    void getUserInfo(in User user);
}

5. in out inout 关键字

可以看到在编写 aidl 文件时方法的入参我加了 in 关键字,这几个关键字有什么意义呢?

  • in 表示只能从客户端流向服务端,服务端对流入参数的修改不会同步到客户端。
  • out 表示服务端流入的数据修改会同步到客户端。
  • inout 表示一端修改都会同步到另一端。

6. oneway 关键字

最开始提到过,oneway 是用来异步调用的,一般不需要返回数据给客户端时,可以直接用 oneway 修饰接口方法,如本文的例子:

package com.example.service;
import com.example.service.User;

interface IUserInfo {
    oneway void getUserInfo(in User user);
}

无论服务端的 getUserInfo 在干什么,客户端都不会等待。

使用 oneway 需要注意几点:

  • oneway 修饰的方法不能有返回值。
  • oneway 修饰的方法不能有 out 类型的入参。
  • oneway 只对不同进程的调用是异步的,如果本地进程依然是同步调用。

参考

Android 接口定义语言(AIDL)

船长的笔记