引用自:http://crash.163.com/#news/!newsId=5
0X0 前言
在 Android 系统中,当我们安装apk文件时,lib目录下的so文件会被解压到app的原生库目录。一般来说,这些so文件会放置在/data/data//lib目录下。然而,根据系统和CPU架构的不同,其拷贝策略也会有所差异。在我们的测试过程中发现了一些不正确配置so文件的情况。例如,在某些应用程序使用第三方so时,只配置了其中某一种CPU架构的so可能导致应用程序在某些机型上适配问题。因此,本文主要介绍了不同版本Android系统中PackageManagerService选择解压so库的策略,并提供了一些建议来正确配置so文件。
0x1 Android4.0以前
当 apk 被安装时,虽然执行路径有所不同,但最终都会调用到一个核心函数——copyApk。该函数的主要职责是拷贝 apk 中的资源。
根据 Android 源码的 2.3.6 版本,其内部函数 copyApk 包含了一段选取原生库 so 的逻辑。
public static int listPackageNativeBinariesLI(ZipFile zipFile, List> nativeFiles) throws ZipException, IOException { String cpuAbi = Build.CPU_ABI; int result = listPackageSharedLibsForAbiLI(zipFile, cpuAbi, nativeFiles); /* * Some architectures are capable of supporting several CPU ABIs * for example, 'armeabi-v7a' also supports 'armeabi' native code * this is indicated by the definition of the ro.product.cpu.abi2 * system property. * * only scan the package twice in case of ABI mismatch */ if (result == PACKAGE_INSTALL_NATIVE_ABI_MISMATCH) { final String cpuAbi2 = SystemProperties.get("ro.product.cpu.abi2", null); if (cpuAbi2 != null) { result = listPackageSharedLibsForAbiLI(zipFile, cpuAbi2, nativeFiles); } if (result == PACKAGE_INSTALL_NATIVE_ABI_MISMATCH) { Slog.w(TAG, "Native ABI mismatch from package file"); return PackageManager.INSTALL_FAILED_INVALID_APK; } if (result == PACKAGE_INSTALL_NATIVE_FOUND_LIBRARIES) { cpuAbi = cpuAbi2; } } /* * Debuggable packages may have gdbserver embedded, so add it to * the list to the list of items to be extracted (as lib/gdbserver) * into the application's native library directory later. */ if (result == PACKAGE_INSTALL_NATIVE_FOUND_LIBRARIES) { listPackageGdbServerLI(zipFile, cpuAbi, nativeFiles); } return PackageManager.INSTALL_SUCCEEDED;}
这段代码中的 Build.CPU_ABI 和 "ro.product.cpu.abi2" 分别为手机支持的主 abi 和次 abi 属性字符串,abi 为手机支持的指令集所代表的字符串,比如 armeabi-v7a、armeabi、x86、mips 等,而主 abi 和次 abi 分别表示手机支持的第一指令集和第二指令集。代码首先调用 listPackageSharedLibsForAbiLI 来遍历主 abi 目录。当主 abi 目录不存在时,才会接着调用 listPackageSharedLibsForAbiLI 遍历次 abi 目录。
private static int listPackageSharedLibsForAbiLI(ZipFile zipFile, String cpuAbi, List libEntries) throws IOException, ZipException { final int cpuAbiLen = cpuAbi.length(); boolean hasNativeLibraries = false; boolean installedNativeLibraries = false; if (DEBUG_NATIVE) { Slog.d(TAG, Checking + zipFile.getName() + for shared libraries of CPU ABI type + cpuAbi); } Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); // skip directories if (entry.isDirectory()) { continue; } String entryName = entry.getName(); /* * Check that the entry looks like lib//lib.so * here, but don't check the ABI just yet. * * - must be sufficiently long * - must end with LIB_SUFFIX, i.e. .so * - must start with APK_LIB, i.e. lib/ */ if (entryName.length()So策略的复制:
在遍历apk文件时,如果在apk的lib目录下的主abi子目录中存在so文件,则将主abi子目录下的所有so文件全部复制;只有当主abi子目录下没有so文件(即PACKAGE_INSTALL_NATIVE_ABI_MISMATCH情况)时,才会复制次abi子目录下的so文件。
战略难题:
当so文件放置不当时,安装apk可能导致拷贝不完整。例如,在apk的lib目录下存在三个so文件:armeabi/libx.so、armeabi/liby.so和armeabi-v7a/libx.so。在主ABI为armeabi-v7a且系统版本小于4.0的手机上,安装apk后按照拷贝策略只会拷贝主ABI目录下的文件,即armeabi-v7a/libx.so。这就导致加载liby.so时会报找不到so文件的异常。另外,如果主ABI目录不存在,则该策略会遍历两次apk,效率较低。
0x2 Android 4.0-Android 4.0.3
参考4.0.3的 Android 源码,同理,找到处理 so 拷贝的核心逻辑( native 层):
static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ScopedUtfChars filePath(env, javaFilePath); ScopedUtfChars cpuAbi(env, javaCpuAbi); ScopedUtfChars cpuAbi2(env, javaCpuAbi2); ZipFileRO zipFile; if (zipFile.open(filePath.c_str()) != NO_ERROR) { LOGI("Couldn't open APK %s\n", filePath.c_str()); return INSTALL_FAILED_INVALID_APK; } const int N = zipFile.getNumEntries(); char fileName[PATH_MAX]; for (int i = 0; i < N; i++) { const ZipEntryRO entry = zipFile.findEntryByIndex(i); if (entry == NULL) { continue; } // Make sure this entry has a filename. if (zipFile.getEntryFileName(entry, fileName, sizeof(fileName))) { continue; } // Make sure we're in the lib directory of the ZIP. if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) { continue; } // Make sure the filename is at least to the minimum library name size. const size_t fileNameLen = strlen(fileName); static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; if (fileNameLen < minLength) { continue; } const char* lastSlash = strrchr(fileName, '/'); LOG_ASSERT(lastSlash != NULL, "last slash was null somehow for %s\n", fileName); // Check to make sure the CPU ABI of this file is one we support. const char* cpuAbiOffset = fileName + APK_LIB_LEN; const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset; LOGV("Comparing ABIs %s and %s versus %s\n", cpuAbi.c_str(), cpuAbi2.c_str(), cpuAbiOffset); if (cpuAbi.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi.c_str(), cpuAbiRegionSize)) { LOGV("Using ABI %s\n", cpuAbi.c_str()); } else if (cpuAbi2.size() == cpuAbiRegionSize && *(cpuAbiOffset + cpuAbi2.size()) == '/' && !strncmp(cpuAbiOffset, cpuAbi2.c_str(), cpuAbiRegionSize)) { LOGV("Using ABI %s\n", cpuAbi2.c_str()); } else { LOGV("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) { LOGV("Failure for entry %s", lastSlash + 1); return ret; } } } return INSTALL_SUCCEEDED;}
So策略的复制:
对于每个 apk 文件,进行遍历。如果文件符合 so 文件的规则,并且属于主 ABI 目录或次 ABI 目录下的 so 文件,则将其解压并复制到相应目录中。
战略难题:
如果一个应用的 armeabi 和 armeabi-v7a 目录下都包含同名的 so 文件,就会发生覆盖现象。覆盖的先后顺序取决于 so 文件在 ZipFileR0 中对应的哈希值。举个例子来说明,假设一个 apk 同时包含 armeabi/libx.so 和 armeabi-v7a/libx.so 两个文件,并且安装到主 ABI 为 armeabi-v7a 的手机上。在拷贝 so 文件时,根据遍历顺序可能会出现这样一种情况:首先遍历并拷贝了 armeab-v7a/libx.so,然后再遍历并拷贝了 armeabi/libx.so,结果导致前者被后者覆盖。本来应该加载 armeabi-v7a 目录下的 so 文件,但按照这个策略却拷贝了 armeabi 目录下的 so 文件。
APK文件中的entry散列计算函数如下所示:
unsigned int ZipFileRO::computeHash(const char* str, int len) { unsigned int hash = 0; while (len--) hash = hash * 31 + *str++; return hash; } /* * Add a new entry to the hash table. */ void ZipFileRO::addToHash(const char* str, int strLen, unsigned int hash) { int ent = hash & (mHashTableSize-1); /* * We o */ }static install_status_t iterateOverNativeFiles(JNIEnv *env, jstring javaFilePath, jstring javaCpuAbi, jstring javaCpuAbi2, iterFunc callFunc, void* callArg) { ScopedUtfChars filePath(env, javaFilePath); ScopedUtfChars cpuAbi(env, javaCpuAbi); ScopedUtfChars cpuAbi2(env, javaCpuAbi2); ZipFileRO zipFile; if (zipFile.open(filePath.c_str()) != NO_ERROR) { ALOGI("Couldn't open APK %s\n", filePath.c_str()); return INSTALL_FAILED_INVALID_APK; } const int N = zipFile.getNumEntries(); char fileName[PATH_MAX]; bool hasPrimaryAbi = false; for (int i = 0; i < N; i++) { const ZipEntryRO entry = zipFile.findEntryByIndex(i); if (entry == NULL) { continue; } // Make sure this entry has a filename. if (zipFile.getEntryFileName(entry, fileName, sizeof(fileName))) { continue; } // Make sure we're in the lib directory of the ZIP. if (strncmp(fileName, APK_LIB, APK_LIB_LEN)) { continue; } // Make sure the filename is at least to the minimum library name size. const size_t fileNameLen = strlen(fileName); static const size_t minLength = APK_LIB_LEN + 2 + LIB_PREFIX_LEN + 1 + LIB_SUFFIX_LEN; if (fileNameLen < minLength) { continue; } const char* lastSlash = strrchr(fileName, '/'); ALOG_ASSERT(lastSlash != NULL, "last slash was null somehow for %s\n", fileName); // Check to make sure the CPU ABI of this file is one we support. const char* cpuAbiOffset = fileName + APK_LIB_LEN; const size_t cpuAbiRegionSize = lastSlash - cpuAbiOffset; ALOGV("Comparing ABIs %s and %s versus %s\n", cpuAbi.c_str(), cpuAbi2.c_str(), cpuAbiOffset); 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;}
So策略的复制:
在遍历apk文件时,如果发现包含主要Abi目录的so文件,将其复制并设置hasPrimaryAbi标记为true。随后,在后续的遍历中,只复制主要Abi目录下的so文件。当hasPrimaryAbi标记为false时,如果遍历到的so文件名包含当前abi字符串,则进行复制操作。
战略难题:
经过实际测试,发现当so文件放置不当时,在安装apk时可能会出现so拷贝不全的情况。为了解决4.0 ~ 4.0.3系统中so随意覆盖的问题,我们采取了以下策略:如果存在主abi目录下的so文件,则进行拷贝;如果主abi目录不存在该so文件,则拷贝次级abi目录下的对应so文件。然而,代码逻辑是根据ZipFileR0遍历顺序来决定是否进行拷贝操作。举例来说,假设存在这样一个apk包:在lib目录下有armeabi/libx.so、armeabi/liby.so和armeabi-v7a/libx.so这三个文件。
0x4 64位系统支持
以5.1.0系统为例,Android在5.0之后开始支持64位ABI。
public static int copyNativeBinariesWithOverride(Handle handle, File libraryRoot, String abiOverride) { try { if (handle.multiArch) { // Warn if an abiOverride is set for multi-lib packages. // For such packages, both 32-bit and 64-bit libraries need to be copied. if (abiOverride != null && !CLEAR_ABI_OVERRIDE.equals(abiOverride)) { Slog.w(TAG, Ignoring abiOverride for multi-arch application.); } int copyRet = PackageManager.NO_NATIVE_LIBRARIES; if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, Build.SUPPORTED_32_BIT_ABIS, true /* use isa specific subdirs */); if (copyRet 0) { copyRet = copyNativeBinariesForSupportedAbi(handle, libraryRoot, Build.SUPPORTED_64_BIT_ABIS, true /* use isa specific subdirs */); if (copyRetpublic static int copyNativeBinariesForSupportedAbi(Handle handle, File libraryRoot, String[] abiList, boolean useIsaSubdir) throws IOException { createNativeLibrarySubdir(libraryRoot); // Unpack the libraries if necessary for internal applications or when nativeLibraryPath points to app-lib directory int abi = findSupportedAbi(handle, abiList); if (abi >= 0) { // Construct a subdir under the native library root that corresponds to this instruction set final String instructionSet = VMRuntime.getInstructionSet(abiList[abi]); ... } }static int findSupportedAbi(JNIEnv *env, jlong apkHandle, jobjectArray supportedAbisArray) { const int numAbis = env->GetArrayLength(supportedAbisArray); Vector supportedAbis; for (int i = 0; i GetObjectArrayElement(supportedAbisArray, i))); } ZipFileRO* zipFile = reinterpret_cast(apkHandle); if (zipFile == NULL) { return INSTALL_FAILED_INVALID_APK; } UniquePtr it(NativeLibrariesIterator::create(zipFile)); if (it.get() == NULL) { return INSTALL_FAILED_INVALID_APK; } ZipEntryRO entry = NULL; char fileName[PATH_MAX]; 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
在处理32位so拷贝时,一旦findSupportedAbi索引返回值为0,就应该拷贝armeabi-v7a目录下的so文件;如果返回值为1,则应该拷贝armeabi目录下的so文件。
So策略的复制:
将32位和64位abi目录的so文件分别处理,根据遍历apk结果中符合abilist列表的所有so文件中最靠前的序号来决定所拷贝的abi目录,然后将该abi目录下的so文件进行拷贝。
战略难题:
根据策略,假设每个 abi 目录下都完整地放置了所有的 so 文件,这与2.3.6版本的处理逻辑相同。然而,仍然存在可能遗漏拷贝 so 文件的情况。
0x5 建议
我们提供了一些关于配置 so 的建议,以解决针对 Android 系统的拷贝策略问题。
- 1)针对 armeabi 和 armeabi-v7a 两种 ABI
方案1:由于 armeabi-v7a 指令集与 armeabi 指令集兼容,因此可以接受一些应用性能的损失。为了避免保留两份库的拷贝,可以删除 armeabi-v7a 目录及其下的库文件,只保留 armeabi 目录。例如,在 apk 中只有一个名为 armeabi 的 abi 时,可以考虑移除 lib 目录下的 armeabi-v7a 目录。
第二种方法:将so文件分别放置在armeabi和armeabi-v7a目录中。
- 2)针对x86
目前市面上的x86机型为了兼容arm指令,几乎都内置了libhoudini模块,这个模块提供了二进制转码支持。它的作用是将ARM指令转换为x86指令。因此,如果考虑apk包大小,并且可以接受一些性能损失的话,可以选择删除x86库目录。即使删除了x86下配置的armeabi目录中的so库,应用程序仍然可以正常加载和使用。
- 3)针对64位 ABI
如果应用程序开发者打算支持64位,那么必须将64位的so文件全部包含进去。否则,可以选择不单独编译64位的so文件,而是全部使用32位的so文件。在64位设备上,默认会加载32位的so文件。例如,如果apk中使用了第三方只有32位abi版本的so文件,可以考虑删除apk中lib目录下的64位abi子目录,以确保安装后能正常使用。
0x6 备注
这篇文章实际上是因为在Android的so加载过程中遇到了很多困难。我相信很多人都曾经遇到过UnsatisfiedLinkError错误,而且这个错误在不同的设备上表现也各不相同。但是你有没有想过,可能并不是apk逻辑的问题,而是由于Android系统在安装APK时PackageManager出现了问题,并没有正确地拷贝相应的SO文件呢?你可以参考下面第4个链接,作者提供了一种解决方案:当出现UnsatisfiedLinkError错误时,手动拷贝SO文件来解决。
参考链接: - Android 源码:https://android.googlesource.com/platform/frameworks/base - apk 安装过程及原理说明:http://blog.csdn.net/hdhd588/article/details/6739281 - 网易云加密:http://apk.aq.163.com/ - UnsatisfiedLinkError 的错误及解决方案:https://medium.com/keepsafe-engineering/the-perils-of-loading-native-libraries-on-android-befa49dce2db#.hell8vvdm 更多资讯文章,请关注微博公众号“网易云捕”。
还木有评论哦,快来抢沙发吧~