概览
什么是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 只对不同进程的调用是异步的,如果本地进程依然是同步调用。