Почему на некоторых телефонах Android наше приложение выдает ошибку java.lang.UnsatisfiedLinkError?

Мы наблюдаем 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 == 3 и subFilesLarger0 == 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 с нерутированным телефоном без пользовательского ПЗУ.

РЕДАКТИРОВАНИЕ 2 – Решение

После 2 лет траты времени на эту проблему. Мы нашли решение, которое хорошо работает для нас сейчас. У нас открытый исходный код: https://github.com/KeepSafe/ReLinker.


person philipp    schedule 07.08.2013    source источник
comment
Вы случайно не знаете, на каких устройствах он не работает? Возможно ли, что вы собираете только арм, а не для x86?   -  person Chrispix    schedule 07.08.2013
comment
да, мы знаем устройства. Есть телефоны Samsung Galaxy SII и LG. Все телефоны, которые дают сбой, являются телефонами ARM. Мы не собираем для x86.   -  person philipp    schedule 08.08.2013
comment
Вы имеете в виду stlport_shared из образа системы или это то, что вы связываете непосредственно с вашим приложением? Предоставленный системой - это просто stlport, хотя вы также можете скомпилировать/связать с stlport_static. Кроме того, какие версии ОС являются неисправными устройствами (Gingerbread или что-то более новое)?   -  person Stephen Hines    schedule 08.08.2013
comment
Мы отправляем stl_port вместе с приложением. Проблема проявляется во всех версиях Android.   -  person philipp    schedule 08.08.2013
comment
Вместо этого вы можете попробовать создать ссылку на gnustl_shared. У меня было несколько специфичных для устройства проблем с stlport. Обратите внимание, что GPL v3 имеет исключение для этой библиотеки и позволяет вам связывать проприетарное программное обеспечение.   -  person Andrew G    schedule 08.08.2013
comment
Связанный? code.google.com/p/android/issues/detail?id= 35962   -  person auselen    schedule 08.08.2013
comment
Это также может быть связано с: groups.google.com/forum/# !topic/android-ndk/X1mRZwBxZLc   -  person philipp    schedule 15.08.2013
comment
Нельзя ли использовать sltport, который поставляется с устройством? Возможно, для корректной работы stlport необходимо внести определенные изменения в некоторые устройства.   -  person Chrispix    schedule 24.08.2013
comment
Мы решили поставить stlport самостоятельно, так как он был добавлен только в NDK r9, и мы обнаружили, что не каждый производитель мобильных телефонов реализует его должным образом.   -  person philipp    schedule 05.09.2013
comment
Только как обновление. Мы не смогли решить эту проблему. У нас как раз такие аварии.   -  person philipp    schedule 18.03.2014
comment
@philipp - см. мой ответ ниже, предлагающий распаковать исходный apk вручную, чтобы извлечь правильную общую библиотеку. У меня работает во всех тестах, которые я смог выполнить, и только сегодня загрузил сборку с этим решением в Google Play. Посмотрю, буду ли я по-прежнему получать UnsatisfiedLinkErrors для моей родной библиотеки после этого обновления.   -  person gregko    schedule 24.06.2014
comment
@gregko посмотри мое редактирование. Мы решили проблему наконец.   -  person philipp    schedule 11.11.2015


Ответы (5)


У меня та же проблема, и ошибка 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-файлом и доступен для чтения без рута. Поэтому, если я поймаю java.lang.UnsatisfiedLinkError, я могу извлечь и скопировать свою родную библиотеку из папки .apk (zip) lib/armeabi-v7a (или любой другой архитектуры, на которой я работаю) в любой каталог, где Я могу читать/записывать/выполнять и загружать его с помощью System.load(full_path).

Изменить: кажется, работает

Обновление от 1 июля 2014 г. с момента выпуска версии моего продукта с кодом, аналогичным приведенному ниже, 23 июня 2014 г. не было ошибок неудовлетворенных ссылок из моей родной библиотеки.

Вот код, который я использовал:

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

Грег

person gregko    schedule 18.06.2014
comment
спасибо, я это видел, просто не было возможности попробовать. Похоже, отличная идея. Ты есть в твиттере или около того? Я уверен, что есть еще проблемы, которые стоит обменять. - person philipp; 25.06.2014
comment
@philipp — Твиттер — не мой любимый способ общения, лучше всего я работаю по традиционной электронной почте, см. страницу «Контакты» на моем веб-сайте, hyperionics.com . Кроме того, я выпустил свою версию приложения, используя вышеуказанный метод, уже 3 дня нет ошибок неудовлетворенных ссылок, но в более длительном пробеге будет видно. - person gregko; 25.06.2014
comment
где вы вызываете initNativeLib в жизненном цикле приложения? - person user65721; 09.01.2015
comment
Это действительно не имеет значения, где, при условии, что это происходит до того, как вы на самом деле попытаетесь использовать любой из собственных вызовов. Предпочтительно, чтобы это не было в потоке пользовательского интерфейса, если срабатывает длительная операция распаковки и перемещения нативной библиотеки. Я фактически вызываю ее, когда запускается моя служба. - person gregko; 10.01.2015

РЕДАКТИРОВАТЬ: Поскольку вчера я получил еще один отчет о сбое для одного из моих приложений, я немного углубился в этот вопрос и нашел третье очень вероятное объяснение этой проблемы:

Не удалось обновить частичный APK Google Play

Честно говоря, я не знал об этой функции. Суффикс имени файла 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 могут решить проблему, например, изменение некоторых неиспользуемых фиктивных переменных для каждого выпуска. Однако это должно быть сделано таким образом, чтобы компилятор сохранил эту переменную, не оптимизируя ее.

EDIT (19.12.13): Идея изменения кода собственной библиотеки для каждой сборки, к сожалению, не работает. Я попробовал это с одним из своих приложений и получил отчет о сбое «неудовлетворенная ошибка ссылки» от клиента, который все равно обновился.

Неполная установка APK

К сожалению, это просто из моей памяти, и я больше не могу найти ссылку. В прошлом году я прочитал статью в блоге о проблеме с неудовлетворенными ссылками. Автор сказал, что это ошибка в процедуре установки APK.

Когда копирование собственных библиотек в их целевой каталог завершается сбоем по какой-либо причине (на устройстве закончилось место для хранения, возможно, также испорчены права на запись в каталог...), установщик по-прежнему возвращает «успех», как если бы собственные библиотеки были просто «дополнительными расширениями» для приложение.

В этом случае единственным обходным решением будет переустановка APK, убедившись, что для приложения достаточно места.

Тем не менее, я больше не могу найти ни сообщения об ошибках Android, ни исходную статью в блоге, и я искал ее довольно долго. Так что это объяснение может быть из области мифов и легенд.

Каталог "armeabi-v7a" имеет приоритет над каталогом "armeabi"

Это обсуждение заявки на ошибку намекает на "проблему параллелизма" с установленными собственными библиотеками, см. заявку об ошибке Android № 9089.

Если имеется каталог "armeabi-v7a" только с одной собственной библиотекой, весь каталог для этой архитектуры имеет приоритет над каталогом "armeabi".

Если вы попытаетесь загрузить библиотеку, которая только что присутствует в «armeabi», вы получите исключение UnsatisfiedLinkException. Кстати, эта ошибка была помечена как «работает, как задумано».

Возможный обходной путь

В любом случае: я нашел интересный ответ на аналогичный вопрос здесь, на SO. Все сводится к упаковке всех нативных библиотек в виде необработанных ресурсов в ваш APK и копированию при первом запуске приложения правильных библиотек для текущей архитектуры процессора в файловую систему (приватную). Используйте System.load с полными путями к файлам для загрузки этих библиотек.

Однако у этого обходного пути есть недостаток: поскольку собственные библиотеки будут находиться в качестве ресурсов в APK, Google Play больше не сможет их найти и создать фильтры устройств для обязательных архитектур процессоров. Это можно обойти, поместив «фиктивные» нативные библиотеки в папку lib для всех целевых архитектур.

В целом я считаю, что об этой проблеме следует должным образом сообщить в Google. Кажется, что и Jelly Bean, и Google Play имеют недостатки.

Обычно помогает предложить клиенту с этой проблемой переустановить приложение. К сожалению, это не очень хорошее решение, если вас беспокоит потеря данных приложения.

person tiguchi    schedule 08.08.2013
comment
Отличная информация здесь. Мы строим против arm и armv7a. У нас есть все библиотеки для обеих архитектур. Проблемы с распаковкой, кажется, имеют смысл для проблемы, с которой мы столкнулись. Это во всех версиях андроида. Я знаю некоторых людей, которые отправляют свои библиотеки в папку с ресурсами, распаковывают их в app/files и загружают оттуда с помощью System.load() У нас есть встроенные файлы, и они никогда не подводили. Я попробую. - person philipp; 08.08.2013
comment
@philipp Я получил еще один такой отчет о сбое от пользователя, и я думаю, что очень подозрительно, что файл APK имеет суффикс -2.apk. Произошло это после обновления. Может это просто временный сбой? Сообщали ли ваши пользователи, что ваше приложение снова работает при втором запуске? Прямо сейчас я пытаюсь найти другой обходной путь, например, очистку кэша dex. Возможно, это также переустановит собственные библиотеки при загрузке. - person tiguchi; 20.08.2013
comment
Я думаю, что -2.apk - это просто временный apk, пока он загружается, и он в основном выполняет обмен с другим apk. Я не уверен (нужно копаться в исходном коде, чтобы проверить). - person Chrispix; 24.08.2013
comment
@Chrispix Я считаю, что эти добавочные обновления являются просто функцией / ошибкой Google Play, и вы не сможете найти ничего связанного в исходном коде Android. Но если вы все равно что-то найдете, пожалуйста, сообщите мне :-) - person tiguchi; 24.08.2013
comment
@NobuGames - я не верю, что суффиксы -1.apk, -2.apk и т. д. указывают на то, что при установке что-то пошло не так. При тестировании и установке отладочных версий моего приложения я вижу, что -1 или -2 регулярно добавляются к имени файла apk, часто меняются при каждой установке, и в работе приложения нет ничего плохого. Должно быть что-то еще, скорее всего, родная библиотека не устанавливается из-за нехватки места на соответствующем разделе. Смотрите мой ответ для более простого обходного пути, который я нашел. - person gregko; 20.06.2014

Android начал упаковывать свои библиотеки в /data/app-lib// где-то около Ice Cream Sandwich или Jellybean, я не могу вспомнить.

Вы создаете и распространяете как для «arm», так и для «armv7a»? Насколько я понимаю, вы построили только для одной из архитектур, а тестируете другую.

person David    schedule 08.08.2013
comment
Есть ли у вас дополнительная информация об изменении места хранения библиотеки (hasAllNativeLibs) - person philipp; 14.08.2013

Если вы выпускаете только наборы приложений, эта проблема может быть связана с https://issuetracker.google.com/issues/127691101

Это происходит на некоторых устройствах LG или старых устройствах Samsung, где пользователь переместил приложение на SD-карту.

Один из способов решить эту проблему — использовать библиотеку Relinker для загрузки собственных библиотек вместо прямого вызова метода System.load. Это сработало для варианта использования моего приложения.

https://github.com/KeepSafe/ReLinker

Другой способ — заблокировать перемещение приложения на SD-карту.

Вы также можете оставить android.bundle.enableUncompressedNativeLibs=false в файле gradle.properties. Но это увеличит размер загружаемого приложения в Play Store, а также размер его диска.

person shubhamgarg1    schedule 28.06.2021

После того, как я сам столкнулся с этой проблемой и провел небольшое исследование (также известное как гугление), я смог решить проблему, нацелившись на более низкий SDK.

У меня для compileSdkVersion было установлено значение 20 (что работало на 4.4)

а также

minSdkVersion 20 targetSdkVersion 20

Как только я изменил их на 18 (я был в состоянии сделать это без каких-либо серьезных последствий), я смог без проблем запустить приложение на Lollipop.

Надеюсь, это поможет - это сводило меня с ума в течение нескольких дней.

person AJVW    schedule 29.07.2015
comment
интересный. У нас был самый низкий уровень API 9 в течение долгого времени, а теперь это 15. Проблема по-прежнему остается нашей самой большой проблемой. Недавно мы работали над решением более низкого уровня. Обновлю, как только подтвердится, что работает - person philipp; 29.07.2015
comment
Понижение targetSdkVersion не является решением. - person amitooshacham; 14.12.2016