本期视频地址 : 车载Android运用开发与剖析 – AIDL实践与封装(上)_哔哩哔哩_bilibili
开发手机APP时咱们一般都是写一个独立的运用,很少会涉及到除了体系服务以外的多个进程间交互的状况,但开发车载运用则不同,随着车载体系需求复杂程度的逐渐进步,现代的车载运用或多或少都会涉及多进程间的交互。
实践的项目中,也会发现一些即便有着多年运用开发经验的同事,关于安卓跨进程通讯的运用并不熟练,常常整出一些啼笑皆非的事端,所以本期视频咱们将介绍车载Android运用开发最常用的跨进程通讯计划-AIDL,以及它是怎么运用和封装的。
「1. AIDL 简介」
AIDL 简介
AIDL 全称Android 接口界说言语(Android Interface Definition Language),是一种用于界说客户端和服务端之间的通讯接口的言语,它能够让不同进程之间经过IPC(进程间通讯)进行数据交互。
在 Android 体系中一个进程通常无法直接拜访另一个进程的内存空间,这被称为Application Sandbox。因此,为了完结进程间通讯,Android体系供给了用于完结跨进程通讯的协议,可是完结通讯协议往往比较复杂,需求将通讯数据进行编组和解组,运用AIDL能够让上述操作变得简略。
AIDL的架构能够看作是一种CS(Client-Server)架构,即客户端-服务端架构。简略介绍如下:
1)「客户端」是指需求调用「服务端」供给的数据或功用的运用,它经过绑定「服务端」的Service来获取一个IBinder目标,然后经过该目标调用「服务端」暴露出来的接口办法 。
2)「服务端」是指供给数据或功用给「客户端」的运用,它经过创立一个Service并在onBind()办法中回来一个IBinder目标来完结通讯接口,该目标需求重写.aidl文件中界说的接口办法 。
3)「客户端」和「服务端」需求共享一个.aidl文件,用来声明通讯接口和办法,该文件会被Android SDK工具转换成一个Java接口,该接口包括一个Stub类和一个Proxy类 。
运用场景
Android 体系中的 IPC不只是有AIDL,Android体系还供给了以下几种常用的 IPC 的办法:
- Messenger
一种依据AIDL的IPC通讯的办法,它对AIDL进行了封装,简化了运用进程,只需求创立一个Handler目标来处理消息。Messenger只支撑单线程串行恳求,只能传输Message目标,不能传输自界说的Parcelable目标。
- ContentProvider
一种用于供给数据拜访接口的IPC通讯的办法,它能够让不同进程之间经过URI和Cursor进行数据交互。ContentProvider能够处理多线程并发恳求,能够传输任意类型的数据,但运用进程比较繁琐,需求完结多个办法。
- Socket
一种依据TCP/IP协议的IPC通讯的办法,它能够让不同进程之间经过网络套接字进行数据交互。Socket能够处理多线程并发恳求,能够传输任意类型的数据,但运用进程比较底层,需求处理网络反常和安全问题。
咱们能够依据不同的场景和需求,挑选适宜的IPC的办法。一般来说:
-
假如需求完结跨运用的数据共享,能够运用ContentProvider。
-
假如需求完结跨运用的功用调用,能够运用AIDL。
-
假如需求完结跨运用的消息传递,能够运用Messenger。
-
假如需求完结跨网络的数据交换,能够运用Socket。
接下来,咱们经过代码来实践一个 AIDL 通讯的示例。
「2. AIDL实践 」
在编写示例之前,先做出需求界说。
假定咱们有一个「服务端」,供给一个核算器的功用,能够进行加减乘除等多种运算。咱们想让其他「客户端」运用也能调用这个「服务端」,进行核算,咱们能够依照以下进程来完结:
第 1 步,创立SDK工程,界说 AIDL 接口
在实践工作中,强烈主张将 AIDL 的接口封装到一个独立的工程(Module)中,运用时将该工程编译成一个jar包,再交给其它模块运用。这样做能够避免需求一起在APP工程以及Service工程中界说AIDL接口的状况,也方便咱们后期的维护。
在SDK工程中,界说一个AIDL接口,声明咱们想要供给的办法和参数。例如,咱们能够创立一个ICalculator.aidl文件,内容如下:
interface ICalculator {
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);
}
第 2 步,创立 Service 工程,完结AIDL接口
在「服务端」运用中,创立一个Service类,完结AIDL接口,并在onBind办法中回来一个IBinder目标。例如,咱们能够创立一个CalculatorService类,内容如下:
public class CalculatorService extends Service {
private final Calculator.Stub mBinder = new Calculator.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
return a + b;
}
@Override
public int subtract(int a, int b) throws RemoteException {
return a - b;
}
@Override
public int multiply(int a, int b) throws RemoteException {
return a * b;
}
@Override
public int divide(int a, int b) throws RemoteException {
if (b == 0) {
throw new IllegalArgumentException("Divisor cannot be zero");
}
return a / b;
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
在「服务端」运用中,注册Service,并设置android:enabled和android:exported特点为true,以便其他运用能够拜访它。
假如需求还能够增加一个intent-filter,指定一个action,让其他运用能够经过intent发动服务,一起服务端也能够经过读取intent中的action来过滤绑定恳求。
例如,在AndroidManifest.xml文件中,咱们能够增加以下代码:
<service
android:name=".CalculatorService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.example.calculator.CALCULATOR_SERVICE" />
</intent-filter>
</service>
在Android 8.0之后的体系中,Service发动后需求增加Notification,将Service设定为前台Service,否则会抛出反常。
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "onCreate: ");
startServiceForeground();
}
private static final String CHANNEL_ID_STRING = "com.wj.service";
private static final int CHANNEL_ID = 0x11;
private void startServiceForeground() {
NotificationManager notificationManager = (NotificationManager)
getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel;
channel = new NotificationChannel(CHANNEL_ID_STRING, getString(R.string.app_name),
NotificationManager.IMPORTANCE_LOW);
notificationManager.createNotificationChannel(channel);
Notification notification = new Notification.Builder(getApplicationContext(),
CHANNEL_ID_STRING).build();
startForeground(CHANNEL_ID, notification);
}
第 3 步,创立客户端工程,调用AIDL接口
在「客户端」运用中,创立一个ServiceConnection
目标,完结onServiceConnected和onServiceDisconnected办法,在onServiceConnected办法中获取IBinder目标的署理,并转换为AIDL接口类型。
private ICalculator mCalculator;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mCalculator = ICalculator.Stub.asInterface(service);
// 核算 3*6
calculate('*',3,6);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCalculator = null;
}
};
在运用核算器功用的运用中,绑定供给核算器功用的运用的Service,并传递一个Intent目标,指定供给核算器功用的运用的包名和Service类名。假如供给核算器功用的运用设置了intent-filter,还需求指定相应的action。
private void bindToServer() {
Intent intent = new Intent();
intent.setAction("com.wj.CALCULATOR_SERVICE");
intent.setComponent(new ComponentName("com.wj.service", "com.wj.service.CalculatorService"));
boolean connected = bindService(intent, mConnection, BIND_AUTO_CREATE);
Log.e(TAG, "onCreate: " + connected);
}
获取到IBinder目标的署理后就能够经过该目标调用「服务端」供给的办法了。
private void calculate(final char operator, final int num1, final int num2) {
try {
int result = 0;
switch (operator) {
case '+':
result = mCalculator.add(num1, num2);
break;
case '-':
result = mCalculator.subtract(num1, num2);
break;
case '*':
result = mCalculator.multiply(num1, num2);
break;
case '/':
result = mCalculator.divide(num1, num2);
break;
}
Log.i(TAG, "calculate result : " + result);
} catch (RemoteException exception) {
Log.i(TAG, "calculate: " + exception);
}
}
留意,从Android 11 开端,体系对运用的可见性进行了维护,假如 build.gradle 中的Target API > = 30,那么还需求在 AndroidManifest.xml 装备queries标签指定「服务端」运用的包名,才能够绑定长途服务。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<queries>
<package android:name="com.wj.service"/>
</queries>
</manifest>
「3. AIDL 实践进阶」
在上面的示例中,咱们介绍了简略的AIDL是怎么创立的,可是在开发中,上述的示例远不足以支撑实践的运用场景,接下来整理10个开发进程大概率会遇到的到问题,以及它的解决计划。
问题 1:AIDL 数据类型
上述示例中,咱们运用AIDL传递的是最简略的int型数据,AIDL不仅支撑int型数据,AIDL支撑的数据类型有:
-
Java编程言语中的一切原始类型(如int、long、char、boolean等)
-
String和CharSequence
-
List,只支撑ArrayList,里边每个元素都有必要能够被AIDL支撑
-
Map,只支撑HashMap,里边的每个元素都有必要被AIDL支撑,包括key和value
-
Parcelable,一切完结了Parcelable接口的目标
-
Serializable,一切完结了Serializable接口的目标(不能独立传输)
-
AIDL,一切的AIDL接口自身也能够在AIDL文件中运用
Parcelable
在安卓中非根本数据类型的目标,除了String和CharSequence都是不能够直接经过AIDL进行传输的,需求先进行序列化操作。序列化就是将目标转换为可存储或可传输的状态,序列化后的目标能够在网络上进行传输,也能够存储到本地。
Parcelable 是安卓完结的可序列化接口。它假定一种特定的结构和处理办法,这样一个完结了 Parcelable接口的目标能够相对快速地进行序列化和反序列化。
在接下来的比如中,咱们界说一个Sample目标,并完结Parcelable接口将其序列化,在Android Studio上经过插件Android Parcelable Code Generator,咱们能够很快速的将一个目标序列化,而不用自行编写代码。
紧接着咱们只需求在需求序列化的类中,右键->generate->parcelable 选中需求序列化的成员变量,即可完结目标的序列化。
然后在aidl目录下同样的包名里创立Sample.aidl文件,这样Android SDK就能识别出Sample目标。
Sample.aidl文件内容如下:
// Sample.aidl
package com.wj.sdk.bean;
parcelable Sample;
在将需求传输的目标序列化后,咱们在ICalculator.aidl
中界说一个新的办法,并将Sample经过AIDL接口传递给「服务端」。
// ICalculator.aidl
package com.wj.sdk;
import com.wj.sdk.bean.Sample;
interface ICalculator {
void optionParcel(in Sample sample);
}
Serializable
Serializable 是 Java 供给的一个序列化接口,它是一个空接口,为目标供给规范的序列化和反序列化操作。运用 Serializable 来完结序列化适当简略,只需目标完结了Serializable 接口即可完结默许的序列化进程。Serializable 的序列化和反序列化进程由体系主动完结。
AIDL尽管支撑Serializable序列化的目标,可是并不能直接在AIDL接口中传递Serializable的目标,有必要放在一个Parcelable目标中传递。
Parcelable & Serializable 对比
Serializable 尽管运用简略,可是在AIDL中并不引荐运用,因为Serializable 运用了反射机制,功率较低,并且会产生大量的暂时变量,增加内存开支。而Parcelable直接在内存中进行读写,功率较高,并且没有额外的开支。
一般来说,假如需求将数据经过网络传输或许耐久化到本地,主张运用Serializable,假如只是在运用内部进行数据传递,则主张运用Parcelable。
问题 2:AIDL参数的数据流向
在上面的ICalculator.aidl
中,addOptParcelable()
办法中出现了in、out、inout这些关键字,是因为在传递序列化参数时,有必要界说这些参数的数据流方向,in、out、inout关键字的影响主要体现在参数目标在传输进程中是否被仿制和修正。具体来说:
-
in:表明数据从客户端流向服务端,客户端会将参数目标仿制一份并发送给服务端,服务端收到后能够对该目标进行修正,但不会影响客户端的原始目标 。
-
out:表明数据从服务端流向客户端,客户端会将参数目标的空引用发送给服务端,服务端收到后能够创立一个新的目标并赋值给该引用,然后回来给客户端,客户端会将原始目标替换成服务端回来的目标 。
-
inout:表明数据双向活动,客户端会将参数目标仿制一份并发送给服务端,服务端收到后能够对该目标进行修正,并将修正后的目标回来给客户端,客户端会将原始目标替换成服务端回来的目标 。
运用这些关键字时,需求留意以下几点:
-
假如参数目标是不可变的(如String),则不需求运用out或inout关键字,因为服务端无法修正其内容 。
-
假如参数目标是可变的(如List或Map),则需求依据实践需求挑选适宜的关键字,以避免不必要的数据拷贝和传输 。
-
假如参数目标是自界说的Parcelable类型,则需求在其writeToParcel()和readFromParcel()办法中依据flags参数判断是否需求写入或读取数据,以适应不同的关键字 。
问题 3:运用AIDL传递复数个目标
AIDL支撑传递一些根本类型和 Parcelable 类型的数据。假如需求传递一些复杂的目标或许多个目标以及数量不定的目标时,能够运用 Bundle 类来封装这些数据,然后经过 AIDL 接口传递Bundle
目标。Bundle
类是一个键值对的容器,它能够存储不同类型的数据,并且完结了Parcelable接口,所以能够在进程间传输。
假如AIDL接口包括接收Bundle
作为参数(预计包括 Parcelable 类型)的办法,则在尝试从Bundle
读取之前,请必须经过调用 Bundle.setClassLoader(ClassLoader)
设置Bundle的类加载器。否则,即便在运用中正确界说 Parcelable 类型,也会遇到 ClassNotFoundException
。例如,
// ICalculator.aidl
package com.wj.sdk;
interface ICalculator {
void optionBundle(in Bundle bundle);
}
如下方完结所示,在读取Bundle
的中数据之前,ClassLoader 已在Bundle
中完结显式设置。
@Override
public void optionBundle(final Bundle bundle) throws RemoteException {
Log.i(TAG, "optionBundle: " + bundle.toString());
bundle.setClassLoader(getClassLoader());
Sample2 sample2 = (Sample2) bundle.getSerializable("sample2");
Log.i(TAG, "optionBundle: " + sample2.toString());
Sample sample = bundle.getParcelable("sample");
Log.i(TAG, "optionBundle: " + sample.toString());
}
为什么需求设置类加载器?因为Bundle
目标或许包括其他的Parcelable目标,而这些目标的类界说或许不在默许的类加载器中。设置类加载器能够让Bundle
目标正确地找到和创立Parcelable目标。
例如,假如你想传递一个Android体系的NetworkInfo
目标,你需求在AIDL文件中声明它是一个Parcelable目标:
package android.net;
parcelable NetworkInfo;
然后,在客户端和服务端的代码中,你需求在获取Bundle目标之前,设置类加载器为NetworkInfo
的类加载器:
Bundle bundle = data.readBundle();
bundle.setClassLoader(NetworkInfo.class.getClassLoader());
NetworkInfo networkInfo = bundle.getParcelable("network_info");
这样,Bundle目标就能够正确地反序列化NetworkInfo目标了。
问题 4:运用 AIDL传递大文件
众所周知,AIDL是一种依据Binder完结的跨进程调用计划,Binder 对传输数据巨细有约束,传输超越 1M 的文件就会报 android.os.TransactionTooLargeException 反常。不过咱们依然有大文件传输的解决计划,其中一种解决办法是,运用AIDL传递文件描述符ParcelFileDescriptor
,来完结超大型文件的跨进程传输。
该部分内容较多,能够查看我之前写的文章:Android 运用AIDL传输超大型文件 –
问题 5:AIDL 引起的 ANR
Android AIDL 通讯自身是一个耗时操作,因为它涉及到进程间的数据传输和序列化/反序列化的进程。假如在「客户端」的主线程中调用 AIDL 接口,并且「服务端」的办法履行比较耗时,就会导致「客户端」主线程被堵塞,然后引发ANR。
为了避免 AIDL 引起的 ANR,能够采纳以下这些办法:
- 不要在主线程中调用 AIDL 接口,而是运用子线程或许异步使命来进行 IPC。
- 不要在 onServiceConnected () 或许 onServiceDisconnected () 中直接操作服务端办法,因为这些办法是在主线程中履行的。
- 运用
oneway
键字来润饰 AIDL 接口,使得 IPC 调用变成非堵塞的。
oneway 简介
不要在主线程中直接调用「服务端」的办法,这个很好理解,咱们主要来看oneway
。oneway
是AIDL界说接口时可选的一个关键字,它能够润饰 AIDL 接口中的办法,修正长途调用的行为。
oneway
主要有以下两个特性:
- 将长途调用改为「异步调用」,使得长途调用变成非堵塞式的,客户端不需求等候服务端的处理,只是发送数据并当即回来。
-
oneway
润饰办法,在同一个IBinder目标调用中,会依照调用次序顺次履行。
运用场景
运用oneway
的场景一般是当你不需求等候服务端的回来值或许回调时,能够进步 IPC 的功率。
oneway
能够用来润饰在interface之前,这样会让interface内一切的办法都隐式地带上oneway
,也能够润饰在interface里的各个办法之前。
例如:例如,你或许需求向服务端发送一些控制命令或许通知,而不关心服务端是否处理成功。
// ICalculator.aidl
package com.wj.sdk;
interface ICalculator {
oneway void optionOneway(int i);
}
或直接将oneway
增加在interface前。
// ICalculator.aidl
package com.wj.sdk;
oneway interface ICalculator {
void optionOneway(int i);
}
留意事项
给AIDL接口增加oneway
关键词有以下的事项需求留意:
-
oneway
润饰本地调用没有效果,仍然是同步的,「客户端」需求等候「服务端」的处理。
本地调用是指「客户端」和「服务端」在同一个进程中,不需求进行 IPC 通讯,而是直接调用 AIDL 接口的办法。这种状况下,oneway
就失效了,因为没有进程间的数据传输和序列化/反序列化的进程,也就没有堵塞的问题。
-
oneway
不能用于润饰有回来值的办法,或许抛出反常,因为「客户端」无法接收到这些信息。 - 同一个IBinder目标进行oneway调用,这些调用会依照原始调用的次序顺次履行。不同的IBinder目标或许导致调用次序和履行次序不一致。
同一个IBinder目标的oneway
调用,会依照调用的次序顺次履行,这是因为内核中每个IBinder目标都有一个oneway
业务的行列,只有当上一个业务完结后才会从行列中取出下一个业务。也是因为这个行列的存在,所以不同IBinder目标oneway
调用的履行次序,不一定和调用次序一致。
-
oneway
要慎重用于润饰调用极其频繁的IPC接口
当「服务端」的处理较慢,可是「客户端」的oneway
调用十分频繁时,来不及处理的调用会占满binder驱动的缓存,导致transaction failed,假如你对剖析进程感兴趣,能够参考这篇文章:www.jianshu.com/p/4c8d34618…
「6. 总结」
本期视频咱们介绍了车载Android开发中最常用的跨进程通讯办法-AIDL,不过因为内容太多,所以会分成上下两个部分。本篇,主要聚集在一些常见的运用场景上,下一篇,咱们将介绍AIDL接口权限控制、封装、办法索引等内容。
好,以上就是本视频的全部内容了。本视频的文字内容发布在我的个人微信大众号-『车载 Android』和我的个人博客中,视频中运用的 PPT 文件和源码发布在我的Github[github.com/linxu-link/…
感谢您的观看,咱们下期视频再会,拜拜。