Android ABI 详解及适配

不同的 Android 设备可能使用不同的 CPU 架构(如 ARM、X86 等),而不同的 CPU 架构支持的指令集又不同。CPU 与指令集的每种组合都有专属的应用二进制接口 (Application Binary Interface, ABI)。

按照某个 ABI 编译好的 .so 文件能在所有兼容该 ABI 的操作系统中无需改动就能运行。所以针对不同的 CPU 架构就要按照不同的 ABI 编译出相应的 .so 文件。

Android(NDK) 支持的 ABI

Android 目前支持的 ABI 有:armeabi-v7a、arm64-v8a、x86、x86_64。

NDK 以前支持 ARMv5 (armeabi) 以及 32 位和 64 位 MIPS,但 NDK r17 及之后已不再支持。

  • armeabi-v7a:此 ABI 适用于第 7 代及以上的基于 32 位 ARM 的 CPU。
  • arm64-v8a:此 ABI 适用于基于 ARMv8-A 的 CPU,支持 64 位 AArch64 架构。
  • x86:此 ABI 适用于支持通常称为“x86”、“i386”或“IA-32”的指令集的 CPU。
  • x86_64:此 ABI 适用于支持通常称为“x86-64”的指令集的 CPU。
  • armeabi:此 ABI 适用于基于 ARMv5 的 32 位CPU。

当我们想要在项目中使用 native(C/C++)类库或者依赖一些第三方库的时候,往往需要导入包含 native 代码的 .so 文件。默认情况下,Gradle(无论是通过 Android Studio 使用,还是从命令行使用)会针对所有非弃用 ABI 进行构建。这样做的好处是支持所有的架构,并且兼容性好;但同时也会增加 Apk 的体积,影像下载和安装速度。

因此,Google 为开发者提供了 abiFilters 来限制应用支持的 ABI 集合。使用方式如下:

android {
    defaultConfig {
        ndk {
            abiFilters 'arm64-v8a', 'x86_64' // 代表只针对 arm 和 x86 的64位 ABI 进行构建
        }
    }
}

构建系统打包时的默认行为是将配置的所有 ABI 的二进制文件添加到一个 APK 内。这样一个安装包可以支持所有兼容的 ABI,但是当安装到设备上后,只使用与当前设备兼容的 .so 文件,其他 ABI 的 .so 文件是用不到的,这就造成了不必要的资源浪费。

为了解决上述问题,Google 推出了 app bundle 和  APK 拆分 两个方案来减小 APK 的大小,同时仍保持最大限度的设备兼容性。

  • app bundle:Android App Bundle 是一种发布格式,其中包含您应用的所有经过编译的代码和资源,它会将 APK 生成及签名交由 Google Play 来完成。Google Play 会使用您的 App Bundle 针对每种设备配置生成并提供经过优化的 APK,因此只会下载特定设备所需的代码和资源来运行您的应用。您不必再构建、签署和管理多个 APK 来优化对不同设备的支持,而用户也可以获得更小且更优化的下载文件包。
  • Apk 拆分:创建多个 APK,让每个 APK 包含适用于特定屏幕密度或 ABI 的文件。

如果仅仅是面向大陆应用市场的应用可以使用  APK 拆分 方案;但是如果是要提交到 Google Play 的应用,可以使用 app bundle 方案。从 2021 年 8 月起,新应用需要使用 Android App Bundle 才能在 Google Play 中发布。

ABI 如何工作

Play 商店和软件包管理器都希望能在 APK 中按照以下格式的文件路径上找到 NDK 生成的库:

/lib/<abi>/lib<name>.so

其中,<abi> 是支持的 ABI 下列出的 ABI 名称之一,<name> 是 Android.mk 文件中的 LOCAL_MODULE 变量定义库时使用的库名称。

Android 系统在运行时知道它支持哪些 ABI,一般来说,一个 Android设备可以支持多种 ABI:设备主要 ABI 和 辅助 ABI(可选)。此机制确保系统在安装时从软件包提取最佳机器代码。

例如,基于 ARMv5TE 的设备只会定义主 ABI:armeabi。相反,基于 ARMv7 的设备将主 ABI 定义为 armeabi-v7a,并将辅助 ABI 定义为 armeabi,因为它可以运行为每个 ABI 生成的应用原生二进制文件。

64 位设备也支持其 32 位变体。以 arm64-v8a 设备为例,该设备也可以运行 armeabi 和 armeabi-v7a 代码。但请注意,如果应用以 arm64-v8a 为目标,则应用在 64 位设备上的性能要比 armeabi-v7a 的设备好得多。

许多基于 x86 的设备也可运行 armeabi-v7a 和 armeabi NDK 二进制文件。对于这些设备,主 ABI 是 x86,辅助 ABI 是 armeabi-v7a

例如:一个 arm64-v8a 架构的手机运行 app 时,在 jnilibs 目录读取库文件时,先看有没有 arm64-v8a 文件夹,如果没有该文件夹,去找 armeabi-v7a 文件夹,如果还没有再去找 armeabi 文件夹,如果连这个文件夹也没有,就抛出异常。

注意事项

1、如果一个 arm64-v8a 架构的手机运行 app 时,有 arm64-v8a 文件夹,那么就去找特定名称的 .so 文件,如果没有找到想要的 .so 文件,不会再往下(armeabi-v7a 文件夹)找了,而是直接抛出异常。

所以如果我们的应用选择了支持多个 ABI,要十分注意:对于每个ABI 下的 so,但要么全部支持,要么都不支持。不应该混合着使用,而应该为每个 ABI 目录提供对应的 .so 文件。

2、如果设备 CPU 架构是 ARMv7,ABI 文件是 armeabi-v7a,但是放进了 armeabi 目录中,并且项目中只有 armeabi 目录。系统会加载armeabi 目录下的 so 库,相当于加载了一个 armeabi 规则的 so 库,因为向前兼容的特性,不会报错也能运行,但存在性能损失的问题。

3、项目中使用到第三方 SDK 时,不同的 SDK 支持的 ABI 可能不同,如果 SDK 使用到了项目不支持的 ABI,或 SDK A 使用了 SDK B 不支持的 ABI,编译打包时会自动包含所有 ABI 目录和文件。这时某些 ABI 目录下的 so 文件可能不全,当运行到相应的 ABI 设备上时就会出现注意事项 1 中提到的问题,发生 Crash。针对这种情况,可以使用 abiFilter 配置,保证对于每个 ABI 下的 so,但要么全部支持,要么都不支持。

如何适配

根据 ABI 向下兼容性的特点,我们可以得出一下这些结论:

因为 armeabi-v7a 和 arm64-v8a 会向下兼容:

  • 只适配 armeabi 的应用可以在 armeabi, armewabi-v7a, arm64-v8, x86, x86_64 上运行。
  • 只适配 armeabi-v7a 的应用可以在 armeabi-v7a, arm64-v8a, x86, x86_64 上运行。
  • 只适配 arm64-v8a 的应用只可以运行在 arm64-v8a 上。

适配方案对比

一、只适配 armeabi

  • 优点:基本上适配了全部 CPU 架构(除了淘汰的 mips 和 mips_64)
  • 缺点:性能低,相当于在绝大多数手机上都是需要辅助 ABI 或动态转码来兼容

二、只适配 armeabi-v7a

能运行在 armeabi-v7a, arm64-v8, x86, x86_64 机器上,在性能和兼容二者中比较平衡。

三、只适配 arm64-v8

只能运行在 arm64-v8上,要放弃部分老旧设备用户,优点就是:性能最佳。

查看手机 CPU 的 ABI

查看 CPU 的 ABI 有两种形式:adb 命令、代码获取。

通过 adb 命令查看

1. adb shell
2. getprop ro.product.cpu.abilist

输出信息如下:

arm64-v8a,armeabi-v7a,armeabi

代码获取

Log.e(TAG, "CPU_ABI: ${Build.CPU_ABI}") // 输出 CPU_ABI: arm64-v8a

Log.e(TAG, "CPU_ABI2: ${Build.CPU_ABI2}") // 输出 CPU_ABI2: 

val abiList = mutableListOf<String>()
abiList.addAll(Build.SUPPORTED_ABIS)
Log.e(TAG, "SUPPORTED_ABIS $abiList") // 输出 SUPPORTED_ABIS [arm64-v8a, armeabi-v7a, armeabi]

val abi32List = mutableListOf<String>()
abi32List.addAll(Build.SUPPORTED_32_BIT_ABIS)
Log.e(TAG, "SUPPORTED_32_BIT_ABIS: $abi32List") // 输出 SUPPORTED_32_BIT_ABIS: [armeabi-v7a, armeabi]

val abi64List = mutableListOf<String>()
abi64List.addAll(Build.SUPPORTED_64_BIT_ABIS)
Log.e(TAG, "SUPPORTED_64_BIT_ABIS: $abi64List") // 输出 SUPPORTED_64_BIT_ABIS: [arm64-v8a]

指定特定 ABI 强制安装

使用该命令可以方便测试应用的 ABI 适配情况。

adb install --abi <abi-identifier> <apk_path>

例如:`adb install –abi arm64-v8a debug.apk` 则应用将会强制以 64 位运行。

参考

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注