Подтвердить что ты не робот

Почему некоторые 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 библиотеки lib в папке /data/data/app_id/lib, но имеют достаточно свободного места. Мне интересно, почему сообщение об ошибке ищет /data/app-lib/app_id-2. Все наши телефоны хранят свои библиотеки в /data/data/app_id/lib. Кроме того, System.loadLibrary() должен использовать согласованный путь для установки и загрузки libs? Как я могу узнать, где ОС ищет библиотеки?

Вопрос

Кто-нибудь испытывает проблемы с установкой родных libs? Какая работа прошла успешно? Какой-либо опыт загрузки загруженных в интернет-потоках собственных интернет-библиотек, если они не существуют и хранятся вручную? Что может вызвать проблему в первую очередь?

ИЗМЕНИТЬ

Теперь у меня также есть пользователь, который сталкивается с этой проблемой после обновления приложения. Предыдущая версия отлично работала на его телефоне, после обновления, по-видимому, отсутствуют собственные библиотеки. Как правило, копирование libs также вызывает проблемы. Он находится на android 4.x с ненастроенным телефоном без пользовательского ПЗУ.

РЕДАКТИРОВАТЬ 2 - Решение

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

4b9b3361

Ответ 1

У меня такая же проблема, и 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).

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

Обновление 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" ), его невозможно разгрузить. Узнав о такой оставшейся не существующей библиотеке, задерживающейся в стандартной папке lib lib, например вызывая один из наших собственных методов и не обнаруживая его (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();
    }
}

Грег

Ответ 2

EDIT: Поскольку вчера я получил еще один отчет о сбоях в одном из моих приложений, я углубился в этот вопрос и нашел третье очень вероятное объяснение этой проблемы:

Частичное обновление 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 (12/19/13): Идея изменения кода собственной библиотеки для каждой сборки, к сожалению, не работает. Я попробовал это с одним из моих приложений и получил сообщение о сбоях "неудовлетворенной ссылки" от клиента, который все равно обновил.

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

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

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

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

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

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

В этом обсуждении с ошибкой намекает на проблему "concurrency" с установленными родными библиотеками, см. Биг-код Android # 9089.

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

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

Возможное обходное решение

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

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

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

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

Ответ 3

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

Вы строите и распространяете как для "руки", так и "armv7a"? Я полагаю, что вы построили только одну из архитектур, и вы тестируете другую.

Ответ 4

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

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

а также

minSdkVersion 20 targetSdkVersion 20

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

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