解决 Android 系统在安装 apk 时出现的 so 解压和安装逻辑问题(安卓系统出现问题怎么解决)(手机的android系统)

访客 115 0

引用自: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 (copyRet  
public 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 

解决 Android 系统在安装 apk 时出现的 so 解压和安装逻辑问题(安卓系统出现问题怎么解决)(手机的android系统)-第1张图片-谷歌商店上架

在处理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. 1)针对 armeabi 和 armeabi-v7a 两种 ABI

    方案1:由于 armeabi-v7a 指令集与 armeabi 指令集兼容,因此可以接受一些应用性能的损失。为了避免保留两份库的拷贝,可以删除 armeabi-v7a 目录及其下的库文件,只保留 armeabi 目录。例如,在 apk 中只有一个名为 armeabi 的 abi 时,可以考虑移除 lib 目录下的 armeabi-v7a 目录。

    第二种方法:将so文件分别放置在armeabi和armeabi-v7a目录中。

  2. 2)针对x86

    目前市面上的x86机型为了兼容arm指令,几乎都内置了libhoudini模块,这个模块提供了二进制转码支持。它的作用是将ARM指令转换为x86指令。因此,如果考虑apk包大小,并且可以接受一些性能损失的话,可以选择删除x86库目录。即使删除了x86下配置的armeabi目录中的so库,应用程序仍然可以正常加载和使用。

  3. 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 更多资讯文章,请关注微博公众号“网易云捕”。

标签: 文件 谷歌商店上架 目录

发表评论 (已有0条评论)

还木有评论哦,快来抢沙发吧~