【问题标题】:Why do some Android phones cause our app to throw an java.lang.UnsatisfiedLinkError?为什么有些 Android 手机会导致我们的应用抛出 java.lang.UnsatisfiedLinkError?
【发布时间】:2013-08-09 07:44:37
【问题描述】:

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

问题描述:

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

使用java.lang.UnsatisfiedLinkError 使应用程序在System.loadLibrary() 行中崩溃。 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 时遇到问题。

真实世界场景

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

问题

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

编辑

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

编辑 2 - 解决方案

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

【问题讨论】:

  • 您是否知道它无法在哪些设备上运行?是否有可能您只构建 arm,而不是 x86?
  • 是的,我们知道这些设备。有一些三星 Galaxy SII 和 LG 手机。所有崩溃的手机都是ARM手机。我们不是为 x86 构建的。
  • 您的意思是系统映像中的“stlport_shared”还是您直接与您的应用程序捆绑的东西?系统提供的只是“stlport”,尽管您也可以使用 stlport_static 编译/链接。此外,故障设备是哪些操作系统版本(Gingerbread 或更新的版本)?
  • 我们将 stl_port 与我们自己的应用一起发布。该问题出现在所有 android 版本中。
  • 您可以尝试链接到 gnustl_shared。我有几个与 stlport 相关的设备特定问题。请注意,GPL v3 对此库有一个例外,允许您链接专有软件。

标签: android android-ndk java-native-interface native unsatisfiedlinkerror


【解决方案1】:

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

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 库代码中的某些强制更改可能会解决问题,例如为每个版本更改一些未使用的虚拟变量。但是,这应该以编译器保留该变​​量而不优化它的方式来完成。

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

APK 安装不完整

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

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

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

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

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

This bug ticket discussion 提示安装的本机库存在“并发问题”,请参阅Android bug ticket #9089

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

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

可能的解决方法

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

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

总的来说,我认为应该将这个问题正确地传达给 Google。 Jelly Bean 和 Google Play 似乎都有缺陷。

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

【讨论】:

  • 这里有很棒的信息。我们正在反对armarmv7a。我们拥有两种架构的所有库。拆包问题似乎对我们遇到的问题有意义。它适用于所有安卓版本。我知道有些人将他们的库放在资产文件夹中并将其解压缩到应用程序/文件并使用System.load() 从那里加载它我们嵌入了他们的东西并且它从未失败过。我会试试看。
  • @philipp 我从用户那里得到了另一个这样的崩溃报告,我认为 APK 文件后缀为“-2.apk”是非常可疑的。它发生在更新之后。会不会只是暂时的故障?您的用户是否报告您的应用在第二次启动时再次运行?现在我正试图找出另一种解决方法,比如清除 dex 缓存。也许这也会在启动时重新安装本机库。
  • 我认为 -2.apk 只是下载时的临时 apk,它基本上与其他 apk 进行交换。我不确定(需要深入研究源代码才能验证)。
  • @Chrispix 我相信这些增量更新只是 Google Play 的一个功能/错误,您将无法在 Android 源代码中找到任何相关内容。但如果你找到了一些东西,请告诉我:-)
  • @NobuGames - 我不相信 -1.apk、-2.apk 等后缀表明安装出现问题。在测试和安装我的应用程序的调试版本时,我看到 -1 或 -2 被常规地添加到 apk 文件名中,并且经常随着每次安装而改变,并且应用程序运行没有任何问题。必须有其他东西,很可能由于相关分区上的存储空间不足而未安装本机库。请参阅我的回复以了解我发现的更简单的解决方法。
【解决方案2】:

我也遇到了同样的问题,并且 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 年 7 月 1 日更新自 2014 年 6 月 23 日发布我的产品版本以来,我的本地库中没有任何不满意的链接错误。

这是我使用的代码:

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() 并在下一次应用启动时依赖我们自己的提取和加载代码。

为了完整起见,这里是我从CodeJava UnzipUtility article 复制和修改的 UnzipUtil 类:

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();
    }
}

格雷格

【讨论】:

  • 谢谢,我看到了,只是还没有机会尝试。看起来是个好主意。你在推特上吗?相信还有更多值得交流的问题。
  • @philipp - Twitter 不是我最喜欢的交流方式,我通过传统电子邮件工作得最好,请参阅我网站上的“联系人”页面,hyperionics.com。另外,我使用上述方法发布了我的应用程序版本,现在 3 天没有不满意的链接错误,但会在更长的时间内看到。
  • 在应用程序生命周期中,您在哪里调用 initNativeLib?
  • 这真的无关紧要,只要在您实际尝试使用任何本机调用之前。最好不要在 UI 线程上,如果解压缩和移动本机库的长时间操作开始了。我实际上是在我的服务启动时调用它。
【解决方案3】:

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

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

【讨论】:

  • 关于图书馆存储位置变更的更多信息吗(hasAllNativeLibs)
【解决方案4】:

如果您只是使用 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 上的应用下载大小以及磁盘大小。

【讨论】:

    【解决方案5】:

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

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

    还有

    minSdkVersion 20 targetSdkVersion 20

    一旦我将它们更改为 18(我能够这样做而没有任何更大的影响),我就能够在 Lollipop 上运行该应用程序而不会出现问题。

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

    【讨论】:

    • 有趣。很长一段时间以来,我们的最低 API 级别为 9,现在是 15。问题仍然是我们最大的问题。我们最近一直在研究一些较低级别的解决方案。确认有效后将更新
    • 降低 targetSdkVersion 不是解决方案。
    猜你喜欢
    • 2021-10-06
    • 2022-12-10
    • 1970-01-01
    • 1970-01-01
    • 2014-04-22
    • 1970-01-01
    • 1970-01-01
    • 2011-03-28
    • 1970-01-01
    相关资源
    最近更新 更多