为什么一些安卓手机会导致我们的应用程序抛出java.lang.UnsatisfiedLinkError错误?

42

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

问题描述:

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

在其中一个System.loadLibrary()行中崩溃,并出现java.lang.UnsatisfiedLinkError错误。

java.lang.UnsatisfiedLinkError: 无法从加载器 dalvik.system.PathClassLoader[dexPath=/data/app/app_id-2.apk,libraryPath=/data/app-lib/app_id-2] 中找到 stlport_shared 库,findLibrary 返回 null

解决方案

我们开始对所有安装进行一些自定义诊断,以检查是否将每个lib都解压缩到/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 == 3并且subFilesLarger0 == true。我们想测试所有库文件是否被正确地解压缩并且大小大于0字节。此外,我们还查看freeSpace以查看可用的磁盘空间量。在屏幕底部的“设置”-->“应用程序”中可以找到与freeSpace相同的内存量。我们采用这种方法的思路是,当磁盘上没有足够的可用空间时,安装程序可能会在解压缩APK时出现问题。

真实场景

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

问题

是否有人遇到安装本地库文件的问题?有哪些成功的解决方法?有没有在库文件不存在时仅从互联网上下载本地库文件并手动存储的经验?问题最初可能是由什么引起的?

编辑

我现在还有一个用户,在应用程序更新后遇到了这个问题。旧版本在他的手机上运行良好,但更新后似乎缺少了本地库文件。手动复制库文件也似乎会出问题。他使用的是未root的Android 4.x手机,没有自定义ROM。

编辑2-解决方案

经过两年的研究和努力,我们找到了一个对我们很有效的解决方案。我们将其开源: https://github.com/KeepSafe/ReLinker


1
你知道它在哪些设备上无法工作吗?你是否只构建了 ARM 而没有为 x86 构建? - Chrispix
你可以尝试链接到gnustl_shared。我在使用stlport时遇到了几个设备特定的问题。请注意,GPL v3对此库有一个例外,并允许您链接专有软件。 - Andrew G
您不能使用随设备提供的stlport吗?可能某些设备需要进行特定更改才能使stlport正常工作。 - Chrispix
我们决定自己运送stlport,因为它只在NDK r9中添加,而且我们发现并不是每个手机制造商都适当地实现了它。 - philipp
只是一个更新。我们还没有能够解决那个问题。我们只是遇到了这些崩溃。 - philipp
显示剩余7条评论
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

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

如果真是这样,我怀疑NDK库代码中的某些强制性更改可能会解决问题,例如为每个发布更改一些未使用的虚拟变量。但是,应该以编译器保留该变量而不进行优化的方式进行操作。

编辑(2013年12月19日): 每次构建更改本机库代码的想法不幸地行不通。我尝试了其中一个应用程序,并从已经更新的客户那里得到了一个“不满足的链接错误”的崩溃报告。

不完整的APK安装

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

当因为设备存储空间不足,目录写入权限混乱等原因导致将本地库复制到目标目录失败时,安装程序仍然会返回“成功”,就好像本地库只是应用的“可选扩展”一样。

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

然而,我无法找到任何Android错误票证或原始博客文章,我已经搜索了很长时间。所以这个解释可能是神话和传说的领域。

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

此错误票证讨论 暗示已安装的本地库存在“并发问题”,请参见 Android错误票证#9089

如果存在一个只有一个本地库的“armeabi-v7a”目录,则该体系结构的整个目录优先于“armeabi”目录。

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

可能的解决方法

无论哪种情况:我在这里发现了一个有趣的答案,解决了类似问题。它的关键是将所有本地库打包为原始资源到您的APK中,并在第一次应用程序启动时复制适用于当前处理器架构的正确本地库到(应用程序私有)文件系统。使用完整文件路径和System.load来加载这些库。

然而,这个解决方法有一个缺陷:由于本地库将作为APK中的资源存在,因此Google Play将无法找到它们并为必需的处理器体系结构创建设备过滤器。可以通过将“虚拟”本地库放入所有目标体系结构的lib文件夹中来解决这个问题。

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

如果客户遇到这个问题,通常建议他们重新安装应用程序。但是,如果担心应用数据丢失,这不是一个好的解决方案。


这里有很棒的信息。我们正在针对armarmv7a进行构建。我们已经为两种架构准备了所有库。似乎解压问题与我们遇到的问题有关。这是在所有Android版本中都存在的。我知道一些人将他们的库打包到资产文件夹中,然后解压到app/files并使用System.load()从那里加载它们。我们已经嵌入了他们的东西,从未失败过。我会尝试一下。 - philipp
@philipp 我从另一个用户那里收到了类似的崩溃报告,我认为APK文件后缀为“-2.apk”非常可疑。这是在更新之后发生的。这可能只是一个暂时性的故障吗?你的用户是否报告说第二次启动应用程序可以正常工作?现在我正在尝试找到另一种解决方法,比如清除dex缓存。也许这也会在启动时重新安装本地库。 - tiguchi
我认为 -2.apk 只是临时 apk,用于下载,它基本上会与其他 apk 进行交换。我不确定(需要深入源代码进行验证)。 - Chrispix
@Chrispix 我相信这些增量更新只是 Google Play 的一个特性/错误,你在 Android 源代码中找不到任何相关的东西。但如果你无论如何找到了什么,请告诉我 :-) - tiguchi
@NobuGames - 我不相信-1.apk,-2.apk等后缀表示安装出现了问题。在测试和安装我的应用程序的调试版本时,我发现apk文件名通常会添加-1或-2,并且经常随每次安装而更改,但应用程序功能没有任何问题。必须有其他原因,最可能是由于相关分区的存储空间不足而导致本地库未安装。请查看我的回复以获取我发现的更简单的解决方法。 - gregko

14

我有同样的问题,而且在所有Android版本上都会出现UnsatisfiedLinkErrors错误-在过去的六个月中,针对一个目前拥有超过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")来加载这个本地库,那么就没有办法卸载它。当发现这样一个残留的、失效的本地库仍然停留在标准应用程序本地库文件夹中时,例如通过调用我们的某个本地方法并发现它不存在(再次出现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();
    }
}

格雷格


谢谢,我看到了,只是还没有机会尝试。看起来是个好主意。你在推特上吗?我相信有更多值得交流的问题。 - philipp
@philipp - 推特不是我最喜欢的沟通方式,我最擅长通过传统的电子邮件进行工作,请查看我的网站上的“联系人”页面,http://hyperionics.com。此外,我使用上述方法发布了我的应用程序版本,现在已经过去了3天,没有出现不满意的链接错误,但需要在更长时间内观察。 - gregko
在应用程序生命周期中,您在哪里调用initNativeLib函数? - user65721
这个位置并不重要,只要在尝试使用任何本机调用之前就可以了。如果解压缩和移动本地库的长时间操作开始启动,则最好不要在UI线程上进行。我实际上是在服务启动时调用它。 - gregko

0

Android在冰淇淋三明治或果冻豆上开始将其库打包在/data/app-lib/中,我记不清是哪个了。

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


你是否有更多关于库存储位置变更(hasAllNativeLibs)的信息? - philipp

0

如果您刚刚使用应用程序包发布,那么此问题可能与https://issuetracker.google.com/issues/127691101有关。

这种情况发生在LG或旧三星设备的某些设备上,用户已将应用程序移动到SD卡中。

解决此问题的一种方法是使用Relinker库来加载本地库,而不是直接调用System.load方法。对于我的应用程序使用情况,它确实起作用了。

https://github.com/KeepSafe/ReLinker

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

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


-2

在自己遇到这个问题并进行了一些研究(也就是谷歌搜索)后,我通过针对较低的SDK版本解决了这个问题。

我将compileSdkVersion设置为20(在4.4上工作正常)

还有

minSdkVersion 20 targetSdkVersion 20

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

希望它能帮助 - 它让我疯狂了几天。


有趣。我们的API级别长期以来一直是9,现在是15。问题仍然是我们最大的问题。我们最近一直在研究一些低级解决方案。一旦确认它有效,我们会进行更新。 - philipp
降低targetSdkVersion并不是一个解决方案。 - amitooshacham

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接