×

Android SO文件的概念、兼容、适配和可能的错误

96
仁昌居士
2017.10.14 09:25* 字数 4353

转载整合自: 关于Android的.so文件你所需要知道的
Android SO 文件的兼容和适配
不论是否被发现,一切荣誉归属于大佬。

现有的CPU架构类型

开发Android应用时,有时候Java层的编码不能满足实现需求,就需要到C/C++实现后生成SO文件,再用System.loadLibrary()加载进行调用,这里成为JNI层的实现。常见的场景如:加解密算法,音视频编解码等。在生成SO文件时,需要考虑适配市面上不同手机CPU架构,而生成支持不同平台的SO文件进行兼容。目前Android共支持七种不同类型的CPU架构,分别是:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起)

现有的常用ABI

应用程序二进制接口(Application Binary Interface)定义了其所对应的CPU架构能够执行的二进制文件(特别是.so文件)的格式规范。在Android系统上,不同 Android 手机使用不同的 CPU,因此支持不同的指令集。CPU 与指令集的每种组合都有其自己的应用二进制界面(或 ABI)。 ABI 可以非常精确地定义应用的机器代码在运行时如何与系统交互。 您必须为应用要使用的每个 CPU 架构指定 ABI:armeabi,armeabi-v7a,x86,mips,arm64-v8a,mips64,x86_64【1】。

典型的 ABI 包含以下信息:

  • 机器代码应使用的 CPU 指令集。
  • 运行时内存存储和加载的字节顺序。
  • 可执行二进制文件(例如程序和共享库)的格式,以及它们支持的内容类型。
  • 用于解析内容与系统之间数据的各种约定。这些约定包括对齐限制,以及系统如何使用堆栈和在调用函数时注册。
  • 运行时可用于机器代码的函数符号列表 - 通常来自非常具体的库集。
  • 支持一个或多个指令集。

SO(CPU)的兼容

每一个CPU架构对应一个ABI,一个cpu属于某一种架构,多核cpu需要属于相同架构才能一起工作,很多设备仅支持一种的CPU架构。

如果你要完美兼容所有类型的手机,理论上是要在的libs目录下放置各个架构平台的SO文件。

这样一写,虽然可以兼容所有机型,但你的项目体积也会变得非常庞大。是否一定需要带入这么多SO文件去兼容呢?答案是否定的。

对于CPU来说,不同的架构并不意味着一定互不兼容,根据目前Android共支持七种不同类型的CPU架构,其兼容特点可总结如下:

armeabi设备只兼容armeabi;
armeabi-v7a设备兼容armeabi-v7a、armeabi;
arm64-v8a设备兼容arm64-v8a、armeabi-v7a、armeabi;
X86设备兼容X86、armeabi;
X86_64设备兼容X86_64、X86、armeabi;
mips64设备兼容mips64、mips;
mips只兼容mips;

根据以上的兼容总结,我们还可以得到一些规律:

  • armeabi的SO文件基本上可以说是万金油,它能运行在除了mips和mips64的设备上,但在非armeabi设备上运行性能还是有所损耗;
  • 64位的CPU架构总能向下兼容其对应的32位指令集,如:x86_64兼容X86,arm64-v8a兼容armeabi-v7a,mips64兼容mips;

.so文件的相关注意事项

SO的适配

当一个应用安装在设备上,只有该设备支持的CPU架构对应的.so文件会被安装。在x86设备上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件(因为x86设备也支持armeabi-v7a和armeabi)。

从目前移动端CPU市场的份额数据看,ARM架构几乎垄断,所以,除非你的用户很特殊,否则几乎可以不考虑单独编译带入X86、X86_64、mips、mips64架构SO文件。除去这四个架构之后,还要带入armeabi、armeabi-v7a、arm64-v8a这三个不同类型,这对于一个拥有大量SO文件的应用来说,安装包的体积将会增大不少。

针对以上情况,我们可以应用的设备分布和市场情况再进行取舍斟酌,如果你的应用仍有不少armeabi类型的设备,可以考虑只保留armeabi目录下的SO文件(万金油特性)。但是,尽管armeabi可以兼容多种平台,仍有些运算在armeabi-v7a、arm64-v8a,去使用armeabi的SO文件时,性能会非常差强人意,所以还是应该用其对应平台架构的SO文件进行运算。

注意:
这里并不是要带多一整套SO文件到不同的目录下,而是将性能差异比较明显的某个armeabi-v7a、arm64-v8a平台下的SO文件放到armeabi目录,然后通过代码判断设备的CPU类型,再加载其对应架构的SO文件,很多大厂的应用便是这么做的。你应该尽可能的提供专为每个ABI优化过的.so文件,但要么全部支持,要么都不支持:你不应该混合着使用。你应该为每个ABI目录提供对应的.so文件。
如微信的lib下虽然只有armeabi一个目录,但目录内的文件仍放着v5、v7a架构的SO文件,用于处理兼容带来的某些性能运算问题。

微信的armeabi一个目录

总结
就目前市场份额而言,绝大部分的设备都已经是armeabi-v7a、arm64-v8a,你也可以考虑只保留armeabi-v7a架构的SO文件,这样能获得更好的性能效果。性能差异比较明显加入单的的so文件并在代码中去判断。

引入.so文件的错误

当你引入一个.so文件时,不止影响到CPU架构。从其他开发者那里可以看到一系列常见的错误,其中最多的是"UnsatisfiedLinkError","dlopen: failed"以及其他类型的crash或者低下的性能:

1. 使用android-21平台版本编译的.so文件运行在android-15的设备上

使用NDK时,你可能会倾向于使用最新的编译平台,但事实上这是错误的,因为NDK平台不是后向兼容的,而是前向兼容的。推荐使用app的minSdkVersion对应的编译平台。

这也意味着当你引入一个预编译好的.so文件时,你需要检查它被编译所用的平台版本。

2. 混合使用不同C++运行时编译的.so文件

.so文件可以依赖于不同的C++运行时,静态编译或者动态加载。混合使用不同版本的C++运行时可能导致很多奇怪的crash,是应该避免的。作为一个经验法则,当只有一个.so文件时,静态编译C++运行时是没问题的,否则当存在多个.so文件时,应该让所有的.so文件都动态链接相同的C++运行时。

这意味着当引入一个新的预编译.so文件,而且项目中还存在其他的.so文件时,我们需要首先确认新引入的.so文件使用的C++运行时是否和已经存在的.so文件一致。

3. 没有为每个支持的CPU架构提供对应的.so文件

这一点在前文已经说到了,但你应该真的特别注意它,因为它可能发生在根本没有意识到的情况下。

例如:你的app支持armeabi-v7a和x86架构,然后使用Android Studio新增了一个函数库依赖,这个函数库包含.so文件并支持更多的CPU架构,例如新增android-gif-drawable函数库:

compile ‘pl.droidsonroids.gif:android-gif-drawable:1.1.+’

发布我们的app后,会发现它在某些设备上会发生Crash,例如Galaxy S6,最终可以发现只有64位目录下的.so文件被安装进手机。

解决方案:重新编译我们的.so文件使其支持缺失的ABIs,或者设置

ndk.abiFilters

显示指定支持的ABIs。

注意事项:如果你是一个SDK提供者,但提供的函数库不支持所有的ABIs,那你将会搞砸你的用户,因为他们能支持的ABIs必将只能少于你提供的。

4. 将.so文件放在错误的地方

我们往往很容易对.so文件应该放在或者生成到哪里感到困惑,下面是一个总结:

  • Android Studio工程放在jniLibs/ABI目录中(当然也可以通过在build.gradle文件中的设置jniLibs.srcDir属性自己指定)
  • Eclipse工程放在libs/ABI目录中(这也是ndk-build命令默认生成.so文件的目录)
  • AAR压缩包中位于jni/ABI目录中(.so文件会自动包含到引用AAR压缩包的APK中)
  • 最终APK文件中的lib/ABI目录中
    通过PackageManager安装后,在小于Android 5.0的系统中,.so文件位于app的nativeLibraryPath目录中;在大于等于Android 5.0的系统中,.so文件位于app的nativeLibraryRootDir/CPU_ARCH目录中。

5. 只提供armeabi架构的.so文件而忽略其他ABIs的

如前文提到的,所有的x86/x86_64/armeabi-v7a/arm64-v8a设备都支持armeabi架构的.so文件,因此似乎移除其他ABIs的.so文件是一个减少APK大小的好技巧。但事实上并不是:这不只影响到函数库的性能和兼容性。

x86设备能够很好的运行ARM类型函数库,但并不保证100%不发生crash,特别是对旧设备。64位设备(arm64-v8a, x86_64, mips64)能够运行32位的函数库,但是以32位模式运行,在64位平台上运行32位版本的ART和Android组件,将丢失专为64位优化过的性能(ART,webview,media等等)。

以减少APK包大小为由是一个错误的借口,因为你也可以选择在应用市场上传指定ABI版本的APK,生成不同ABI版本的APK可以在build.gradle中如下配置:

android {
    ... 
    splits {
        abi {
            enable true
            reset()
            include 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' //select ABIs to build APKs for
            universalApk true //generate an additional APK that contains all the ABIs
        }
    }

    // map for the version code
    project.ext.versionCodes = ['armeabi': 1, 'armeabi-v7a': 2, 'arm64-v8a': 3, 'mips': 5, 'mips64': 6, 'x86': 8, 'x86_64': 9]

    android.applicationVariants.all { variant ->
        // assign different version code for each output
        variant.outputs.each { output ->
            output.versionCodeOverride =
                    project.ext.versionCodes.get(output.getFilter(com.android.build.OutputFile.ABI), 0) * 1000000 + android.defaultConfig.versionCode
        }
    }
 }

附录:

【1】

ABI 和支持的指令集

armeabi
此 ABI 适用于基于 ARM、至少支持 ARMv5TE 指令集的 CPU。 请参阅以下文档了解详情:

AAPCS 标准将 EABI 定义为类似但不同 ABI 的系列。 此外,Android 还采用小字节序 ARM GNU/Linux ABI
此 ABI 不支持硬件辅助的浮点计算。 相反,所有浮点运算都使用编译器 libgcc.a
静态库中的软件帮助程序函数。
armeabi ABI 支持 ARM 的 Thumb(亦称 Thumb-1)指令集。NDK 默认生成 Thumb 代码,除非您在 Android.mk 文件中使用 LOCAL_ARM_MODE变量指定不同的行为。

armeabi-v7a
此 ABI 可扩展 armeabi 以包含多个 CPU 指令集扩展。 此 Android 特定 ABI 支持的指令扩展包括:

  • Thumb-2 指令集扩展,其性能堪比 32 位 ARM 指令,简洁性类似于 Thumb-1。
  • VFP 硬件 FPU 指令。更具体一点,包括 VFPv3-D16,它除了 ARM 核心中的 16 个 32 位寄存器之外,还包含 16 个专用 64 位浮点寄存器。

v7-a ARM 规格描述的其他扩展,包括 高级 SIMD(亦称 NEON)、VFPv3-D32ThumbEE,都是此 ABI 可选的。 由于不能保证它们存在,因此系统在运行时应检查扩展是否可用。 如果不可用,则必须使用替代代码路径。此检查类似于系统在检查或使用 MMXSSE2 及 x86 CPU 上其他专用指令集时所执行的检查。

如需了解有关如何执行这些运行时检查的信息,请参阅 cpufeatures
。另外,有关 NDK 支持为 NEON 构建机器代码的信息,请参阅 NEON 支持

armeabi-v7a ABI 使用 -mfloat-abi=softfp开关强制实施规则,要求编译器在函数调用时必须传递核心寄存器对中的所有双精度值,而不是专用浮点值。 系统可以使用 FP 寄存器执行所有内部计算。 这样可极大地加速计算。

arm64-v8a
此 ABI 适用于基于 ARMv8、支持 AArch64 的 CPU。它还包含 NEON 和 VFPv4 指令集。
如需了解详细信息,请参阅 ARMv8 技术预览,并联系 ARM 了解进一步的详细信息。

x86
此 ABI 适用于支持通常称为“x86”或“IA-32”的指令集的 CPU。 此 ABI 的特性包括:

  • 指令一般由具有编译器标志的 GCC 生成,如下所示:
-march=i686 -mtune=intel -mssse3 -mfpmath=sse -m32

这些标志指向 Pentium Pro 指令集,伴随 MMXSSESSE2SSE3SSSE3 指令集扩展。生成的代码在顶层 Intel 32 位 CPU 之间进行了均衡优化。
如需了解有关编译器标志的详细信息,特别是与性能优化相关的信息,请参阅 GCC x86 性能提示

ABI 不含任何其他可选的 IA-32 指令集扩展,例如:

  • MOVBE
  • SSE4 的任何变体。

您仍可使用这些扩展,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供备用方法。
NDK 工具链假设在函数调用之前进行 16 位栈对齐。默认工具和选项强制执行此规则。 如果编写的是汇编代码,必须确保栈对齐,而且其他编译器也遵守此规则。
请参阅以下文档了解详情:

x86_64
此 ABI 适用于支持通常称为“x86-64”的指令集的 CPU。 它支持 GCC 通常使用以下编译器标志生成的指令:

-march=x86-64 -msse4.2 -mpopcnt -m64 -mtune=intel

这些标志指向 x86-64 指令集(根据 GCC 文档),伴随 MMXSSESSE2SSE3SSSE3SSE4.1SSE4.2POPCNT 指令集扩展。 生成的代码在顶层 Intel 64 位 CPU 之间进行了均衡优化。
如需了解有关编译器标志的详细信息,特别是与性能优化相关的信息,请参阅 GCC x86 性能
此 ABI 不含任何其他可选的 x86-64 指令集扩展,例如:

  • MOVBE
  • SHA
  • AVX
  • AVX2

您仍可使用这些扩展,只要您使用运行时功能探测来启用它们,并且为不支持它们的设备提供备用方法。
请参阅以下文档了解详情:

mips
此 ABI 适用于基于 MIPS、至少支持 MIPS32r1 指令集的 CPU。它包含以下功能:

  • MIPS32 修订版 1 ISA
  • 小字节序
  • O32
  • 硬浮点
  • 无 DSP 应用特定的扩展

如需了解详细信息,请参阅以下文档:

如需了解更具体的详细信息,请参阅 MIPS32 架构。常见问答请参阅 MIPS FAQ

mips64
此 ABI 适用于 MIPS64 R6。如需了解详细信息,请参阅 MIPS64 架构

参考文章

关于Android的.so文件你所需要知道的
Android SO 文件的兼容和适配
ABI Management

日记本
Web note ad 1