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

简介:

本期视频地址 : 车载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 简介

不要在主线程中直接调用「服务端」的方法,这个很好理解,我们主要来看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,如果你对分析过程感兴趣,可以参考这篇文章:https://www.jianshu.com/p/4c8d346185cb。

「6. 总结」

本期视频我们介绍了车载Android开发中最常用的跨进程通信方式-AIDL,不过由于内容太多,所以会分成上下两个部分。本篇,主要聚焦在一些常见的使用场景上,下一篇,我们将介绍AIDL接口权限控制、封装、方法索引等内容。

好,以上就是本视频的全部内容了。本视频的文字内容发布在我的个人微信公众号-『车载 Android』和我的个人博客中,视频中使用的 PPT 文件和源码发布在我的Github[https://github.com/linxu-link/CarAndroidCourse]上,在本视频的简介里可以找到相应的地址。

感谢您的观看,我们下期视频再见,拜拜。

目录
相关文章
|
18天前
|
存储 Android开发 开发者
深入理解安卓应用开发的核心组件
【10月更文挑战第8天】探索Android应用开发的精髓,本文带你了解安卓核心组件的奥秘,包括Activity、Service、BroadcastReceiver和ContentProvider。我们将通过代码示例,揭示这些组件如何协同工作,构建出功能强大且响应迅速的应用程序。无论你是初学者还是资深开发者,这篇文章都将为你提供新的视角和深度知识。
|
5天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
29 15
Android 系统缓存扫描与清理方法分析
|
3天前
|
传感器 XML IDE
探索安卓应用开发:从基础到进阶
【10月更文挑战第23天】在数字化时代的浪潮中,移动应用已成为人们日常生活的延伸。本文以安卓平台为例,深入浅出地介绍了如何从零开始构建一个安卓应用,涵盖了开发环境搭建、基本组件使用、界面设计原则以及进阶技巧等关键步骤。通过实例演示和代码片段,引导读者逐步掌握安卓应用开发的核心技能,旨在激发更多开发者对安卓平台的探索热情,并为初学者提供一条清晰的学习路径。
|
16天前
|
Java Android开发 Swift
掌握安卓与iOS应用开发:技术比较与选择指南
在移动应用开发领域,谷歌的安卓和苹果的iOS系统无疑是两大巨头。它们不仅塑造了智能手机市场,还影响了开发者的日常决策。本文深入探讨了安卓与iOS平台的技术差异、开发环境及工具、以及市场表现和用户基础。通过对比分析,旨在为开发者提供实用的指导,帮助他们根据项目需求、预算限制和性能要求,做出最合适的平台选择。无论是追求高度定制的用户体验,还是期望快速进入市场,本文都将为您的开发旅程提供有价值的见解。
|
2天前
|
开发工具 Android开发 Swift
探索iOS与安卓应用开发的异同点
【10月更文挑战第24天】本文通过比较iOS和安卓开发环境,旨在揭示两大移动平台在开发过程中的相似性与差异性。我们将探讨开发工具、编程语言、用户界面设计、性能优化及市场分布等方面,以期为开发者提供全面的视角。通过深入浅出的分析,文章将帮助读者更好地理解每个平台的独特之处及其对应用开发的影响。
|
3天前
|
XML IDE Java
安卓应用开发入门:从零开始的旅程
【10月更文挑战第23天】本文将带领读者开启一段安卓应用开发的奇妙之旅。我们将从最基础的概念讲起,逐步深入到开发实践,最后通过一个简易的代码示例,展示如何将理论知识转化为实际的应用。无论你是编程新手,还是希望扩展技能的软件工程师,这篇文章都将为你提供有价值的指导和启发。
12 0
|
14天前
|
安全 Java 开发工具
掌握安卓应用开发:从基础到高级的全面指南
本文旨在为读者提供一个详尽的指南,帮助他们掌握安卓应用开发的基础知识及高级技巧。从环境搭建到项目实践,逐步深入讲解安卓开发的各个环节。无论是对于刚入门的初学者还是希望进一步提升的开发者,本文都将提供实用的建议和示例代码,帮助你快速上手并提升技能。
|
15天前
|
存储 Java 开发工具
掌握安卓应用开发:从基础到高级的全面指南
这篇文章旨在为读者提供一个关于安卓应用开发的全面指南。无论您是初学者还是有一定经验的开发者,本文将带您深入探讨安卓开发的核心概念、工具和技术。我们将从环境搭建和基本组件讲起,逐步引导您了解布局管理、用户交互处理、数据存储与网络通信等高级主题。通过阅读本文,您将能够更好地理解安卓应用开发的整体流程,并具备创建高质量安卓应用的能力。
|
18天前
|
开发工具 Android开发 Swift
安卓与iOS开发环境的差异性分析
【10月更文挑战第8天】 本文旨在探讨Android和iOS两大移动操作系统在开发环境上的不同,包括开发语言、工具、平台特性等方面。通过对这些差异性的分析,帮助开发者更好地理解两大平台,以便在项目开发中做出更合适的技术选择。
|
22天前
|
缓存 搜索推荐 Android开发
安卓开发中的自定义控件实践
【10月更文挑战第4天】在安卓开发的海洋中,自定义控件是那片璀璨的星辰。它不仅让应用界面设计变得丰富多彩,还提升了用户体验。本文将带你探索自定义控件的核心概念、实现过程以及优化技巧,让你的应用在众多竞争者中脱颖而出。