辉夜的博客

繁花似锦,辉夜如昼

0%

开发AIDL示例项目的完整流程

科研的第二个任务是AIDL和binder机制相关内容,b站上找到了binder源码剖析的很好的讲解,但是AIDL的很多博客都已经过时了。
由于本人没有任何安卓开发经验,所以很需要一步一步示范的教程。在ytb上找到了一个教程,基于此解决了Android11以后bindService无法绑定成功、不会回调onServiceConnected的问题,经测试可以在Android12 (Samsung G9910)上跑通

AIDL概述

AIDL是安卓为进程间通讯留下的接口,和其他RPC框架一样,AIDL也包括代理层,协议层和通讯层。代理层是安卓的跨进程通讯机制Binder,协议层是安卓接口定义语言AIDL,代理层除了AIDL自动生成的存根代码之外,还需要手动实现服务的绑定以及服务并将存根转换为服务接口。这就构成了整个AIDL的框架。
具体实现AIDL的过程分为以下几步:

  1. 创建服务端项目,编写AIDL
  2. 注册服务,实现服务方法并回传服务存根
  3. 创建客户端项目,拷贝AIDL
  4. 设计客户端UI
  5. 在客户端实现业务逻辑
  6. 在客户端注册queries

注意,缺少这一步将无法在Android11以上的机器中绑定服务

  1. 在客户端实现服务绑定并用AIDL存根作为通讯接口
  2. 利用存根中的方法实现需求功能

在本例中,我们将实现一个进行四则运算计算器,以及一个计算器的服务器。计算器通过绑定到服务器上的服务来完成计算功能

阅读须知

本文可以仅需要RPC基础和c语言基础,对于java中类的继承、抽象类、方法等概念只需初步了解,无需Android开发经验

示例开发

先决条件:
Android Studio
Android Studio 虚拟机或开启了USB调试功能的Android实体机

安装android虚拟机的方法

  • 首先找到设备管理器

devicemanager

  • 然后点击创建设备

createdevice

  • 没有特殊需求就选默认硬件,直接点next
  • 操作系统映像选默认的(和硬件匹配的)点Download,许可协议选accept然后再点next就开始下载了。我这里是已经装好了

image

开启USB调试的方法

这个各个设备不太一样,一般要先在设备信息界面点击某个信息十次解锁,比如我是点击设置-设备信息-软件信息-编译编号解锁开发者模式

解锁了之后在设置-开发者选项里找到USB调试打开就可以了。

1.创建服务端项目,编写AIDL代码

创建新项目

从左上角File-New-New Project新建项目,注意选择”Empty Activity”

newproject

接下来可以点next,但在最后的界面记得把语言改成java,项目名字我这里叫做AIDLServer

java

创建新的包

在项目列表里选择java目录,右键,点击new,点击Package,在目录结构里选main

newpackage

给它起个名字,注意过会需要创建这个包的一个副本,所以名字别起太怪,不方便检查,这里叫做AidlPackage

创建新的AIDL文件

刚才的创建的包上右键创建AIDL文件,名字也别起太怪,这里叫做IAidlDemo

newaidlfile

应该能在新建的Aidl文件中看到如下的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
// IAidlDemo.aidl
package AidlPackage;

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

interface IAidlDemo {
/**
* 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);
}

编写接口定义

我们要把计算的过程委托给远程服务器,也就是传递两个运算数和一个运算符,接受一个结果。方便起见,规定运算是整数运算,并且用1 2 3 4表示+ - * /,这样我们就形成了如下接口:

1
2
3
4
5
6
7
8
// IAidlDemo.aidl
package AidlPackage;

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

interface IAidlDemo {
int calculate(int v1, int v2, int op);
}

然后需要点击Build-Rebuild Project或者点击右上角配置栏左侧的绿色小锤子进行Make Project。注意,每次修改AIDL文件都要进行rebuild
到此为止,我们的服务应用程序框架就创建完成。

2.注册服务,实现方法

在APP里右键创建新的Service,注意不要Service(Intent Service),在接下来的窗口里enabledexported都要选中

newservice

新建存根

打开新建的服务的java代码app/java/AidlPackage/<ServiceName>.java,为<ServiceName>类添加一个存根变量

1
2
private final IAidlDemo.Stub
...

注意,这里按下.之后,应该提示后面的Stub类型,如果没有提示就说明刚才没有进行Rebuild!点击下图所示绿色小锤子进行Rebuild

rebuild

确认自己rebuild完成之后继续写。

1
private final IAidlDemo.Stub = new IAidlDemo.

现在应该能自动补全Stub了,按Tab键之后,就应该出现类似下面的内容:

1
2
3
4
5
6
7
8
private final IAidlDemo.Stub stub = new IAidlDemo.Stub() {
@Override
public int calculate(int v1, int v2, int op) throws RemoteException {
return 0;
}
};
//不会自动打分号

注意到calculate正是我们之前定义的接口定义语言,因为AIDL生成的存根类型是个抽象类,因此系统会自动提醒我们复写其中的calculate方法。

实现方法

接下来,我们就来修改这个存根的内部的方法,这里就简单的复制一份代码吧!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private final IAidlDemo.Stub stub = new IAidlDemo.Stub() {
@Override
public int calculate(int v1, int v2, int op) throws RemoteException {
switch (op) {
case 1:
return v1 + v2;
case 2:
return v1 - v2;
case 3:
return v1 * v2;
case 4:
return v1 / v2;
default:
Log.e("Error: ","Invalid Operator");
return 0;
}
}
};
//记得打分号

由于这是一个远程方法,因此默认可能会抛出一个远程异常,不过我们不必管他,一般不会出现这种情况。

传递存根

<ServiceName>.java中应该还有一个默认的方法onBind:

1
2
3
4
5
@Override
public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("Not yet implemented");
}

这是因为我们的服务继承了Service抽象类,因此需要实现onBind方法,也就是指定当服务绑定时要进行的操作。我们只需要返回刚才创建的存根即可。

1
2
3
4
@Override
public IBinder onBind(Intent intent) {
return stub;
}

到此为止我们的服务端项目就编写完成,可以跑一下试试看:

server

虽然没有UI界面,但是服务已经就绪了~

3.创建客户端项目,拷贝AIDL

新建客户端项目可以单开一个project也可以就在这个project里操作,这里选择单开一个project。过程就不再赘述,注意仍然用Empty Activity作为界面。

这里仍然在app\java上右键,选New-Package新建包,注意包名要与刚刚创建的包名相同
在包上右键,新建Aidl文件,注意文件名要和刚刚创建的文件相同
复制刚刚的Aidl文件内容,粘贴到新的Aidl文件里
包名、文件名、文件内容完全相同。
copyaidl

4.设计客户端UI

打开activity_main.xml默认在创建项目的时候就会有这个文件生成,如果不小心关掉了就从左侧项目目录app/res/layout/目录下打开。
请检查一下这个目录,确保其中只有activity_main.xml。在右上角选择以Code方式查看该文件的内容:
layout

文件内容应该如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?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:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

这个xml制定了应用程序主界面的UI,如果layout文件夹里有多个文件或者这个.xml文件的内容和上述内容不一样,那么很有可能是因为你没有选择EmptyActivity类型的项目,你可自行判断是否需要重建项目。如果你不清楚这个.xml文件的具体行为,可以考虑重建。

接下来我们还是在右上角将视图切换到View,或者你也可以直接复制本项目的.xml文档,直接跳到实现业务逻辑的部分

修改布局

默认的布局是constraint,这样图标的上下左右位置都需要指定,很麻烦,于是我们把所有的androidx.constraintlayout.widget.ConstraintLayout修改为RelativeLayout

1
2
3
4
5
6
7
8
9
  <?xml version="1.0" encoding="utf-8"?>
- <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ <RelativeLayout xmlns: xmlns:android="http://schemas.android.com/apk/res/android"

...


- </androidx.constraintlayout.widget.ConstraintLayout>
+ </RelativeLayout>

如果修改成功,Android应该没有任何Warning,并且你会发现Hello World从原来的居中变为出现在左上角。(要查看.xml的效果,你可以在右上角选择SplitView模式)

添加TextView

我们需要一个文本框来显示结果,把下面这段话粘贴到.xml中,并且删除Hello World(从< 开始删除到 >为止)

1
2
3
4
5
6
7
8
9
10
11
<TextView
android:id="@+id/display_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_margin="20dp"
android:background="#FDF171"
android:hint="Result"
android:padding="15dp"
android:textSize="20sp"
android:textStyle="bold" />

现在你应该看到这样的文本框:

textview

所以TextView就是一个文本框,我们设置了默认显示的字符串(hint)、背景颜色、字号、字体等内容信息,还设置了长宽、水平居中、边距等定型定位尺寸。我们可以在java代码中和TextView进行沟通,比如通过setText方法来设置要显示的内容,这样我们就可以把结果输出到这个文本框里。
还有值得注意的一点是,每个组建都拥有自己的id,比如@+id/display_result就对应我们刚刚的文本框,因为我们的第一行就指明了他的id。这个id1可以在xml中描述组件之间的相对关系,也可以在java中用于绑定组件信息。

添加EditText

我们需要两个文本框来接受输入的运算数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<EditText
android:id="@+id/enter_first_value"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_below="@+id/display_result"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:ems="10"
android:hint="Enter First Value"
android:imeOptions="actionNext"
android:inputType="number"
android:maxLength="5"
android:singleLine="true" />

<EditText
android:id="@+id/enter_next_value"
android:layout_width="200dp"
android:layout_height="50dp"
android:layout_below="@+id/enter_first_value"
android:layout_marginStart="20dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="20dp"
android:autofillHints=""
android:ems="10"
android:hint="Enter Next Value"
android:inputType="number"
android:maxLength="5"
android:singleLine="true" />

开头是一个id号,而所有含有layout的都是定型尺寸和定位尺寸,这里注意layout_below指明它要放在上面那个文本框的下方。这里还限制了输入类型是数字,最多输入5个且只接受单行输入。

添加按钮

首先添加四则运算,这四个按钮之间的位置关系通过layout_belowlayout_toEndOf来指定,可以看到加法减法位于同一行,乘法除法位于同一行,减法除法在加法乘法右侧。
按钮默认全字大写,所以这里关闭了textAllCaps

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<Button
android:id="@+id/add"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@+id/enter_next_value"
android:layout_marginLeft="20dp"
android:background="#0288D1"
android:text="Add"
android:textAllCaps="false"
android:textColor="#ffff" />

<Button
android:id="@+id/subtract"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@+id/enter_next_value"
android:layout_marginStart="20dp"
android:layout_toEndOf="@+id/add"
android:background="#0288D1"
android:text="Subtract"
android:textAllCaps="false"
android:textColor="#ffff" />

<Button
android:id="@+id/multiply"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@+id/add"
android:layout_marginLeft="20dp"
android:layout_marginTop="10dp"
android:background="#0288D1"
android:text="Multiply"
android:textAllCaps="false"
android:textColor="#ffff" />

<Button
android:id="@+id/divide"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_below="@+id/subtract"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:layout_toEndOf="@+id/multiply"
android:background="#0288D1"
android:text="Divide"
android:textAllCaps="false"
android:textColor="#ffff" />

这两个按钮是用于清零和绑定服务的,让他们居中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<Button
android:id="@+id/clear_data"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/division"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:text="Clear Data" />

<Button
android:id="@+id/bind_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/clear_data"
android:layout_centerHorizontal="true"
android:layout_marginTop="10dp"
android:text="Bind Service" />

整体效果应该如图:
ui

注意,各个按钮之间的相对关系是通过id指定的,如果更改了id多半会有error,修改id修复error即可
另外会有一些warning,比如叫你把字符串做编码之类的,建议不懂的话忽略就好

这就是我们的ui界面了!AndroidUI的设计还是很简洁的,但是背后是项目自动为我们生成的许多文件~

5.实现业务逻辑

接下来我们将开始修改客户端的java代码,实现和前端组件的交互、业务流程、服务绑定以及利用远程服务实现功能!

修改主类,继承事件监听

下面打开客户端的MainActivity.java,这里我们可以直接修改主类,让主类完成对UI事件的监听,于是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.aidlclient;

import ...

- public class MainActivity extends AppCompatActivity {
+ public class MainActivity extends Activity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}

}
//注意别漏了左大括号

这样它马上就会报错,先把鼠标放到View.OnClickListener里,可以看到这样的提示:
implement

我们赶紧按ALT+ENTER自动导入这个包,之后把鼠标挪到extend后面的Activity上,再导入一次。他可能会要你选是导入还是创建,就选Import Class Activity就行。
不过还是有错,这是因为我们继承了一个抽象类,必须实现其方法,所以我们在主类里再写一个方法注意方法名称、形参、返回值必须一模一样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.example.aidlclient;

import ...
public class MainActivity extends Activity implements View.OnClickListener{
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
+ @Override
+ public void onClick (View view) {
+
+ }

}

现在应该一个error都没有了。

初始化UI

首先在类里声明私有成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.aidlclient;

import ...
public class MainActivity extends Activity implements View.OnClickListener{
+ private TextView textViewDisplayResult;
+ private EditText editTextFirstValue, editTextNextValue;
+ private Button buttonAdd,buttonSubtract, buttonMultiply, buttonDivide, buttonClearData,buttonBind;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
...
}

成员的名字无所谓,只要和之后的业务逻辑里面的代码对上就行。但接下来要实例化UI对象,这就必须和之前UI里设计时候的id一一对应了!
onCreate是活动被创建时会执行的方法,下面在onCreate方法中编写函数体:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

//Initialize UI
textViewDisplayResult = findViewById(R.id.display_result);
editTextFirstValue = findViewById(R.id.enter_first_value);
editTextNextValue = findViewById(R.id.enter_next_value);
buttonAdd = findViewById(R.id.add);
buttonSubtract = findViewById(R.id.subtract);
buttonDivide = findViewById(R.id.divide);
buttonMultiply = findViewById(R.id.multiply);
buttonClearData = findViewById(R.id.clear_data);
buttonBind = findViewById(R.id.bind_service);

//click listener
buttonAdd.setOnClickListener(this);
buttonSubtract.setOnClickListener(this);
buttonDivide.setOnClickListener(this);
buttonMultiply.setOnClickListener(this);
buttonClearData.setOnClickListener(this);
buttonBind.setOnClickListener(this);
}

注意findViewById中必须传对应的id名,否则会报错。这个函数完成了UI的初始化并且把按钮的事件监听器绑定到本类

首先编写事件监听器的逻辑,也就是onClick方法,不同的按钮被点击之后,我们让他们调用不同的方法进行响应。
这里有两个方法尚未实现:bindToDemoServiceverifyAndCalculate,前者是Bind Service按钮用来绑定服务的方法,后者是四个小按钮进行计算的方法。
在这里我们完全实现的只有Clear Data,注意到它通过setText方法将三个文本框的内容都设置为null,这样就会展示hint内容,也就是我们一开始设置的提示信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public void onClick (View view) {
switch(view.getId())
{
case R.id.add:
verifyAndCalculate(1);
break;
case R.id.subtract:
verifyAndCalculate(2);
break;
case R.id.multiply:
verifyAndCalculate(3);
break;
case R.id.divide:
verifyAndCalculate(4);
break;
case R.id.clear_data:
editTextNextValue.setText(null);
editTextFirstValue.setText(null);
textViewDisplayResult.setText(null);
break;
case R.id.bind_service:
bindToDemoService();
break;

}
}

接下来我们来实现verifyAndCalculate,它首先验证是否两个输入文本框内都有内容,如果都有的话就获取内容并进行计算、显示结果。如果没有的话就在屏幕上显示一个小消息框(就是平时看到的那种网络连接不佳的小消息框>_<)
注意这里的calculateData方法是假的!完成服务绑定后我们会把他替换成通过stub调用的远程方法,到时候我们会对这部分的代码再加以改动,如果想获得最终版本的verifyAndCalculate可以直接到最后部分查看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void verifyAndCalculate(int op) {
if(isAnyValueMissing()){
Toast.makeText(this,"Please enter the values", Toast.LENGTH_SHORT).show();
//Toast就是用来显示小弹窗的东西
} else {
int result,firstValue,nextValue;
firstValue = Integer.parseInt(editTextFirstValue.getText().toString());
nextValue = Integer.parseInt(editTextNextValue.getText().toString());
//获取信息,转换为字符串,再用整数包裹把字符串解析为整数

result = calculateData(firstValue,nextValue,op);
textViewDisplayResult.setText(""+result);
//利用空字符串完成int到String的强制转换
}
}
private boolean isAnyValueMissing() {
return editTextFirstValue.getText().toString().isEmpty() ||
editTextNextValue.getText().toString().isEmpty();
//我个人感觉面向对象语言已经充分自注释了哈哈哈哈
}

到此为止,我们的业务逻辑已经编写完成,按下按钮会调用函数并传递运算符类型。

6.在客户端注册queries

首先,打开服务端项目,在app/menifests/AndroidMenifest.xml里找到服务程序的包名和服务名:
packageandservice
这两个名字都记下来,现在这里先用包名。打开客户端项目,在app/menifests/AndroidMenifest.xml中,manifest标签里,与application标签同级。这里的
"com.example.aidlserver"替换成自己的服务端包名

1
2
3
4
5
6
7
8
9
10
11
 <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mydemo">

<application
...
</application>
+ <queries>
+ <package android:name="com.example.aidlserver" />
+ </queries>
</manifest>

从Android 11开始,应用程序要想访问别的应用,就必须指明该应用程序的包,很多不够新的教程里都没有这个阶段,因此无法正常运行。

7.绑定服务

这里我们来实现bindToDemoService方法,在MainActivity.java里,将下列内容填写进函数体,大家可以参照注释来理解代码。需要做的替换内容,也都在注释中说明。如果代码有变红的,就是因为有包没导入,用把鼠标放到红色的字上,ALT+ENTER导入

未完待续 以下全是草稿

注意:ComponentName的第一个参数是刚才记下来的服务端包名,第二个参数是"<包名>.<服务名>"
这里要用到bindService方法,因此还需要准备一个ServiceConnection对象,
我们需要复写其中的onServiceConnected方法,也就是服务接通之后的回调方法,这里用先打印一个提示信息,然后再将IBinder转换为Aidl存根。aidlObject = IAidlDemo.Stub.asInterface(iBinder);,其中aidlObject可以声明为一个私有变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private IAidlDemo aidlObject;
private void bindToDemoService() {
Toast.makeText(MainActivity.this, "Service connecting", Toast.LENGTH_SHORT).show();
Intent serviceIntentExplicit = new Intent();
serviceIntentExplicit.setComponent(new ComponentName("com.example.aidlserver", "com.example.aidlserver.Calculate"));


boolean qaq = bindService(
serviceIntentExplicit,
serviceConnectionObject,Context.BIND_AUTO_CREATE);
if (!qaq) {
Toast.makeText(MainActivity.this, "Please start server", Toast.LENGTH_SHORT).show();
}
}

ServiceConnection serviceConnectionObject = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
Toast.makeText(MainActivity.this, "Service connected", Toast.LENGTH_SHORT).show();
aidlObject = IAidlDemo.Stub.asInterface(iBinder);
}

@Override
public void onServiceDisconnected(ComponentName componentName) {

}
};

remotee

1
2
3
4
5
6
7
result = 0;
try {
result = aidlObject.calculate(firstValue,nextValue,op
} catch (RemoteException e) {
e.printStackTrace();
}
textViewDisplayResult.setText(""+result);