AIDL踩坑实战
AIDL理论
AIDL(Android 接口定义语言)可以利用它定义客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。 在 Android 上,一个进程通常无法访问另一个进程的内存。 尽管如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。 编写执行这一编组操作的代码是一项繁琐的工作,因此 Android 会使用 AIDL 来处理。
如需使用 AIDL 创建绑定服务,请执行以下步骤:
- 创建 .aidl 文件
此文件定义带有方法签名的编程接口。
2.实现接口
Android SDK 工具基于您的 .aidl 文件,使用 Java 编程语言生成一个接口。此接口具有一个名为 Stub 的内部抽象类,用于扩展 Binder 类并实现 AIDL 接口中的方法。您必须扩展 Stub 类并实现方法。
3.向客户端公开该接口
实现 Service 并重写 onBind() 以返回 Stub 类的实现。
创建 .aidl 文件
AIDL 使用简单语法,使您能通过可带参数和返回值的一个或多个方法来声明接口。 参数和返回值可以是任意类型,甚至可以是其他 AIDL 生成的接口。
必须使用 Java 编程语言构建 .aidl 文件。每个 .aidl 文件都必须定义单个接口,并且只需包含接口声明和方法签名。
默认情况下,AIDL 支持下列数据类型:
- Java 编程语言中的所有原语类型(如 int、long、char、boolean 等等)
- String
- CharSequence
- List
List 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 可选择将 List 用作“通用”类(例如,List<String>)。另一端实际接收的具体类始终是 ArrayList,但生成的方法使用的是 List 接口。 - Map
Map 中的所有元素都必须是以上列表中支持的数据类型、其他 AIDL 生成的接口或您声明的可打包类型。 不支持通用 Map(如 Map<String,Integer> 形式的 Map)。 另一端实际接收的具体类始终是 HashMap,但生成的方法使用的是 Map 接口。
必须为以上未列出的每个附加类型加入一个 import 语句,即使这些类型是在与您的接口相同的软件包中定义。
定义服务接口时,需要注意:
- 方法可带零个或多个参数,返回值或空值。
- 所有非原语参数都需要指示数据走向的方向标记。可以是 in、out 或 inout。原语默认为 in,不能是其他方向。in 表示由客户端设置,修饰输入参数,非基本类型的输入参数必须使用in修饰。out 表示由服务器端设置,修饰输出参数,非基本类型的输出参数必须使用out修饰。inout 表示既是输入参数,也是输出参数。应该将方向限定为真正需要的方向,因为编组参数的开销极大。
- .aidl 文件中包括的所有代码注释都包含在生成的 IBinder 接口中(import 和 package 语句之前的注释除外)
- 只支持方法;不能公开 AIDL 中的静态字段。
// IRemoteService.aidl
package com.example.android;
// Declare any non-default types here with import statements
/** Example service interface */
interface IRemoteService {
/** Request the process ID of this service, to do evil things with it. */
int getPid();
/** 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);
}
实现接口
android sdk会生成一个以.aidl文件命名的.java接口文件,生成的接口包括一个Stub子类,用于声明.aidl中的所有方法。
以下是一个使用匿名实例实现名为 IRemoteService 的接口(由以上 IRemoteService.aidl 示例定义)的示例:
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
// Does nothing
}
};
现在,mBinder 是 Stub 类的一个实例(一个 Binder),用于定义服务的 RPC 接口。 在下一步中,将向客户端公开该实例,以便客户端能与服务进行交互。
在实现 AIDL 接口时应注意遵守以下这几个规则:
- 由于不能保证在主线程上执行传入调用,因此您一开始就需要做好多线程处理准备,并将您的服务正确地编译为线程安全服务。
- 默认情况下,RPC 调用是同步调用。如果您明知服务完成请求的时间不止几毫秒,就不应该从 Activity 的主线程调用服务,因为这样做可能会使应用挂起(Android 可能会显示“Application is Not Responding”对话框)— 您通常应该从客户端内的单独线程调用服务。
- 您引发的任何异常都不会回传给调用方。
向客户端公开该接口
您为服务实现该接口后,就需要向客户端公开该接口,以便客户端进行绑定。 要为您的服务公开该接口,请扩展 Service 并实现 onBind(),以返回一个类实例,这个类实现了生成的 Stub(见前文所述)。以下是一个向客户端公开 IRemoteService 示例接口的服务示例。
public class RemoteService extends Service {
@Override
public void onCreate() {
super.onCreate();
}
@Override
public IBinder onBind(Intent intent) {
// Return the interface
return mBinder;
}
private final IRemoteService.Stub mBinder = new IRemoteService.Stub() {
public int getPid(){
return Process.myPid();
}
public void basicTypes(int anInt, long aLong, boolean aBoolean,
float aFloat, double aDouble, String aString) {
// Does nothing
}
};
}
现在,当客户端(如 Activity)调用 bindService() 以连接此服务时,客户端的 onServiceConnected() 回调会接收服务的 onBind() 方法返回的 mBinder 实例。
客户端还必须具有对 interface 类的访问权限,因此如果客户端和服务在不同的应用内,则客户端的应用 src/ 目录内必须包含 .aidl 文件(它生成 android.os.Binder 接口 -- 为客户端提供对 AIDL 方法的访问权限)的副本。
当客户端在 onServiceConnected() 回调中收到 IBinder 时,它必须调用 YourServiceInterface.Stub.asInterface(service) 以将返回的参数转换成 YourServiceInterface 类型。例如:
IRemoteService mIRemoteService;
private ServiceConnection mConnection = new ServiceConnection() {
// Called when the connection with the service is established
public void onServiceConnected(ComponentName className, IBinder service) {
// Following the example above for an AIDL interface,
// this gets an instance of the IRemoteInterface, which we can use to call on the service
mIRemoteService = IRemoteService.Stub.asInterface(service);
}
// Called when the connection with the service disconnects unexpectedly
public void onServiceDisconnected(ComponentName className) {
Log.e(TAG, "Service has unexpectedly disconnected");
mIRemoteService = null;
}
};
以上内容摘自https://developer.android.com/guide/components/aidl#CreateAidl
动手实战
说实话,这些理论我是没有看明白,目前我只希望知道要如何使用,就算完成任务了(最好还是要理解了)。
服务端
首先编写服务端代码,新建一个项目,我的叫做AIDLServer
。创建过程如下:
全部完成后系统就会自动创建一个工程,此时已经埋下了第一个坑。创建完成后的项目如图所示。
项目完成的目录结构.png
接下来就来创建aidl文件,创建一个名为student的aidl文件(当年学数据库的时候,就是拿各种学生表,课程表来举例,找回上学的感觉),创建过程如下:
1.两个手指点击触控板,弹出并作如下选择
创建AIDL文件
2.输入AIDL文件名称,这里输入Student
输入AIDL文件名
3.系统自动创建AIDL文件
系统自动创建的AIDL文件
4.创建Student类,用来存储姓名和年龄,使用插件(android parcelable code generator ,可以在studio中直接下载,下载完成后在mac中的使用方法是按com+N,选择Parcelable即可)自动实现Parcelable接口,手动添加如下代码
/**
* <p>从parcel中读取,顺序与write一致</p>
* 如果要支持为 out 或者 inout 的定向 tag 的话,需要实现 readFromParcel() 方法
* @param dest
*/
public void readFromParcel(Parcel dest) {
name = dest.readString();
age = dest.readInt();
}
此时,Student.java类完成。完整代码如下:
package com.tom.aidlserver;
import android.os.Parcel;
import android.os.Parcelable;
/**
* <p>Title: Student</p>
* <p>Description: </p>
*
* @author tom
* @date 2018/11/20 16:31
**/
public class Student implements Parcelable {
private String name;
private int age;
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.name);
dest.writeInt(this.age);
}
/**
* <p>从parcel中读取,从parcel中读取,顺序与write一致</p>
* 如果要支持为 out 或者 inout 的定向 tag 的话,需要实现 readFromParcel() 方法
* @param dest
*/
public void readFromParcel(Parcel dest) {
name = dest.readString();
age = dest.readInt();
}
public Student() {
}
protected Student(Parcel in) {
this.name = in.readString();
this.age = in.readInt();
}
public static final Parcelable.Creator<Student> CREATOR = new Parcelable.Creator<Student>() {
@Override
public Student createFromParcel(Parcel source) {
return new Student(source);
}
@Override
public Student[] newArray(int size) {
return new Student[size];
}
};
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 String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
5.回到Student.aidl,作出如下修改
//引入一个序列化对象Student供其他AIDL文件使用
//Student.aidl 与Student的包名应该要相同
package com.tom.aidlserver;
//parcelable 要小写
parcelable Student;
6.按照相同的套路,创建StudentManager.aidl文件,并修改如下:
// StudentManager.aidl
package com.tom.aidlserver;
// Declare any non-default types here with import statements
import com.tom.aidlserver.Student;
interface StudentManager {
//获取学生总数
List<Student> getStudents();
//添加学生
void addStudent(in Student student);
}
7.创建服务端代码AIDLService
,代码如下:
package com.tom.aidlserver;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
public class AIDLService extends Service {
public final String TAG = this.getClass().getSimpleName();
//包含Student对象的list
private List<Student> mStudentList = new ArrayList<>();
//由AIDL文件生成的StudentManager.Stub
private final StudentManager.Stub mStudentManager = new StudentManager.Stub() {
@Override
public List<Student> getStudents() throws RemoteException {
synchronized (this) {
if (mStudentList != null) {
return mStudentList;
}
}
return new ArrayList<>();
}
@Override
public void addStudent(Student student) throws RemoteException {
synchronized (this) {
if (mStudentList == null) {
mStudentList = new ArrayList<>();
}
if (student == null) {
Log.d(TAG, "student is null");
student = new Student();
}
//如果名字为Tom,修改年龄为18
if (TextUtils.equals(student.getName().toString().trim(), "Tom")) {
student.setAge(18);
}
//假设学生不能重名
if (!mStudentList.contains(student)) {
mStudentList.add(student);
}
Log.d(TAG, "Student: " + student.toString());
}
}
};
@Override
public void onCreate() {
super.onCreate();
Student student = new Student();
student.setName("小明");
student.setAge(10);
mStudentList.add(student);
}
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG, String.format("on bind,intent = %s", intent.toString()));
return mStudentManager;
}
}
代码部分就写完了,还需要在AndroidManifest.xml
文件中添加如下内容才算完成。
<service
android:name=".AIDLService"
android:enabled="true"
android:exported="true">
<intent-filter >
<action android:name="com.tom.aidlserver.ACTION_AIDL_SERVICE"/>
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</service>
这样,这个service就可以供其他应用使用了。给其他应用使用,我选择的是提供jar 包给第三方,这里涉及到把服务端代码打包成jar包的情况,此时,就是解决第一个坑的时候了。
1.打开build.gradle(Module:app)
,第一行修改为:
apply plugin: 'com.android.library'
2.把applicationId
这一行注释掉。此时的文件如图所示
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
// applicationId "com.tom.aidlserver"
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
3.编译aar包给第三方应用使用。
点击右边的Gradle
,按图所示,双击,在outputs下的aar包,就是我们需要的aar包。
生成的aar包
如果生成过程有错误,可以尝试把整个
build
文件夹删掉,再来执行。至此,整个服务端的代码都ok了,把aar包复制出来,然后把第1,2步的修改改回去,安装到机器上,有一点需要注意的是,执行配置哪里选择app,如图:
修改执行配置为app
客户端
客户端就比较简单了,直接创建就可以了,我的项目名字叫做AIDLClient
客户端输出日志:
12-31 19:43:53.259 7236-7236/com.tom.aidlclient E/MainActivity: Student{name='Tom', age=32}
服务端输出日志:
12-31 19:43:53.258 6215-6233/com.tom.aidlserver D/AIDLService: Student: Student{name='Tom', age=18}
从日志可以看到,已经将年龄修改为18了。
客户端的代码比较简单,新建一个libs包,把aar包扔进去,在gradle文件添加一行,我将导出的aar改名了,这里的名字和libs里面的对应就好了。
implementation(name: 'aidlserver', ext: 'aar')
给出MainActivity的完整代码
package com.tom.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.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Toast;
import com.tom.aidlserver.Student;
import com.tom.aidlserver.StudentManager;
import java.util.List;
public class MainActivity extends AppCompatActivity {
//由aidl生成的java类
private StudentManager mStudentManager = null;
//标志当前与服务端连接状况的布尔值,false未连接,true连接中
private boolean mBound = false;
//包含Student对象的list
private List<Student> mStudentList;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
public void addStudent(View view) {
if (!mBound) {
attemptToBindService();
Toast.makeText(this, "当前与服务端处于未连接状态,正在尝试重连,请稍后再试", Toast.LENGTH_SHORT).show();
return;
}
if (mStudentManager == null) return;
Student student = new Student();
student.setName("Tom");
student.setAge(32);
try {
mStudentManager.addStudent(student);
Log.e(getLocalClassName(), student.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
protected void onStart() {
super.onStart();
if (!mBound) {
attemptToBindService();
}
}
@Override
protected void onStop() {
super.onStop();
if (mBound) {
unbindService(mServiceConnection);
mBound = false;
}
}
private void attemptToBindService() {
Intent intent = new Intent();
intent.setAction("com.tom.aidlserver.ACTION_AIDL_SERVICE");
intent.setPackage("com.tom.aidlserver");
bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
}
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mStudentManager = StudentManager.Stub.asInterface(service);
mBound = true;
if (mStudentManager != null) {
try {
mStudentList = mStudentManager.getStudents();
Log.d(getLocalClassName(),"获取到学生数据: " + mStudentList.toString());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
mBound = false;
}
};
}
布局文件activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加学生"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:onClick="addStudent"/>
</android.support.constraint.ConstraintLayout>
整体就是这样了,自己动手搞一波,会用,理解的话我再慢慢来学习把。