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

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

问题描述:

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

使用java.lang.UnsatisfiedLinkErrorSystem.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

解决scheme

我们开始在所有安装上运行一些自定义诊断程序,以检查每个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; } } } } 

在每个testing手机上,我们有numberOfLibFiles == 3subFilesLarger0 == true 。 我们想testing所有的库是否正确解压缩,然后大于0字节。 另外,我们正在研究freeSpace以查看有多less磁盘空间可用。 freeSpace与您可以在屏幕底部的设置 – >应用程序中find的内存量相匹配。 这种方法背后的想法是,当磁盘空间不足时,安装程​​序可能在解压缩APK时遇到问题。

真实世界的场景

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

任何遇到安装本地库的问题? 什么解决方法已经成功了? 任何经验,只要通过互联网下载本地库,当他们不存在和手动存储他们? 有什么可能导致这个问题呢?

编辑

我现在也有一个用户在应用程序更新后遇到此问题。 以前的版本在他的手机上工作得很好,在更新之后本地库似乎不见了。 手动复制库似乎也会造成麻烦。 他在Android 4.x上没有固定电话,没有自定义ROM。

编辑2 – 解决scheme

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

我也遇到了同样的麻烦,在所有版本的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个新用户。

我想我想出了一个更简单的解决办法 。 我可以通过这个简单的调用find我的应用程序的原始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)。

编辑:这似乎工作

20147月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”)加载,则无法卸载它。 一旦find了标准的应用程序本地lib文件夹中残留的这种残余库,例如通过调用我们的本地方法之一并找出它不存在(再次UnsatisfiedLinkError),我们可以存储一个首选项,以避免调用标准的System.loadLibrary )完全依靠我们自己的提取和加载代码在下一个应用程序的初创公司。

为了完整起见,下面是从这个CodeJava UnzipUtility文章中复制并修改的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(); } } 

格雷格

编辑:因为我昨天得到了另一个崩溃报告为我的应用程序之一,我挖了一点这个问题更深入,发现第三个很可能的解释为这个问题:

Google Play部分APK更新出错

说实话,我不知道这个function。 APK文件名后缀“-2.apk”让我感到可疑。 在这个问题的崩溃消息中提到,我也可以在我的客户的崩溃报告中find后缀。

我相信“-2.apk”提示部分更新可能会为Android设备带来更小的增量。 从上一个版本开始,这个delta显然不包含本地库。

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

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

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

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

编辑(12月19日):不幸的是改变每个版本的本地库代码的想法不起作用。 我试着用我的一个应用程序,并得到一个“不满意的链接错误”崩溃报告,从一个客户谁更新。

不完整的APK安装

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

当将本地库复制到其目标目录时,出于任何原因(设备耗尽存储空间,也许还搞乱了目录写入权限…),失败时,安装程​​序仍会返回“成功”,就好像本地库只是“可选扩展名”一个应用程序

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

不过,我找不到任何Android bug bug,也没有find原来的博客文章,我search了很多。 所以这个解释可能是在神话和传说的领域。

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

这个错误票证的讨论暗示了已安装的本地库的“并发问题”,请参阅Android bug代码#9089 。

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

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

可能的解决方法

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

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

总的来说,我确实认为这个问题应该正确地传达给Google。 这似乎果冻豆和谷歌播放有缺陷。

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

Android开始将它们的库打包到/ data / app-lib中//有时在Ice Cream Sandwich或者Jellybean周围,我不记得是哪一个。

你是build立和分配“arm”和“armv7a”吗? 我最好的猜测是,你只为其中一个架构而build,而你正在testing另一个架构。

自己有这个问题并做了一些研究(又名Googlesearch)之后,我可以通过定位一个较低的SDK来解决这个问题。

我把compileSdkVersion设置为20(在4.4上工作)

并且

minSdkVersion 20 targetSdkVersion 20

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

希望它有帮助 – 这让我疯狂了好几天。

Interesting Posts