42

我们java.lang.UnsatisfiedLinkError在市场上使用我们的应用程序的一些 Android 手机上遇到了问题。

问题描述:

static
{
    System.loadLibrary("stlport_shared"); // C++ STL        
    System.loadLibrary("lib2"); 
    System.loadLibrary("lib3"); 
}

使应用程序在System.loadLibrary()带有java.lang.UnsatisfiedLinkError. java.lang.UnsatisfiedLinkError: Couldn't load stlport_shared from loader dalvik.system.PathClassLoader[dexPath=/data/app/app_id-2.apk,libraryPath=/data/app-lib/app_id-2]: findLibrary returned null

解决方法

我们开始对所有安装运行一些自定义诊断,以检查/data/data/app_id/lib文件夹中是否已解压每个库。

PackageManager m = context.getPackageManager();
String s = context.getPackageName();
PackageInfo p;
p = m.getPackageInfo(s, 0);
s = p.applicationInfo.dataDir;

File appDir = new File(s);
long freeSpace = appDir.getFreeSpace();

File[] appDirList = appDir.listFiles();
int numberOfLibFiles = 0;
boolean subFilesLarger0 = true;
for (int i = 0; i < appDirList.length; i++) {

    if(appDirList[i].getName().startsWith("lib")) {
        File[] subFile = appDirList[i].listFiles(FileFilters.FilterDirs);   
        numberOfLibFiles = subFile.length;
        for (int j = 0; j < subFile.length; j++) {
            if(subFile[j].length() <= 0) {
                subFilesLarger0 = false;
                break;
            }
        }
    }
}

在我们拥有的每部测试手机上numberOfLibFiles == 3subFilesLarger0 == true. 我们想测试是否所有库都正确解包并且大于 0 字节。此外,我们正在查看有freeSpace多少可用磁盘空间。freeSpace与您可以在屏幕底部的设置 --> 应用程序中找到的内存量相匹配。这种方法背后的想法是,当磁盘上没有足够的可用空间时,安装程​​序可能会在解压 APK 时遇到问题。

真实世界场景

查看诊断信息,一些设备在文件夹中没有全部 3 个库,/data/data/app_id/lib但有足够的可用空间。我想知道为什么错误消息正在寻找/data/app-lib/app_id-2. 我们所有的手机都将它们的库存储在/data/data/app_id/lib. 此外,System.loadLibrary()应该在安装和加载库时使用一致的路径吗?我怎么知道操作系统在哪里寻找库?

问题

有人在安装本机库时遇到问题吗?哪些变通方法取得了成功?有没有在不存在时通过互联网下载本机库并手动存储它们的经验?首先可能导致问题的原因是什么?

编辑

我现在也有一个用户在应用程序更新后遇到了这个问题。以前的版本在他的手机上运行良好,更新后本地库似乎丢失了。手动复制库似乎也会造成麻烦。他在 android 4.x 上使用没有自定义 ROM 的非 root 手机。

编辑 2 - 解决方案

在这个问题上花费了 2 年时间。我们想出了一个对我们现在很有效的解决方案。我们开源了它:https ://github.com/KeepSafe/ReLinker

4

5 回答 5

20

编辑:由于我昨天收到了我的一个应用程序的另一份崩溃报告,因此我对此事进行了更深入的研究,并找到了该问题的第三种很可能的解释:

Google Play 部分 APK 更新出错

老实说,我不知道这个功能。APK 文件名后缀“-2.apk”让我怀疑。这个问题的崩溃消息中提到了这里,我也可以在我的那个客户的崩溃报告中找到这个后缀。

我相信“-2.apk”暗示部分更新可能会为 Android 设备提供更小的增量。该增量显然不包含本机库,因为它们自上一个版本以来没有更改。

无论出于何种原因,System.loadLibrary 函数都会尝试从部分更新(不存在的地方)中查找本机库。这既是 Android 的缺陷,也是 Google Play 的缺陷。

这是一个非常相关的错误报告,其中包含关于类似观察的有趣讨论:https ://code.google.com/p/android/issues/detail?id=35962

看起来 Jelly Bean 在本地库安装和加载方面可能存在缺陷(进来的两个崩溃报告都是 Jelly Bean 设备)。

如果真的是这种情况,我怀疑 NDK 库代码中的一些强制更改可能会解决问题,例如为每个版本更改一些未使用的虚拟变量。但是,这应该以编译器保留该变​​量而不优化它的方式来完成。

编辑(13 年 12 月 19 日):不幸的是,为每个构建更改本机库代码的想法不起作用。我用我的一个应用程序进行了尝试,并从无论如何更新的客户那里得到了“不满意的链接错误”崩溃报告。

APK安装不完整

不幸的是,这只是我的记忆,我再也找不到链接了。去年我读了一篇关于那个不满意的链接问题的博客文章。作者说这是APK安装例程中的一个bug。

当出于某种原因将本机库复制到其目标目录失败时(设备存储空间不足,也可能是目录写入权限混乱......)安装程序仍然返回“成功”,就好像本机库只是“可选扩展”一样一个应用程序。

在这种情况下,唯一的解决方法是重新安装 APK,同时确保应用程序有足够的存储空间。

但是,我再也找不到任何 Android 错误票或原始博客文章,我搜索了很多。所以这个解释可能属于神话传说的范畴。

“armeabi-v7a”目录优先于“armeabi”目录

此错误票证讨论暗示了已安装本机库的“并发问题”,请参阅Android 错误票证 #9089

如果“armeabi-v7a”目录中只有一个本地库,则该架构的整个目录优先于“armeabi”目录。

如果您尝试加载仅存在于“armeabi”中的库,您将收到 UnsatisfiedLinkException。顺便说一句,该错误已被标记为“按预期工作”。

可能的解决方法

无论哪种情况:我在 SO 上找到了一个类似问题的有趣答案。这一切都归结为将所有本机库作为原始资源打包到您的 APK 中,并在第一个应用程序启动时将当前处理器架构的正确库复制到(应用程序私有)文件系统。使用带有完整文件路径的 System.load 来加载这些库。

然而,这种解决方法有一个缺陷:由于本机库将作为资源驻留在 APK 中,Google Play 将无法再找到它们并为强制性处理器架构创建设备过滤器。可以通过将“虚拟”本机库放入所有目标架构的 lib 文件夹中来解决此问题。

总的来说,我相信这个问题应该正确地传达给谷歌。似乎 Jelly Bean 和 Google Play 都存在缺陷。

告诉有该问题的客户重新安装应用程序通常会有所帮助。不幸的是,如果担心应用程序数据丢失,这不是一个好的解决方案。

于 2013-08-08T15:35:26.897 回答
14

我也遇到了同样的问题,并且 UnsatisfiedLinkErrors 出现在所有版本的 Android 上 - 在过去的 6 个月中,对于当前拥有超过 90000 次活动安装的应用程序,我有:

Android 4.2     36  57.1%
Android 4.1     11  17.5%
Android 4.3     8   12.7%
Android 2.3.x   6   9.5%
Android 4.4     1   1.6%
Android 4.0.x   1   1.6%

并且用户报告说它通常发生在应用程序更新之后。这是针对每天获得大约 200 到 500 个新用户的应用程序。

我想我想出了一个更简单的解决方法。我可以通过这个简单的调用找出我的应用程序的原始 apk 在哪里:

    String apkFileName = context.getApplicationInfo().sourceDir;

这将返回类似于“/data/app/com.example.pkgname-3.apk”的内容,即我的应用程序 APK 文件的确切文件名。该文件是一个普通的 ZIP 文件,无需 root 即可读取。因此,如果我捕获 java.lang.UnsatisfiedLinkError,我可以从 .apk (zip) lib/armeabi-v7a 文件夹(或我所在的任何架构)内部提取并复制我的本机库,并将其复制到任何目录我可以读/写/执行,并使用 System.load(full_path) 加载它。

编辑:它似乎工作

自 2014 年 6 月 23 日发布我的产品版本以来,2014 年 7 月 1 日更新,代码类似于下面列出的代码,我的本地库中没有任何不满意的链接错误。

这是我使用的代码:

public static void initNativeLib(Context context) {
    try {
        // Try loading our native lib, see if it works...
        System.loadLibrary("MyNativeLibName");
    } catch (UnsatisfiedLinkError er) {
        ApplicationInfo appInfo = context.getApplicationInfo();
        String libName = "libMyNativeLibName.so";
        String destPath = context.getFilesDir().toString();
        try {
            String soName = destPath + File.separator + libName;
            new File(soName).delete();
            UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
            System.load(soName);
        } catch (IOException e) {
            // extractFile to app files dir did not work. Not enough space? Try elsewhere...
            destPath = context.getExternalCacheDir().toString();
            // Note: location on external memory is not secure, everyone can read/write it...
            // However we extract from a "secure" place (our apk) and instantly load it,
            // on each start of the app, this should make it safer.
            String soName = destPath + File.separator + libName;
            new File(soName).delete(); // this copy could be old, or altered by an attack
            try {
                UnzipUtil.extractFile(appInfo.sourceDir, "lib/" + Build.CPU_ABI + "/" + libName, destPath);
                System.load(soName);
            } catch (IOException e2) {
                Log.e(TAG "Exception in InstallInfo.init(): " + e);
                e.printStackTrace();
            }
        }
    }
}

不幸的是,如果一个糟糕的应用程序更新留下了旧版本的原生库,或者我们使用 System.loadLibrary("MyNativeLibName") 加载的副本以某种方式损坏,则无法卸载它。在发现标准应用程序本机 lib 文件夹中残留的已失效库时,例如通过调用我们的本机方法之一并发现它不存在(再次为 UnsatisfiedLinkError),我们可以存储首选项以避免调用标准 System.loadLibrary( ) 完全依赖于我们自己的提取和加载代码在下一次应用程序启动时。

为了完整起见,这里是 UnzipUtil 类,我从这篇CodeJava UnzipUtility 文章中复制并修改了它:

import java.io.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

public class UnzipUtil {
    /**
     * Size of the buffer to read/write data
     */

    private static final int BUFFER_SIZE = 4096;
    /**
     * Extracts a zip file specified by the zipFilePath to a directory specified by
     * destDirectory (will be created if does not exists)
     * @param zipFilePath
     * @param destDirectory
     * @throws java.io.IOException
     */
    public static void unzip(String zipFilePath, String destDirectory) throws IOException {
        File destDir = new File(destDirectory);
        if (!destDir.exists()) {
            destDir.mkdir();
        }
        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
        ZipEntry entry = zipIn.getNextEntry();
        // iterates over entries in the zip file
        while (entry != null) {
            String filePath = destDirectory + File.separator + entry.getName();
            if (!entry.isDirectory()) {
                // if the entry is a file, extracts it
                extractFile(zipIn, filePath);
            } else {
                // if the entry is a directory, make the directory
                File dir = new File(filePath);
                dir.mkdir();
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
        zipIn.close();
    }

    /**
     * Extracts a file from a zip to specified destination directory.
     * The path of the file inside the zip is discarded, the file is
     * copied directly to the destDirectory.
     * @param zipFilePath - path and file name of a zip file
     * @param inZipFilePath - path and file name inside the zip
     * @param destDirectory - directory to which the file from zip should be extracted, the path part is discarded.
     * @throws java.io.IOException
     */
    public static void extractFile(String zipFilePath, String inZipFilePath, String destDirectory) throws IOException  {
        ZipInputStream zipIn = new ZipInputStream(new FileInputStream(zipFilePath));
        ZipEntry entry = zipIn.getNextEntry();
        // iterates over entries in the zip file
        while (entry != null) {
            if (!entry.isDirectory() && inZipFilePath.equals(entry.getName())) {
                String filePath = entry.getName();
                int separatorIndex = filePath.lastIndexOf(File.separator);
                if (separatorIndex > -1)
                    filePath = filePath.substring(separatorIndex + 1, filePath.length());
                filePath = destDirectory + File.separator + filePath;
                extractFile(zipIn, filePath);
                break;
            }
            zipIn.closeEntry();
            entry = zipIn.getNextEntry();
        }
        zipIn.close();
    }

    /**
     * Extracts a zip entry (file entry)
     * @param zipIn
     * @param filePath
     * @throws java.io.IOException
     */
    private static void extractFile(ZipInputStream zipIn, String filePath) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
        byte[] bytesIn = new byte[BUFFER_SIZE];
        int read = 0;
        while ((read = zipIn.read(bytesIn)) != -1) {
            bos.write(bytesIn, 0, read);
        }
        bos.close();
    }
}

格雷格

于 2014-06-18T23:26:01.197 回答
0

Android 开始在 Ice Cream Sandwich 或 Jellybean 的某个时候将他们的库打包在 /data/app-lib// 中,我不记得是哪个了。

您是否正在为“arm”和“armv7a”构建和分发?我最好的猜测是您只为其中一种架构构建,而您正在对另一种架构进行测试。

于 2013-08-08T15:50:56.913 回答
0

如果您只是使用 app bundle 发布,此问题可能与https://issuetracker.google.com/issues/127691101有关

它发生在用户已将应用程序移至 SD 卡的 LG 或旧三星设备的某些设备上。

解决此问题的一种方法是使用 Relinker 库来加载您的本机库,而不是直接调用 System.load 方法。它确实适用于我的应用程序的用例。

https://github.com/KeepSafe/ReLinker

另一种方法是阻止应用程序移动到 SD 卡。

您还可以在 gradle.properties 文件中保留 android.bundle.enableUncompressedNativeLibs=false 。但它会增加 Play Store 上的应用程序下载大小及其磁盘大小。

于 2021-06-28T20:01:06.383 回答
-2

在我自己遇到这个问题并做了一些研究(又名谷歌搜索)之后,我能够通过定位较低的 SDK 来解决这个问题。

我将 compileSdkVersion 设置为 20(适用于 4.4)

并且

minSdkVersion 20 目标SdkVersion 20

一旦我将它们更改为 18(我可以在没有任何更大影响的情况下这样做),我就能够在 Lollipop 上毫无问题地运行该应用程序。

希望它有所帮助 - 它让我发疯了好几天。

于 2015-07-29T09:30:20.483 回答