之前的文章『「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,使用硬件浮点运算,具有高级扩展功能(支持armeabi
和armeabi-v7a
)。arm64-v8a
:第 8 代,64 位,包含 AArch32、AArch64 两个执行状态对应 32-bit 和 64-bit(支持armeabi-v7a
、armeabi
和arm64-v8a
)。x86
:Intel 32 位,一般用于平板(支持x86
和armeabi
但性能有所损耗)。x86_64
:Intel 64 位,一般用于平板(支持x86
和x86_64
)。mips
:32 位,支持 RISC。mips64
:64 位,支持 RISC。
项目导入 SO 库
因为 SO 库是针对不同 CPU 架构编译出来的,所以其往往不会像 JAR 一样只会编译一份,而是会针对不同的 ABI 生成多个 SO 库,所以我们在使用时也需要往项目中导入多个 SO 文件。
由于包含 SO 文件的开源框架并不十分常见,我在这里就以 vivo 网游联运 SDK 为例进行介绍。
首先将其提供的 SDK 压缩文件下载到本地,并对其解压,得到如下目录:
可以看到,vivo 网游联运 SDK 提供了 armeabi
、armeabi-v7a
、arm64-v8a
、x86
和 x86_64
共五种架构的支持,SO 文件分别以同名的方式位于这五个文件夹下,这与我们的 res
类型同名文件存放方式是相似的。
接下来就可以导入了,『Android Studio』中 SO 库的导入有三种方式。
通过 libs
目录导入
理论上,『Android Studio』中所有的库文件都可以扔进「libs」目录,正如之前介绍过的『「Android Studio」如何导入 JAR 包』一般。
将 SO 文件及上层文件夹复制到 Module 的「libs」目录下:
由于 Java 代码调用 SO 库涉及到跨语言交互,所以必须通过 JNI(Java Native Interface)进行,同时,通过 JNI 交互的文件也必须通过标识才能被『Android Studio』处理,因此还需要在 Module 的「build.gradle」中加入如下代码:
android {
...
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
同步之后即可在项目中使用了。
切换至「Android」视图下,你还可以发现『Android Studio』将其识别为「jniLibs」文件夹:
通过 jniLibs
目录导入
除了将「libs」目录指定为「jniLibs」文件夹,我们还可以为 SO 库创建专门的「jniLibs」目录。
在 /src/main
目录下新建一个名为「jniLibs」的文件夹,将 SO 文件及上层文件夹复制到该目录即可:
由于文件夹的名称已经做了标识,所以我们无需添加代码告知『Android Studio』去处理。
以 JAR 包的方式导入
这种方式比较少见。
我们可以将 SO 库按照如下目录结构压缩成 JAR,之前『「Android Studio」如何导入 JAR 包』一文也提到,JAR 是以 ZIP 格式构建的,所以可以使用任何压缩工具甚至是命令来压缩。
然后按照『「Android Studio」如何导入 JAR 包』一文所提到的方法,跟导入普通 JAR 包一样导入即可。
当不同库兼容的 ABI 不一致
由于系统会根据 CPU 架构去运行不同的 SO 库,在实际开发项目中,假如我们引用了其他第三库,而这个库可能兼容的 ABI 与已有的不一致,就会产生问题。
举个例子,比如上方举例的 vivo 网游联运 SDK 兼容了 armeabi
、armeabi-v7a
、arm64-v8a
、x86
和 x86_64
五种架构,我现在引入微信语音识别 SDK,发现其支持 armeabi-v7a
、armeabi
、mips
和 x86
四种架构,导入后每个文件夹中的 SO 文件并不对等,运行到某些设备上就会崩溃。
因为当应用运行时,设备会有先查看与 CPU 匹配的最优 ABI,如果没有,才会去查找其他 ABI,当 CPU 与 ABI 匹配上后,应用仅会读取该 ABI 下的 SO 库,其他 ABI 会直接被无视,假若该 ABI 缺失了对应的 SO 库,运行到对应代码时就会崩溃。
所以,遇到这种情况时,我们需要对 ABI 做处理。
仅保留交集 ABI
这种解决方法非常简单粗暴,就是取交集,仅保留所有库都支持的 ABI,其他文件夹都删除。
比如上方举例提到的两个第三方 SDK,取交集后可以保留 armeabi
、armeabi-v7a
和 x86
三种 ABI。
当然,考虑到市面上设备占比以及目标用户分布,你甚至可以仅保留 armeabi-v7a
一种 ABI。
将已有的 SO 文件复制到其他 ABI 目录
前面提到,arm64-v8a
是兼容 armeabi-v7a
和 armeabi
的,所以,你还可以将其复制到空缺的文件夹。
比如将微信语音识别 SDK 中 armeabi
下的 SO 文件复制到 arm64-v8a
下,就可以使其支持 arm64-v8a
了。
ABI Filters
部分第三方库你还可能是通过远程依赖的,这时候对不同 SDK 的 ABI 处理就会变得复杂,但我们其实可以通过 Gradle 来操作。
在 Module 的「build.gradle」中加入如下代码:
android {
...
defaultConfig {
...
ndk {
abiFilters 'armeabi', 'arm64-v8a', 'x86'
}
}
}
这样,『Android Studio』就可以针对我们指定的 ABI 进行构建了。