本期视频地址 : 车载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,咱们能够很快速的将一个目标序列化,而不用自行编写代码。

【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上)

紧接着咱们只需求在需求序列化的类中,右键->generate->parcelable 选中需求序列化的成员变量,即可完结目标的序列化。

【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上)

然后在aidl目录下同样的包名里创立Sample.aidl文件,这样Android SDK就能识别出Sample目标。

【视频文稿】车载Android应用开发与分析 - AIDL实践与封装(上)

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 简介

不要在主线程中直接调用「服务端」的办法,这个很好理解,咱们主要来看onewayoneway 是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/…

感谢您的观看,咱们下期视频再会,拜拜。