¿Por qué algunos teléfonos Android hacen que nuestra aplicación para lanzar un java.lang.UnsatisfiedLinkError?


Estamos experimentando un java.lang.UnsatisfiedLinkError en algunos de los teléfonos Android que están utilizando nuestra aplicación en el mercado.

Descripción del Problema:

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

Bloquea la aplicación en una de las líneas System.loadLibrary() con un 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

Enfoque de Solución

Empezamos a ejecutar algunos diagnósticos personalizados en todas nuestras instalaciones para comprobar si cada lib está descomprimida en la carpeta /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;
            }
        }
    }
}

En cada teléfono de prueba que tenemos numberOfLibFiles == 3 y subFilesLarger0 == true. Queríamos probar si todas las libs se desempaquetan correctamente y son más grandes que 0 bytes. Además estamos mirando freeSpace para ver cuánto espacio en disco está disponible. freeSpace coincide con la cantidad de memoria que puede encontrar en Configuración Applications> Aplicaciones en la parte inferior de la pantalla. La idea detrás de este enfoque era que cuando no hay suficiente espacio en el disco disponible, el instalador podría tener problemas para desempaquetar el APK.

Escenario del mundo real

Mirando los diagnósticos, algunos de los los dispositivos que hay NO tienen las 3 libs en la carpeta /data/data/app_id/lib pero tienen mucho espacio libre. Me pregunto por qué el mensaje de error está buscando /data/app-lib/app_id-2. Todos nuestros teléfonos almacenan sus libs en /data/data/app_id/lib. También el System.loadLibrary() debe utilizar una ruta consistente a través de la instalación y la carga de las libs? ¿Cómo puedo saber dónde está el sistema operativo buscando las libs?

Pregunta

¿Alguien tiene problemas con la instalación de bibliotecas nativas? ¿Qué soluciones han tenido éxito? Cualquier experiencia con solo descargar libs nativas a través de Internet cuando no existen y almacenarlas manualmente? ¿Qué podría causar el problema en primer lugar?

EDITAR

Ahora también tengo un usuario que se encuentra con este problema después de una actualización de la aplicación. La versión anterior funcionó bien en su teléfono, y después de una actualización, las bibliotecas nativas parecen faltar. Copiar las libs manualmente también parece causar problemas. Está en Android 4.x con un teléfono no arraigado sin ROM personalizada.

EDITAR 2-Solución

Después de 2 años de dedicar tiempo a este problema. Se nos ocurrió una solución que funciona bien para nosotros ahora. Hemos abierto el código fuente: https://github.com/KeepSafe/ReLinker

Author: philipp, 2013-08-07

4 answers

Tengo el mismo problema, y el UnsatisfiedLinkErrors viene en todas las versiones de Android-en los últimos 6 meses, para una aplicación que actualmente tiene más de 90000 instalaciones activas, tuve:

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%

Y los usuarios informan que generalmente sucede justo después de la actualización de la aplicación. Esto es para una aplicación que obtiene alrededor de 200-500 nuevos usuarios por día.

Creo que se me ocurrió una solución más simple. Puedo averiguar dónde está el apk original de mi aplicación con este simple llamada:

    String apkFileName = context.getApplicationInfo().sourceDir;

Esto devuelve algo como "/data/app/com.ejemplo.pkgname-3.apk", el nombre exacto del archivo APK de mi aplicación. Este archivo es un archivo ZIP normal y es legible sin root. Por lo tanto, si cojo el java.lang.UnsatisfiedLinkError, puedo extraer y copiar mi biblioteca nativa, desde el interior de .apk (zip) lib/armeabi-v7a folder (o cualquier arquitectura en la que esté), a cualquier directorio donde pueda leer/escribir / ejecutar, y cargarlo con el Sistema.load (full_path).

Editar: parece funcionar

Actualización 1 de julio de 2014 desde el lanzamiento de una versión de mi producto con el código similar al que se muestra a continuación, el 23 de junio de 2014, no tenía ningún Error de Enlace Insatisfecho de mi biblioteca nativa.

Aquí está el código que usé:

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

Desafortunadamente, si una mala actualización de la aplicación deja una versión antigua de la biblioteca nativa, o una copia de alguna manera dañado, que cargamos con el sistema.LoadLibrary ("MyNativeLibName"), no hay forma de descargarlo. Al enterarse de tal biblioteca desaparecida remanente que persiste en la carpeta de lib nativa de la aplicación estándar, por ejemplo, al llamar a uno de nuestros métodos nativos y descubrir que no está allí (UnsatisfiedLinkError de nuevo), podríamos almacenar una preferencia para evitar llamar al sistema estándar.LoadLibrary () en conjunto y confiando en nuestro propio código de extracción y carga en el próximo inicio de la aplicación.

Para completar, aquí está la clase UnzipUtil, que copié y modifiqué de esta Descompresión de CodeJava artículo:

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

Greg

 13
Author: gregko,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-07-01 13:13:18

EDITAR: Desde que recibí otro informe de bloqueo ayer para una de mis aplicaciones, profundicé un poco más en el asunto y encontré una tercera explicación muy probable para ese problema:

Google Play Parcial APK Actualización Va Mal

Para ser honesto, yo no sabía acerca de esa característica. El sufijo del nombre del archivo APK " -2.apk " me hizo sospechar. Se menciona en el mensaje de bloqueo de esta pregunta aquí y también pude encontrar ese sufijo en el informe de bloqueo de ese cliente mío.

I cree en el " -2.apk " insinúa una actualización parcial que probablemente ofrece un delta más pequeño para dispositivos Android. Ese delta aparentemente no contiene bibliotecas nativas cuando no cambiaron desde la versión anterior.

Por cualquier razón el Sistema.La función LoadLibrary intenta buscar la biblioteca nativa desde la actualización parcial (donde no existe). Es a la vez un defecto en Android y en Google Play.

Aquí hay un informe de error muy relevante con una discusión interesante sobre observaciones similares: https://code.google.com/p/android/issues/detail?id=35962

Parece que Jelly Bean puede tener fallas en términos de instalación y carga de bibliotecas nativas (ambos informes de fallas que llegaron eran dispositivos Jelly Bean).

Si este es realmente el caso, sospecho que algún cambio forzado en el código de la biblioteca NDK puede solucionar el problema, como cambiar alguna variable ficticia no utilizada para cada versión. Sin embargo, eso debe hacerse de una manera que el compilador conserva esa variable sin optimizarla.

EDIT (19/12/13): La idea de cambiar el código de la biblioteca nativa para cada compilación desafortunadamente no funciona. Lo intenté con una de mis aplicaciones y obtuve un informe de bloqueo de "error de enlace insatisfecho" de un cliente que lo actualizó de todos modos.

Instalación incompleta de APK

Eso desafortunadamente está fuera de mi memoria y ya no puedo encontrar un enlace. El año pasado leí un artículo de blog sobre ese enlace insatisfecho cuestión. El autor dijo que esto es un error en la rutina de instalación de APK.

Cuando la copia de bibliotecas nativas a su directorio de destino falla por cualquier razón (el dispositivo se quedó sin espacio de almacenamiento, tal vez también desordenó los permisos de escritura del directorio...) el instalador todavía devuelve "éxito", como si las bibliotecas nativas fueran solo" extensiones opcionales " a una aplicación.

En este caso, la única solución sería reinstalar el APK mientras se asegura de que haya suficiente espacio de almacenamiento para el app.

Sin embargo, ya no puedo encontrar ningún ticket de error de Android ni el artículo original del blog y lo busqué durante bastante tiempo. Así que esta explicación puede estar en el reino de los mitos y leyendas.

"armeabi-v7a directorio" tiene prioridad sobre "armeabi" directorio

Esta discusión del ticket de error apunta a un "problema de concurrencia" con las bibliotecas nativas instaladas, consulte Ticket de error de Android #9089.

Si hay un directorio" armeabi-v7a" presente con una sola biblioteca nativa, todo el directorio de esa arquitectura tiene prioridad sobre el directorio" armeabi".

Si intenta cargar una biblioteca que solo está presente en "armeabi" obtendrá una excepción UnsatisfiedLinkException. Ese error ha sido marcado como" funciona según lo previsto " por cierto.

Posible solución

En cualquier caso: Encontré una respuesta interesante a una pregunta similar aquí en SO. Todo se reduce a empaquetar todo nativo bibliotecas como recursos sin procesar a su APK y copiar en la primera aplicación iniciar los correctos para la arquitectura del procesador actual en el sistema de archivos (aplicación privada). Usa el Sistema.cargue con las rutas de archivo completas para cargar estas bibliotecas.

Sin embargo, esta solución tiene un defecto: dado que las bibliotecas nativas residirán como recursos en el APK, Google Play ya no podrá encontrarlas y crear filtros de dispositivo para las arquitecturas de procesador obligatorias. Podría ser trabajado alrededor poniendo " dummy" bibliotecas nativas en la carpeta lib para todas las arquitecturas de destino.

En general, creo que este problema debe comunicarse correctamente a Google. Parece como si tanto Jelly Bean y Google Play son defectuosos.

Por lo general, ayuda decirle al cliente con ese problema que reinstale la aplicación. Desafortunadamente, esta no es una buena solución si la pérdida de datos de la aplicación es una preocupación.

 19
Author: tiguchi,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-05-23 12:34:24

Android comenzó a empaquetar sus bibliotecas en /data/app-lib// en algún momento alrededor de Ice Cream Sandwich o Jellybean, No puedo recordar cuál.

¿Estás construyendo y distribuyendo tanto para "arm" como para "armv7a"? Mi mejor conjetura es que solo construiste para una de las arquitecturas y estás probando en la otra.

 0
Author: David,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-08-08 15:50:56

Después de tener este problema yo mismo y hacer un poco de investigación (también conocido como googlear), pude solucionar el problema dirigiéndome a un SDK inferior.

Tenía compileSdkVersion establecido en 20 (que funcionó en 4.4)

Y también

MinSdkVersion 20 targetSdkVersion 20

Una vez que los cambié a 18 (estaba en condiciones de hacerlo sin mayores implicaciones) pude ejecutar la aplicación sin problemas en Lollipop.

Espero que ayude - Me volvió loco durante días.

 -2
Author: AJVW,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2015-07-29 09:30:20