文本将介绍如何在 Android 中,使用 AIDL 进行跨进程通信。
什么是 AIDL?
AIDL 是 Android 提供的一种进程间通信机制,专门用于处理在不同应用或进程之间的通信。当你需要让一个应用访问另一个应用的服务,并且涉及多线程处理时,AIDL 就派上用场了。AIDL 采用 C/S 模式实现通信,其中 Client (客户端)端为发送方,Server(服务端)为接收方,服务端应用(或进程)的主要作用是暴露方法给其他应用进行调用,客户端的主要作用是调用其他应用的方法,客户端通过绑定服务端的 Service 来进行交互。
Android 中对于 AIDL 的介绍网址:https://developer.android.com/develop/background-work/services/aidl
官方文档中有一段特别说明:
只有在需要不同应用的客户端通过 IPC 方式访问服务,并且希望在服务中进行多线程处理时,您才有必要使用 AIDL。如果您无需跨不同应用执行并发 IPC,请通过实现 Binder 来创建接口。如果您想执行 IPC,但不需要处理多线程处理,请使用 Messenger 实现接口。无论如何,请务必先了解绑定服务,然后再实现 AIDL。
可以看到,Android 并不希望开发者过多的使用 AIDL,“只有当你允许来自不同的客户端访问你的服务并且需要处理多线程问题时你才必须使用 AIDL”,什么意思呢?大白话说就是“非必要不用”,为什么呢?虽然 AIDL 能提供进程间通信的能力,但是其造成的性能开销是不小的,过度使用对于移动端设备并不友好,如果不需要做复杂通信,使用 Messager 是更好的选择。
如何使用 AIDL?
新建子模块
在新建的项目中(或已有的项目中),新建一个 Module 作为 AIDL 服务端,将模块命名为 aidlserver
。
新建 aidl 文件
在 aidlserver
模块中,新建 aidl 文件。首次新建 aidl 文件时,可能会遇到无法新建的问题:aidl file requires setting the buildfeatures aidl to true in the build file
这时需要在 aidlserver 模块的 build.gradle 文件中,新增以下配置:
buildFeatures {
aidl true
}
然后重新 Sync,这样才能正常新建 aidl 文件。
我们将 aidl 文件命名为 IRemoteService,表示这是服务端的 aidl 文件:
新建 aidl 文件完成后,Android Studio 的项目工程目录下,会生成一个 aidl 文件夹,包含我们刚刚新建的 aidl 文件,可以看到,这其实就是一个接口,包含一个默认的方法 baseTypes()
,入参也就是 java 中的 8 种基本数据类型,表明 AIDL 支持在 IPC 中传递这 8 种类型的数据(实际上不止,AIDL 还支持List
和 Map
)。
自定义 aidl 接口方法
在 IRemoteService 中,可以自定义接口方法,用来接收客户端要向服务端通信传递的数据,这里我们以手机支付为例,新建一个getPay
接口方法,包含 int
类型的 status
(支付状态),String
类型的 message
(支付说明):
void getPay(int status, String message);
这个方法也就是服务端要向客户端暴露的方法。
定义好接口后,需要把这个接口公开给客户端,以便让客户端进行绑定,最主要的做法是创建一个 Service 并实现 onBind()
方法,从而返回生成的 Stub 的类实例。我们在 aidlserver 模块创建一个 RemoteService.java 文件,并让该类继承于 Service 类,表明其是一个后台服务:
在 RemoteService
类中,使用内部类 Stub 方式实现 IRemoteService
接口,并实现我们之前写的自定义接口方法,完整代码如下:
public class RemoteService extends Service {
public static final String TAG = "RemoteService";
@Nullable
@Override
public IBinder onBind(Intent intent) {
return binder;
}
private final IRemoteService.Stub binder = new IRemoteService.Stub() {
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
Log.d(TAG, "basicTypes anInt:" + anInt + ";aLong:" + aLong + ";aBoolean:" + aBoolean + ";aFloat:" + aFloat + ";aDouble:" + aDouble + ";aString:" + aString);
}
@Override
public void getPay(int status, String message) throws RemoteException {
Log.d(TAG, "getPay:status: " + status + " message: " + message);
}
};
}
注册服务端的 Service
接下来还需要在 Manefest 文件中注册我们创建的这个 Service,否则客户端无法绑定服务。
<service android:name=".RemoteService"
android:exported="true"
android:enabled="true"
>
<intent-filter>
<action android:name="com.example.sstest.aidl" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</service>
创建客户端模块
再创建一个客户端模块,命名为 aidlclient,表示是进行进程间通信时的客户端:
创建客户端的 aidl 文件
和服务端的 aidl 文件创建步骤一样,但是这里有一个问题,为了能正常通信,Android 要求客户端和服务端的 aidl 文件包名和文件名必须保持一致,但在客户端模块中新建 aidl 文件无法保证这一点,可以看到,客户端模块中,aidl 文件所在的包名是客户端模块的包名:
所以更方便的方式,是直接将服务端模块中,包含 aidl 文件的目录一起复制到客户端模块中,即可避免这个问题:
如上图所示,两个aidl文件必须完全一致。
客户端调用服务
在客户端模块的 MainActivity 界面,添加一个按钮布局,给按钮的点击事件回调中添加请求和服务端通信的代码:
布局代码如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/bindServiceBT"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="尝试连接"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
MainActivity 逻辑代码如下:
package com.example.aidlclient;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.util.Log;
import android.widget.Button;
import androidx.appcompat.app.AppCompatActivity;
import com.example.aidlserver.IRemoteService;
public class MainActivity extends AppCompatActivity {
public static final String TAG = "MainActivity";
boolean hasBound = false;
private IRemoteService iRemoteService;
private Button bindServiceBT;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
bindServiceBT = findViewById(R.id.bindServiceBT);
bindServiceBT.setOnClickListener(v -> {
if (!hasBound) {
Intent intent = new Intent();
intent.setAction("com.example.sstest.aidl");
intent.setPackage("com.example.aidlserver");
boolean result = bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
Log.d(TAG, "onClick: " + result);
if(result) hasBound = true;
} else {
unbindService(mConnection);
hasBound = false;
bindServiceBT.setText("尝试连接");
}
});
}
ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected -> name: " + name.getPackageName());
iRemoteService = null;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected -> name: " + name.getPackageName() + " service: " + service.toString());
iRemoteService = IRemoteService.Stub.asInterface(service);
try {
int pid = iRemoteService.getPid();
int currentPid = Process.myPid();
Log.d(TAG, "currentPID: " + currentPid + ", remotePID: " + pid);
iRemoteService.basicTypes(1, 123, true, 1.1f, 1.11,
"来自客户端的消息...");
iRemoteService.getPay(1, "支付成功");
} catch (RemoteException e) {
e.printStackTrace();
}
bindServiceBT.setText("取消连接");
}
};
}
这段代码的效果是,用户点击了界面上的“尝试连接”按钮后,客户端会通过 AIDL 机制,向服务端 APP 发送一段信息,服务端接收到消息后,进行后续操作,从而完成两个不同进程之间的数据通信,logcat 打印如下:
D onClick: true
D onServiceConnected -> name: com.example.aidlserver service: android.os.BinderProxy@e5178d6
D currentPID: 8164, remotePID: 7572
D basicTypes anInt:1;aLong:123;aBoolean:true;aFloat:1.1;aDouble:1.11;aString:来自客户端的消息...
D getPay:status: 1 message: 支付成功
最后一条打印表明,服务端 APP 成功收到了来自客户端的消息,从打印也可以看出,确实是来自两个不同进程之间的通信。
server service: android.os.BinderProxy@e5178d6
D currentPID: 8164, remotePID: 7572
D basicTypes anInt:1;aLong:123;aBoolean:true;aFloat:1.1;aDouble:1.11;aString:来自客户端的消息...
D getPay:status: 1 message: 支付成功
最后一条打印表明,服务端 APP 成功收到了来自客户端的消息,从打印也可以看出,确实是来自两个不同进程之间的通信。