1、背景
实际工作中经常遇到so相关的错误,主要包括:
常见问题一:
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.xxx.xxx-1/base.apk”],nativeLibraryDirectories=[/data/app/com.xxx.xxx-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn’t find “xxx.so”,
常见问题二:
java.lang.UnsatisfiedLinkError: dlopen failed: “/data/app/com.xxx.xxx-2/lib/arm64/xxx.so” is 32-bit instead of 64-bit
想要从根本上了解导致上述问题的原因,我们需要了解android系统的so加载流程
2、动态链接库的加载流程
首先从宏观流程上来看,对于 load 过程我们分为 find&load,首先是要找到 so 所在的位置,然后才是 load 加载进内存,先贴一张图看看 so 加载的大概流程。
这个图里面有4个关键点:
- SO查找和加载:System.loadLibrary只会在固定的几个目录(nativeLibraryDirectories)中找so文件是否存在,如果找到则用该so的绝对路径去查询是否加载过,没有则会真正去加载so
- SO查找的路径(nativeLibraryDirectories)来源:主要来自3个地方:data/app-lib/ , data/app/<apkname>.apk!/lib/ , system property(“/vendor/lib, /system/lib”)
- primaryAbi 获取:请参考下面2.1节的介绍
- SO拷贝:通过拼接 nativeLibraryDirectories 和 primaryAbi 得到最终so的完整路径(mLibDir ),并将apk的对应的so拷贝到该路径下面。
2.1、获取PrimaryAbi流程
primaryAbi获取流程可以简单概括以下2个步骤:
- 获取手机系统支持的abilist
- 遍历 apk(其实就是一个压缩文件)中的所有文件,如果文件全路径中包含 abilist 中的某个 abi 字符串,则记录该 abi 字符串的索引,最终返回所有记录索引中最靠前的,即排在 abilist 中最前面的索引
2.1.1、获取手机系统支持的abilist
查询手机支持的ABI 列表的两种方式:
a).通过命令行
1 |
getprop ro.product.cpu.abilist |
具体操作如下,华为mate8手机支持的abi列表为:arm64-v8a, armeabi-v7a, armeabi
b). 在代码中获取
1 |
Build.SUPPORTED_ABIS |
得到的结果如下:
2.1.2、获取primaryAbi
获取primaryAbi的逻辑在NativeLibraryHelper 中的 findSupportedAbi 方法中,它的核心代码主要如下,基本就是我们前文说的主要逻辑,遍历 apk(其实就是一个压缩文件)中的所有文件,如果文件全路径中包含 abilist 中的某个 abi 字符串,则记录该 abi 字符串的索引,最终返回所有记录索引中最靠前的,即排在 abilist 中最前面的索引。
findSupportedAbi核心代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
static int findSupportedAbi(JNIEnv *env, jlong apkHandle, jobjectArray supportedAbisArray, jboolean debuggable) { const int numAbis = env->GetArrayLength(supportedAbisArray); Vector<ScopedUtfChars*> supportedAbis; for (int i = 0; i < numAbis; ++i) { supportedAbis.add(new ScopedUtfChars(env, (jstring) env->GetObjectArrayElement(supportedAbisArray, i))); } ZipFileRO* zipFile = reinterpret_cast<ZipFileRO*>(apkHandle); if (zipFile == NULL) { return INSTALL_FAILED_INVALID_APK; } std::unique_ptr<NativeLibrariesIterator> it( NativeLibrariesIterator::create(zipFile, debuggable)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; int status = NO_NATIVE_LIBRARIES; while ((entry = it->next()) != NULL) { // We're currently in the lib/ directory of the APK, so it does have some native // code. We should return INSTALL_FAILED_NO_MATCHING_ABIS if none of the // libraries match. if (status == NO_NATIVE_LIBRARIES) { status = INSTALL_FAILED_NO_MATCHING_ABIS; } const char* fileName = it->currentEntry(); const char* lastSlash = it->lastSlash(); // Check to see if this CPU ABI matches what we are looking for. const char* abiOffset = fileName + APK_LIB_LEN; const size_t abiSize = lastSlash - abiOffset; for (int i = 0; i < numAbis; i++) { const ScopedUtfChars* abi = supportedAbis[i]; if (abi->size() == abiSize && !strncmp(abiOffset, abi->c_str(), abiSize)) { // The entry that comes in first (i.e. with a lower index) has the higher priority. if (((i < status) && (status >= 0)) || (status < 0) ) { status = i; } } } } for (int i = 0; i < numAbis; ++i) { delete supportedAbis[i]; } return status; } |
举个例子,假如我们的 app 中的 so 地址中有包含 arm64-v8a 的字符串,同时 abilist 是 arm64-v8a,armeabi-v7a,armeab,那么这里就会返回 arm64-v8a。
2.2、SO拷贝
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ...省略部分代码 if (cpuAbi.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { ALOGV("Using primary ABI %s\n", cpuAbi.c_str()); hasPrimaryAbi = true; } else if (cpuAbi2.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi2.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) { /* * If this library matches both the primary and secondary ABIs, * only use the primary ABI. */ if (hasPrimaryAbi) { ALOGV("Already saw primary ABI, skipping secondary ABI %s\n", cpuAbi2.c_str()); continue; } else { ALOGV("Using secondary ABI %s\n", cpuAbi2.c_str()); } } else { ALOGV("abi didn't match anything: %s (end at %zd)\n", cpuAbiOffset, cpuAbiRegionSize); continue; } // If this is a .so file, check to see if we need to copy it. if ((!strncmp(fileName + fileNameLen - LIB_SUFFIX_LEN, LIB_SUFFIX, LIB_SUFFIX_LEN) && !strncmp(lastSlash, LIB_PREFIX, LIB_PREFIX_LEN) && isFilenameSafe(lastSlash + 1)) || !strncmp(lastSlash + 1, GDBSERVER, GDBSERVER_LEN)) { install_status_t ret = callFunc(env, callArg, &zipFile, entry, lastSlash + 1); if (ret != INSTALL_SUCCEEDED) { ALOGV("Failure for entry %s", lastSlash + 1); return ret; } } } return INSTALL_SUCCEEDED; } |
主要的策略就是,遍历 apk 中文件,当遍历到有主 Abi 目录的 so 时,拷贝并设置标记 hasPrimaryAbi 为真,以后遍历则只拷贝主 Abi 目录下的 so。这个主 Abi 就是我们前面 findSupportedAbi 的时候找到的那个 abi 的值,大家可以去回顾下。
当标记为假的时候,如果遍历的 so 的 entry 名包含其他abi字符串,则拷贝该 so,拷贝 so 到我们上文说到 mLibDir 这个目录下。
这里有一个很重要的策略是:ZipFileRO 的遍历顺序,他是根据文件对应 ZipFileR0 中的 hash 值而定,而对于已经 hasPrimaryAbi 的情况下,非 PrimaryAbi 是直接跳过 copy 操作的,所以这里可能会出现很多拷贝 so 失败的情况。
举个例子:假设存在这样的 apk, lib 目录下存在 armeabi/libx.so , armeabi/liby.so , armeabi-v7a/libx.so 这三个 so 文件,且 hash 的顺序为 armeabi-v7a/libx.so 在 armeabi/liby.so 之前,则 apk 安装的时候 liby.so 根本不会被拷贝,因为按照拷贝策略, armeabi-v7a/libx.so 会优先遍历到,由于它是主 abi 目录的 so 文件,所以标记被设置了,当遍历到 armeabi/liby.so 时,由于标记被设置为真, liby.so 的拷贝就被忽略了,从而在加载 liby.so 的时候会报异常。
3、问题回顾
问题1:
java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file “/data/app/com.xxx.xxx-1/base.apk”],nativeLibraryDirectories=[/data/app/com.xxx.xxx-1/lib/arm64, /vendor/lib64, /system/lib64]]] couldn’t find “xxx.so”,
分析:
通过上面的分析我们不难了解到System.loadLibrary首先会去特定的几个目录查找so是否存在,如果不存在则会报这个错误。有两种原因:
- app没有这个so文件。需要加回来
- app 中对应到系统的primaryAbi目录下面的so不全,需要确认app primaryAbi目录下面是否有该so文件
问题2:
java.lang.UnsatisfiedLinkError: dlopen failed: “/data/app/com.xxx.xxx-2/lib/arm64/xxx.so” is 32-bit instead of 64-bit
分析:
System.loadLibrary 方法在查找到so所在的绝对路径后会去加载该so,但是如果该so不兼容64位就会报错。解决办法很简单,重新打一个支持64位的so放在app对应的abi目录下面就可以了。
拓展学习:
Android 在5.0以后其实已经支持64位了,而对于很多时候大家在运行so的时候也会遇到这样的错误:dlopen failed: “xx.so” is 32-bit instead of 64-bit,这种情况其实是因为进程由 64zygote 进程 fork 出来,在64位的进程上必须要64位的动态链接库。
Art 上支持64位程序的主要策略就是区分了 zygote32 和 zygote64,对于32位的程序通过 zygote32 去 fork 而64位的自然是通过 zygote64去 fork。
问题总结:
所以当你的 app 中有64位的 abi,那么就必须所有的 so 文件都有64位的,不能出现一部分64位的一部分32位的,当你的 app 发现 primaryAbi 是64位的时候,他就会通过 zygote64 fork 在64位下,那么其他的32位 so 在 dlopen 的时候就会失败报错。
4、拓展学习
4.1、各种CPU架构的兼容关系
目前android支持如下7中CPU架构:
- armeabi 第5代 ARM v5TE,使用软件浮点运算,兼容所有ARM设备,通用性强,速度慢(只支持armeabi)
- armeabi-v7a 第7代 ARM v7,使用硬件浮点运算,具有高级扩展功能(支持 armeabi 和 armeabi-v7a,目前大部分手机都是这个架构)
- arm64-v8a 第8代,64位,包含AArch32、AArch64两个执行状态对应32、64bit(支持 armeabi-v7a、armeabi 和 arm64-v8a)
- *x86 intel 32位,一般用于平板(支持 armeabi(性能有所损耗) 和 x86)
- x86_64 intel 64位,一般用于平板(支持 x86 和 x86_64)
- mips 基本没见过(支持 mips)
- mips64 基本没见过(支持 mips 和 mips_64)
对应的映射关系表如下:
1 2 3 4 5 6 7 8 9 |
{ mAbiCompatibleMapping.put(ARMEABI, new String[]{ARMEABI}); mAbiCompatibleMapping.put(ARMV7A, new String[]{ARMV7A, ARMEABI}); mAbiCompatibleMapping.put(ARM64, new String[]{ARM64, ARMV7A, ARMEABI}); mAbiCompatibleMapping.put(X86, new String[]{X86}); mAbiCompatibleMapping.put(X86_64, new String[]{X86_64, X86}); mAbiCompatibleMapping.put(MIPS, new String[]{MIPS}); mAbiCompatibleMapping.put(MIPS64, new String[]{MIPS64, MIPS}); } |
5、参考文档
Android 动态链接库加载原理及 HotFix 方案介绍