为什么有些Android手机导致我们的应用程序抛出java.lang.UnsatisfiedLinkError?

时间:2013-08-07 19:06:27

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

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

问题描述:

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

解决方法

我们开始在所有安装上运行一些自定义诊断程序,以检查每个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 == 3subFilesLarger0 == true的每部测试手机上。我们想测试是否所有的库都正确解包并且大于0字节。另外,我们正在查看freeSpace以查看可用磁盘空间的大小。 freeSpace与您在“设置”中找到的内存量相匹配 - &gt;屏幕底部的应用程序。这种方法背后的想法是,当磁盘空间不足时,安装程​​序可能无法解压缩APK。

真实世界情景

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

问题

任何人在安装本机库时遇到问题?什么工作成功了?有没有经验只是在不存在时通过互联网下载本机库并手动存储它们?什么可能导致问题首先?

修改

我现在还有一个用户在应用程序更新后遇到此问题。之前的版本在他的手机上运行良好,经过更新后,原生库似乎缺失了。手动复制库似乎也会带来麻烦。他在android 4.x上使用没有自定义ROM的非根电话。

编辑2 - 解决方案

经过2年的时间花在这个问题上。我们想出了一个适合我们的解决方案。我们开源:https://github.com/KeepSafe/ReLinker

5 个答案:

答案 0 :(得分:19)

编辑:自从昨天我的一个应用程序收到了另一份崩溃报告后,我深入研究了这个问题并找到了第三个非常可能的解释:

Google Play部分APK更新错误

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

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

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

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

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

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

编辑(12/19/13):遗憾的是,为每个版本更改本机库代码的想法不起作用。我用我的一个应用程序尝试了它,并得到了一个来自更新的客户的“不满意的链接错误”崩溃报告。

不完整的APK安装

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

当将本机库复制到目标目录时,无论出于何种原因(设备耗尽存储空间,也可能搞乱目录写入权限......),安装程序仍会返回“成功”,就像本机库只是“可选”一样扩展“到应用程序。

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

然而,我找不到任何Android错误票,也找不到原始的博客文章,我搜索了很多。因此,这种解释可能属于神话和传说的范畴。

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

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

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

如果您尝试加载“armeabi”中存在的库,您将收到UnsatisfiedLinkException。顺便说一下,这个bug被标记为“按预期工作”。

可能的解决方法

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

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

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

通常有助于告诉客户该问题重新安装应用程序。遗憾的是,如果关注应用数据丢失,这不是一个好的解决方案。

答案 1 :(得分:13)

我遇到同样的问题,所有版本的Android都出现了UnsatisfiedLinkErrors - 在过去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;

这会返回类似&#34; /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日发布我的产品版本时,代码类似于下面列出的代码,我的本机库没有任何不满意的链接错误。< / p>

以下是我使用的代码:

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(&#34; MyNativeLibName&#34;),没有办法卸载它。 在找到标准应用程序本机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();
    }
}

格雷格

答案 2 :(得分:0)

Android开始将他们的库打包在/ data / app-lib // @ Ice Cream Sandwich或Jellybean附近,我记不起哪个了。

您是否正在建造和分发“arm”和“armv7a”?我最好的猜测是你只为一个架构而构建,而另一个架构正在测试。

答案 3 :(得分:0)

如果您只是通过 app bundle 发布,这个问题可能与 https://issuetracker.google.com/issues/127691101

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

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

https://github.com/KeepSafe/ReLinker

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

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

答案 4 :(得分:-2)

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

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

以及

minSdkVersion 20   targetSdkVersion 20

一旦我将它们更改为18(我有能力做到这一点没有任何更大的影响)我能够在棒棒糖上运行应用程序没有问题。

希望它有所帮助 - 这让我坚持了好几天。