Android应用so库兼容分析 

1,058 阅读10分钟

Android应用so库兼容分析 

「时光不负,创作不停,本文正在参加2022年中总结征文大赛

作者:Tang

将之前总结的文章分享下,希望对大家有帮助,若有错误的地方,欢迎评论区留言斧正。

前言

近期各大应用市场陆续要求安卓游戏包兼容64位,涉及到so文件的适配相关,查阅了相关资料,加之自己的理解,在此做下总结,有不足之处欢迎斧正。

1. so库文件是什么?

Android是基于Linux Kernel的操作系统,在Linux中,把一个或多个库包括到程序的过程叫做库链接(linking),有两种链接形式:静态链接动态链接;相应的,静态链接的库叫做静态库,动态链接的叫做动态库
Android的App一般采用动态链接库的形式,在链接动态库生成可执行文件时,并不会把动态库的代码复制到执行文件中,而是在执行文件中记录对动态库的引用,程序执行时再去加载动态库文件,可以有效避免静态链接生成的可执行文件大的缺点,如果动态库已经加载,则不必重复加载,从而能节省内存空间。
Linux下动态库文件的文件名形如 libxxx.so,so是 Shared Object 的缩写,顾名思义就是可以共享的目标文件,使用的是ELF(Executable and Linking Format)格式,它是机器可以直接运行的二进制代码。

2. so库文件怎么生成?

在Android中,通过Andriod NDK(Native Development Kit)编译C/Cpp代码(注:ndk-build命令)输出so文件,关于C/Cpp如何编译的配置,这里不再做详细解析,另撰文说明。 若是在gralde中直接编译NDK,可在gradle文件中配置ABI输出:

defaultConfig {  ndk {  abiFilters "armeabi", "armeabi-v7a", "x86", "mips"  } } 

3. so库文件怎么加载的?

so文件既然是Linux的动态库,在Android底层一样也是通过dlopen(filename,flags)来打开的,那从Java层到Linux层是如何加载的呢?大致流程如下:

注意:源码基于Android 6.0,目前so加载流程在>=7.0的版本上略有差异,底层加载so的流程还是类似的。 Java层一般调用System.loadLibrary方法加载so文件,内部其实是调用Runtime.loadLibaray来实现,方法声明如下:

public static void loadLibrary(String libname) {        
    Runtime.getRuntime().loadLibrary(VMStack.getCallingClassLoader(), libname); 
} 

后续的流程:Runtime.loadLibrary() --> Runtime.doLoad() --> Runtime_nativeLoad() -- > LoadNativeLibrary() --> dlopen() 。 底层还是调用Linux的dlopen()方法打开一个so文件,那在加载so的过程中,是如何找到对应so文件的呢? 我们重点来看Runtime.loadLibrary()方法:

void loadLibrary(String libraryName, ClassLoader loader) {
     if (loader != null) {
         //根据动态库名查找到相应动态库的文件路径
         String filename = loader.findLibrary(libraryName);
         if (filename == null) {
             throw new UnsatisfiedLinkError(...);
         }
         //找到了so文件路径,则继续执行加载
         String error = doLoad(filename, loader);
         if (error != null) {
             throw new UnsatisfiedLinkError(error);
         }
             return;
     }
     ...
     throw new UnsatisfiedLinkError(...);
}

源码中看到了大家可能都遇到过的熟悉的 UnsatisfiedLinkError 错误,这种错误一般是加载so失败时抛出的,大概率是so文件不存在或者路径错误,而这个路径查找,核心是findLibrary()方法,调用链如下: Runtime.loadLibrary() --> findLibrary() --> BaseDexClassLoader#findLibrary() --> DexPathList#findLibrary()

//platform/libcore/dalvik/src/main/java/dalvik/system/DexPathList.java  
/** List of native library directories. */
private final File[] nativeLibraryDirectories;  
public String findLibrary(String libraryName) {  
//1. mapLibraryName方法作用:拼接lib前缀和.so后缀,例如 libraryName 为'demo',最终返回:libdemo.so  
String fileName = System.mapLibraryName(libraryName);  

//2. 遍历所有存放 so 文件的目录,确认指定文件是否存在以及是只读文件  
for (File directory: nativeLibraryDirectories) {
    String path = new File(directory, fileName).getPath();  
    if (IoUtils.canOpenReadOnly(path)) {  return path;  }  }  
    return null; 
}  

重点就是 nativeLibraryDirectories 这个变量,这里存放着 so 文件存储的目录路径,在 nativeLibraryDirectories 指向的目录中寻找,若 so 文件存在且可用,则直接加载;一般安卓的第三方应用nativeLibraryDirectories返回值有可能就是 data/app/[PACKAGE_NAME]/lib 目录。 获取到so库的文件路径后,继续调用 Runtime.doLoad() --> Runtime_nativeLoad() -- > LoadNativeLibrary() --> dlopen() 完成so库文件的加载。

4. 如何适配不同ABI的so库文件?

要了解如何做so的适配,首选要明白不同 Android 设备使用不同的 CPU,而不同的 CPU 支持不同的指令集。CPU 与指令集的每种组合都有专属的应用程序二进制接口(ABI: Application Binary Interface,定义了so库文件如何运行在相应的系统平台上,从使用的指令集,内存对齐到可用的系统函数库)。不同的 ABI 对应不同的CPU架构,早期的Android系统几乎只支持ARMv5的CPU架构,发展至今已有7种:

  • ARMv5,
  • ARMv7 (从2010年起),
  • x86 (从2011年起),
  • MIPS (从2012年起),
  • ARMv8,
  • MIPS64,
  • x86_64 (从2014年起)

ABI具体用途说明(注意:NDK r17 已默认不再支持ARMv5 (armeabi) 以及 32 位、 64 位 MIPS):

  • mips / mips64: 极少用于手机,主要用于一些工业芯片,例如国产龙芯就采用mips指令集,在Android上可以忽略 。
  • armeabi: ARM v5 这是相当老旧的一个版本,缺少对浮点数计算的硬件支持,在需要大量计算时有性能瓶颈,目前的手机已基本不再使用。
  • x86 / x86_64: x86 架构的手机都会包含由 Intel 提供的称为 Houdini 的指令集动态转码工具,实现了对 arm的兼容,目前真机占比很低,主要是模拟器用的相对较多(我们业务数据统计占比约5%),因为多了一层转码,arm在模拟器的性能上会有所损耗。
  • armeabi-v7a: 针对有浮点运算或高级扩展功能的ARMv7 CPU,目前是Android应用选用的主流版本
  • arm64-v8a: ARMv8架构,64位支持,目前该架构是Android设备的主流(业务数据统计占比约92%) , 其中
    • 在 64 位的 ABI 有:arm64-v8a,mips64,x86_64
    • 运行在 32 位的 ABI 有:armeabi-v7a,armeabi,mips,x86
    根据上文,我们明白了不同ABI和CPU架构的关系和用途,也知道应用是如何加载so文件的,但应用中的so文件,系统是什么时候,如何处理的呢?我们从系统配置策略和查找策略来进一步说明。

4.1、so库文件系统配置策略

我们可以用 adb shell cat /proc/cpuinfo 命令来查看目标Android系统的cpu架构。

当一个应用安装在设备上,并不会将 apk 包中所有 abi 目录下的 so 文件都解压出来,只有该设备支持的CPU架构对应的.so文件会被解压安装,同时设置nativeLibraryDirectories 属性值,这个属性值代表应用自身的 so 文件存放地址,一般来说是放到/data/app/<package-name>/lib目录下。

应用在安装过程中,系统就已经确定你这个应用是应该运行在 64 位还是 32 位的进程上了,并将这个结果保存在 app 的 primaryCpuAbi 属性值中。不同CPU架构的Android手机加载时会在libs下找自己对应的目录下寻找需要的.so文件。(详见第3节中的findLibrary分析以及 Android ABI

4.2、so库文件查找策略

目前主流的Android设备是arm64-v8a架构,之后的才是armeabi-v7a、armeabi和x86。如果包内同时包含了所有ABI的so库文件,那所有设备都可以运行,程序在运行的时候会去加载不同ABI对应的so,perfect!但问题是这样会导致包size变得很大,包size会直接影响玩家的转化,对推广效果影响大,所以要保证兼容的前提下裁剪性价比较低的so库, 我们针对各种情况做如下分析:

  • armeabi-v7a和armeabi都是32位指令,可以兼容运行在64位设备上。armeabi的设备缺少对浮点数的硬件支持,从业务数据统计来看,占比非常低,可以直接排除掉,应用只要保留armeabi-v7a即可。
  • x86属于32位指令,X86的设备可以直接运行arm指令32位的应用,除非是有性能的极致要求,否则没必要单独出x86的so库。
  • arm64-v8a和x86_64都是64位CPU指令集,arm64-v8a在真机是主流,但从兼容角度来说,应用同时兼容arm64-v8aarmeabi-v7a,同样会使包size增加,可以根据实际情况做包大小和性能的权衡,
    • 若首要考虑包size,次要考虑兼容,那只保留armeabi-v7a是对包size和兼容性的妥协。64位机型默认支持32位so的加载。
    • 若首要考虑性能,次要考虑包size,那只保留arm64-v8a是较好的答案。不过要注意只保留arm64-v8a在32位设备不能安装
    • 若首要考虑性能,包size不做要求,则同时保留arm64-v8a和armeabi-v7a,是对兼容性和性能权衡之后的较好选择。

so放置不当时,安装apk时就会存在so拷贝不全的情况,导致加载so异常,所以我们兼容的核心就是确保在正确的ABI中放入对应的so库文件,保证应用在安装时PMS解压正确完整的so文件集。 一个重要需遵循的原则尽可能的提供专为每个ABI优化过的.so文件,但要么全部支持,要么都不支持:不同 ABI的 so 文件不能够混合使用。  同时要注意:64位的so库文件不能直接复制到32位的ABI中使用,同样32位的so库文件也不能直接放到64位的ABI目录中。 否则会抛出类似如下异常:

   java.lang.UnsatisfiedLinkError: dlopen failed: "libdemo.so" is 32-bit instead of 64-bit 

4.3、如何兼容第三方平台的so库

按照上述不同ABI中so库文件不混用原则,需保证第三方平台的so库在不同ABI中保持对齐:

  • 如果第三方提供了不同平台的.so文件,则复制不同平台的.so文件到项目中对应的文件夹下即可。
  • 如果第三方库只提供了armeabi下的.so文件,我们项目里适配了armeabi-v7a和x86,如果不在对应的文件下放对应的.so文件,就可能导致某些Android设备会出一些问题,我们可以复制armeabi下得.so文件到不同的文件夹下。这里要注意之前提到的32位so不能放到64位的ABI中,此时若你的应用程序需要兼容64位,那就需要第三方提供64位ABI的so,或者删除应用本身64位的ABI,以保证兼容性。

5. 小结

本文主要介绍了Android系统so库文件的来龙去脉,包括生成、加载流程、安装时的解压配置策略、应用so库文件的兼容适配等,希望对大家能有所助益。


参考资料:
函数库:zh.wikipedia.org/wiki/%E5%87…
兼容64位:developer.android.google.cn/distribute/…
System.loadLibrary:developer.android.com/reference/j…
loadLibrary动态库加载过程分析:gityuan.com/2017/03/26/… Android ABI:developer.android.com/ndk/guides/…
System V Application Binary Interface:refspecs.linuxfoundation.org/elf/gabi4+/…