之前的文章『「Android Studio」如何导入 JAR 包』介绍了传统 JAR 包的导入方式,其实 Android 开发中还有一种相对来说用得比较少但却依然十分重要的库类型:SO 文件。

什么是 SO 库

SO 文件(Shared Object)是 Linux 下共享库文件,它的文件格式被称为 ELF 文件格式。由于 Android 操作系统的底层基于 Linux 系统,所以 SO 文件可以运行在 Android 平台上。

一般来说,SO 文件就类似于常说的动态链接库(DLL,即 Dynamic Link Library), 都是 C 或 C++ 编译出来的。

Android 系统也同样开放了 C/C++ 接口供开发者开发 Native 程序。由于基于虚拟机的编程语言 Java 更容易被人反编译,因此越来越多的应用将其中的核心代码以 C/C++ 为编程语言,并且以 SO 文件的形式供上层 Java 代码调用,以保证安全性。

Android ABI

不同于 Java 利用虚拟机实现「Compile Once, Run AnyWhere」的跨平台的特性,C/C++ 是「Write Once, Compile EveryWhere」的,这就导致了其需要针对不同的平台进行编译。

由于 Android 的开源,不同的厂家可以根据自己的需求在不同的硬件上接入 AOSP,因此不同的 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的 ABI(Application Binary Interface,应用二进制接口)。

目前常见的 ABI 有如下几种:

  • armeabi:第 5 代 ARM v5TE,使用软件浮点运算,兼容所有 ARM 设备,通用性强,速度慢(只支持 armeabi)。
  • armeabi-v7a:第 7 代 ARM v7,使用硬件浮点运算,具有高级扩展功能(支持 armeabiarmeabi-v7a)。
  • arm64-v8a:第 8 代,64 位,包含 AArch32、AArch64 两个执行状态对应 32-bit 和 64-bit(支持 armeabi-v7aarmeabiarm64-v8a)。
  • x86:Intel 32 位,一般用于平板(支持 x86armeabi 但性能有所损耗)。
  • x86_64:Intel 64 位,一般用于平板(支持 x86x86_64)。
  • mips:32 位,支持 RISC。
  • mips64:64 位,支持 RISC。

项目导入 SO 库

因为 SO 库是针对不同 CPU 架构编译出来的,所以其往往不会像 JAR 一样只会编译一份,而是会针对不同的 ABI 生成多个 SO 库,所以我们在使用时也需要往项目中导入多个 SO 文件。

由于包含 SO 文件的开源框架并不十分常见,我在这里就以 vivo 网游联运 SDK 为例进行介绍。

首先将其提供的 SDK 压缩文件下载到本地,并对其解压,得到如下目录:

vivo 网游联运 SDK 目录

可以看到,vivo 网游联运 SDK 提供了 armeabiarmeabi-v7aarm64-v8ax86x86_64 共五种架构的支持,SO 文件分别以同名的方式位于这五个文件夹下,这与我们的 res 类型同名文件存放方式是相似的。

接下来就可以导入了,『Android Studio』中 SO 库的导入有三种方式。

通过 libs 目录导入

理论上,『Android Studio』中所有的库文件都可以扔进「libs」目录,正如之前介绍过的『「Android Studio」如何导入 JAR 包』一般。

将 SO 文件及上层文件夹复制到 Module 的「libs」目录下:

通过 libs 目录导入 - Project 视图

由于 Java 代码调用 SO 库涉及到跨语言交互,所以必须通过 JNI(Java Native Interface)进行,同时,通过 JNI 交互的文件也必须通过标识才能被『Android Studio』处理,因此还需要在 Module 的「build.gradle」中加入如下代码:

android {
    ...
    sourceSets {
        main {
            jniLibs.srcDirs = ['libs']
        }
    }
}

同步之后即可在项目中使用了。

切换至「Android」视图下,你还可以发现『Android Studio』将其识别为「jniLibs」文件夹:

通过 libs 目录导入 - Android 视图

通过 jniLibs 目录导入

除了将「libs」目录指定为「jniLibs」文件夹,我们还可以为 SO 库创建专门的「jniLibs」目录。

/src/main 目录下新建一个名为「jniLibs」的文件夹,将 SO 文件及上层文件夹复制到该目录即可:

通过 jniLibs 目录导入

由于文件夹的名称已经做了标识,所以我们无需添加代码告知『Android Studio』去处理。

以 JAR 包的方式导入

这种方式比较少见。

我们可以将 SO 库按照如下目录结构压缩成 JAR,之前『「Android Studio」如何导入 JAR 包』一文也提到,JAR 是以 ZIP 格式构建的,所以可以使用任何压缩工具甚至是命令来压缩。

以 JAR 包的方式导入

然后按照『「Android Studio」如何导入 JAR 包』一文所提到的方法,跟导入普通 JAR 包一样导入即可。

当不同库兼容的 ABI 不一致

由于系统会根据 CPU 架构去运行不同的 SO 库,在实际开发项目中,假如我们引用了其他第三库,而这个库可能兼容的 ABI 与已有的不一致,就会产生问题。

举个例子,比如上方举例的 vivo 网游联运 SDK 兼容了 armeabiarmeabi-v7aarm64-v8ax86x86_64 五种架构,我现在引入微信语音识别 SDK,发现其支持 armeabi-v7aarmeabimipsx86 四种架构,导入后每个文件夹中的 SO 文件并不对等,运行到某些设备上就会崩溃。

导入两个支持不同 ABI 的 SDK

因为当应用运行时,设备会有先查看与 CPU 匹配的最优 ABI,如果没有,才会去查找其他 ABI,当 CPU 与 ABI 匹配上后,应用仅会读取该 ABI 下的 SO 库,其他 ABI 会直接被无视,假若该 ABI 缺失了对应的 SO 库,运行到对应代码时就会崩溃。

所以,遇到这种情况时,我们需要对 ABI 做处理。

仅保留交集 ABI

这种解决方法非常简单粗暴,就是取交集,仅保留所有库都支持的 ABI,其他文件夹都删除。

比如上方举例提到的两个第三方 SDK,取交集后可以保留 armeabiarmeabi-v7ax86 三种 ABI。

两个支持不同 ABI 的 SDK 取并集

当然,考虑到市面上设备占比以及目标用户分布,你甚至可以仅保留 armeabi-v7a 一种 ABI。

将已有的 SO 文件复制到其他 ABI 目录

前面提到,arm64-v8a 是兼容 armeabi-v7aarmeabi 的,所以,你还可以将其复制到空缺的文件夹。

比如将微信语音识别 SDKarmeabi 下的 SO 文件复制到 arm64-v8a 下,就可以使其支持 arm64-v8a 了。

ABI Filters

部分第三方库你还可能是通过远程依赖的,这时候对不同 SDK 的 ABI 处理就会变得复杂,但我们其实可以通过 Gradle 来操作。

在 Module 的「build.gradle」中加入如下代码:

android {
    ...
    defaultConfig {
        ...
        ndk {
            abiFilters 'armeabi', 'arm64-v8a', 'x86'
        }
    }
}

这样,『Android Studio』就可以针对我们指定的 ABI 进行构建了。

参考内容