科研的第二个任务是AIDL和binder机制相关内容,b站上找到了binder源码剖析的很好的讲解,但是AIDL的很多博客都已经过时了。 由于本人没有任何安卓开发经验,所以很需要一步一步示范的教程。在ytb上找到了一个教程,基于此解决了Android11以后bindService无法绑定成功、不会回调onServiceConnected的问题,经测试可以在Android12 (Samsung G9910)上跑通
AIDL概述 AIDL是安卓为进程间通讯留下的接口,和其他RPC框架一样,AIDL也包括代理层,协议层和通讯层。代理层是安卓的跨进程通讯机制Binder,协议层是安卓接口定义语言AIDL,代理层除了AIDL自动生成的存根代码之外,还需要手动实现服务的绑定以及服务并将存根转换为服务接口。这就构成了整个AIDL的框架。 具体实现AIDL的过程分为以下几步:
创建服务端项目,编写AIDL
注册服务,实现服务方法并回传服务存根
创建客户端项目,拷贝AIDL
设计客户端UI
在客户端实现业务逻辑
在客户端注册queries
注意,缺少这一步将无法在Android11以上的机器中绑定服务
在客户端实现服务绑定并用AIDL存根作为通讯接口
利用存根中的方法实现需求功能
在本例中,我们将实现一个进行四则运算计算器,以及一个计算器的服务器。计算器通过绑定到服务器上的服务来完成计算功能
阅读须知 本文可以仅需要RPC基础和c语言基础,对于java中类的继承、抽象类、方法等概念只需初步了解,无需Android开发经验
示例开发
先决条件: Android Studio Android Studio 虚拟机或开启了USB调试功能的Android实体机
安装android虚拟机的方法
没有特殊需求就选默认硬件,直接点next
操作系统映像选默认的(和硬件匹配的)点Download,许可协议选accept然后再点next就开始下载了。我这里是已经装好了
开启USB调试的方法 这个各个设备不太一样,一般要先在设备信息界面点击某个信息十次解锁,比如我是点击设置-设备信息-软件信息-编译编号解锁开发者模式
解锁了之后在设置-开发者选项里找到USB调试 打开就可以了。
1.创建服务端项目,编写AIDL代码 创建新项目 从左上角File-New-New Project新建项目,注意选择”Empty Activity”
接下来可以点next,但在最后的界面记得把语言改成java,项目名字我这里叫做AIDLServer
创建新的包 在项目列表里选择java目录,右键,点击new,点击Package,在目录结构里选main
给它起个名字,注意过会需要创建这个包的一个副本,所以名字别起太怪,不方便检查,这里叫做AidlPackage
创建新的AIDL文件 在刚才的创建的包 上右键创建AIDL文件,名字也别起太怪,这里叫做IAidlDemo
应该能在新建的Aidl文件中看到如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 package AidlPackage;interface IAidlDemo { 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 package AidlPackage;interface IAidlDemo { int calculate (int v1, int v2, int op) ; }
然后需要点击Build-Rebuild Project或者点击右上角配置栏左侧的绿色小锤子进行Make Project。注意,每次修改AIDL文件都要进行rebuild 到此为止,我们的服务应用程序框架就创建完成。
2.注册服务,实现方法 在APP里右键创建新的Service,注意不要 选Service(Intent Service)
,在接下来的窗口里enabled
和exported
都要选中
新建存根 打开新建的服务的java代码app/java/AidlPackage/<ServiceName>.java
,为<ServiceName>
类添加一个存根变量
1 2 private final IAidlDemo.Stub...
注意,这里按下.
之后,应该提示后面的Stub类型,如果没有提示就说明刚才没有进行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) { throw new UnsupportedOperationException("Not yet implemented" ); }
这是因为我们的服务继承了Service抽象类,因此需要实现onBind方法,也就是指定当服务绑定时要进行的操作。我们只需要返回刚才创建的存根即可。
1 2 3 4 @Override public IBinder onBind (Intent intent) { return stub; }
到此为止我们的服务端项目就编写完成,可以跑一下试试看:
虽然没有UI界面,但是服务已经就绪了~
3.创建客户端项目,拷贝AIDL 新建客户端项目可以单开一个project也可以就在这个project里操作,这里选择单开一个project。过程就不再赘述,注意仍然用Empty Activity
作为界面。
这里仍然在app\java
上右键,选New-Package
新建包,注意包名要与刚刚创建的包名相同 在包上右键,新建Aidl文件,注意文件名要和刚刚创建的文件相同 复制 刚刚的Aidl文件内容,粘贴到新的Aidl文件里 包名、文件名、文件内容完全相同。
4.设计客户端UI 打开activity_main.xml
默认在创建项目的时候就会有这个文件生成,如果不小心关掉了就从左侧项目目录app/res/layout/
目录下打开。 请检查一下这个目录,确保其中只有activity_main.xml
。在右上角选择以Code
方式查看该文件的内容:
文件内容应该如下
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的效果,你可以在右上角选择Split
或View
模式)
添加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就是一个文本框,我们设置了默认显示的字符串(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_below
和layout_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" />
整体效果应该如图:
注意,各个按钮之间的相对关系是通过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
里,可以看到这样的提示:
我们赶紧按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); 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); buttonAdd.setOnClickListener(this ); buttonSubtract.setOnClickListener(this ); buttonDivide.setOnClickListener(this ); buttonMultiply.setOnClickListener(this ); buttonClearData.setOnClickListener(this ); buttonBind.setOnClickListener(this ); }
注意findViewById中必须传对应的id名,否则会报错。这个函数完成了UI的初始化并且把按钮的事件监听器绑定到本类
首先编写事件监听器的逻辑,也就是onClick方法,不同的按钮被点击之后,我们让他们调用不同的方法进行响应。 这里有两个方法尚未实现:bindToDemoService
和verifyAndCalculate
,前者是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(); } 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); } } private boolean isAnyValueMissing () { return editTextFirstValue.getText().toString().isEmpty() || editTextNextValue.getText().toString().isEmpty(); }
到此为止,我们的业务逻辑已经编写完成,按下按钮会调用函数并传递运算符类型。
6.在客户端注册queries 首先,打开服务端 项目,在app/menifests/AndroidMenifest.xml
里找到服务程序的包名和服务名: 这两个名字都记下来,现在这里先用包名。打开客户端 项目,在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) { } };
1 2 3 4 5 6 7 result = 0 ; try { result = aidlObject.calculate(firstValue,nextValue,op } catch (RemoteException e) { e.printStackTrace(); } textViewDisplayResult.setText("" +result);