web-dev-qa-db-fra.com

Pourquoi certains téléphones Android Android font que notre application lance une Java.lang.UnsatisfiedLinkError?

Nous rencontrons un Java.lang.UnsatisfiedLinkError Sur certains des téléphones Android qui utilisent notre application sur le marché.

Description du problème:

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

Bloque l'application sur l'une des lignes System.loadLibrary() avec 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

Approche de la solution

Nous avons commencé à exécuter des diagnostics personnalisés sur toutes nos installations pour vérifier si chaque lib est décompressée dans le dossier /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;
            }
        }
    }
}

Sur chaque téléphone de test que nous avons numberOfLibFiles == 3 Et subFilesLarger0 == true. Nous voulions tester si toutes les bibliothèques sont décompressées correctement et sont plus grandes que 0 octet. De plus, nous examinons freeSpace pour voir combien d'espace disque est disponible. freeSpace correspond à la quantité de mémoire que vous pouvez trouver dans Paramètres -> Applications en bas de l'écran. L'idée derrière cette approche était que lorsqu'il n'y a pas assez d'espace sur le disque disponible, le programme d'installation pourrait avoir des problèmes pour décompresser l'APK.

Scénario du monde réel

En regardant les diagnostics, certains des appareils là-bas n'ont [~ # ~] pas [~ # ~] ont les 3 libs dans le /data/data/app_id/lib Mais il y a beaucoup d'espace libre. Je me demande pourquoi le message d'erreur recherche /data/app-lib/app_id-2. Tous nos téléphones stockent leurs bibliothèques dans /data/data/app_id/lib. La System.loadLibrary() devrait également utiliser un chemin cohérent pour l'installation et le chargement des bibliothèques? Comment savoir où le système d'exploitation recherche les bibliothèques?

Question

Quelqu'un rencontre des problèmes avec l'installation de bibliothèques natives? Quels contournements ont réussi? Avez-vous de l'expérience avec le simple téléchargement de bibliothèques natives sur Internet lorsqu'elles n'existent pas et leur stockage manuel? Qu'est-ce qui pourrait causer le problème en premier lieu?

[~ # ~] modifier [~ # ~]

J'ai maintenant également un utilisateur qui rencontre ce problème après une mise à jour d'application. La version précédente fonctionnait bien sur son téléphone, et après une mise à jour, les bibliothèques natives semblent manquantes. La copie manuelle des bibliothèques semble également poser problème. Il est sur Android 4.x avec un téléphone non rooté sans ROM personnalisée.

EDIT 2 - Solution

Après 2 ans de temps passé sur ce problème. Nous avons trouvé une solution qui fonctionne bien pour nous maintenant. Nous l'ouvrons en source: https://github.com/KeepSafe/ReLinker

39
philipp

J'ai le même problème, et le UnsatisfiedLinkErrors est disponible sur toutes les versions de Android - au cours des 6 derniers mois, pour une application qui compte actuellement plus de 90000 installations actives, j'avais:

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%

et les utilisateurs signalent que cela se produit généralement juste après la mise à jour de l'application. Il s'agit d'une application qui reçoit environ 200 à 500 nouveaux utilisateurs par jour.

Je pense que j'ai trouvé une solution de contournement plus simple . Je peux savoir où est l'apk d'origine de mon application avec ce simple appel:

    String apkFileName = context.getApplicationInfo().sourceDir;

cela renvoie quelque chose comme "/data/app/com.example.pkgname-3.apk", le nom de fichier exact du fichier APK de mon application. Ce fichier est un fichier Zip standard et il est lisible sans root. Par conséquent, si j'attrape Java.lang.UnsatisfiedLinkError, je peux extraire et copier ma bibliothèque native, depuis l'intérieur du dossier .apk (Zip) lib/armeabi-v7a (ou quelle que soit l'architecture sur laquelle je suis), vers n'importe quel répertoire où Je peux lire/écrire/exécuter et le charger avec System.load (full_path).

Edit: Il semble fonctionner

Mise à jour le 1er juillet 2014 depuis la sortie d'une version de mon produit avec le code similaire à la liste ci-dessous, le 23 juin 2014, n'a pas eu d'insatisfait Erreurs de lien de ma bibliothèque native.

Voici le code que j'ai utilisé:

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

Malheureusement, si une mauvaise mise à jour de l'application laisse une ancienne version de la bibliothèque native ou une copie endommagée, que nous avons chargée avec System.loadLibrary ("MyNativeLibName"), il n'y a aucun moyen de la décharger. En découvrant une bibliothèque défunte restante persistante dans le dossier lib natif de l'application standard, par exemple en appelant l'une de nos méthodes natives et en découvrant qu'elle n'est pas là (UnsatisfiedLinkError), nous pourrions stocker une préférence pour éviter d'appeler complètement System.loadLibrary () et de s'appuyer sur notre propre code d'extraction et de chargement lors des prochains démarrages d'application.

Pour être complet, voici la classe UnzipUtil, que j'ai copiée et modifiée à partir de cela article 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();
    }
}

Greg

13
gregko

EDIT: Depuis que j'ai reçu un autre rapport de plantage hier pour l'une de mes applications, j'ai approfondi un peu la question et trouvé une troisième explication très probable de ce problème:

La mise à jour partielle de Google Play APK ne va pas

Pour être honnête, je ne connaissais pas cette fonctionnalité. Le suffixe de nom de fichier APK "-2.apk" m'a rendu suspect. Il est mentionné dans le message d'erreur de cette question ici et je pourrais également trouver ce suffixe dans le rapport d'erreur de ce client.

Je crois que le "-2.apk" fait allusion à une mise à jour partielle qui délivre probablement un delta plus petit aux appareils Android. Ce delta ne contient apparemment pas de bibliothèques natives quand elles n'ont pas changé depuis la version précédente .

Pour une raison quelconque, la fonction System.loadLibrary essaie de rechercher la bibliothèque native à partir de la mise à jour partielle (là où elle n'existe pas). C'est à la fois une faille dans Android et dans Google Play.

Voici un rapport de bogue très pertinent avec une discussion intéressante sur des observations similaires: https://code.google.com/p/Android/issues/detail?id=35962

Il semble que Jelly bean soit défectueux en termes d'installation et de chargement de la bibliothèque native (les deux rapports de plantage qui étaient fournis étaient des périphériques Jelly bean).

Si c'est vraiment le cas, je soupçonne qu'un changement forcé dans le code de la bibliothèque NDK peut résoudre le problème, comme changer une variable factice inutilisée pour chaque version. Cependant, cela doit être fait de manière à ce que le compilateur conserve cette variable sans l'optimiser.

EDIT (19/12/13): L'idée de changer le code de bibliothèque natif pour chaque build ne fonctionne malheureusement pas. Je l'ai essayé avec l'une de mes applications et j'ai reçu un rapport d'erreur "Erreur de lien non satisfaite" d'un client qui a quand même mis à jour.

Installation incomplète de l'APK

C'est malheureusement juste hors de ma mémoire et je ne trouve plus de lien. L'année dernière, j'ai lu un article de blog sur ce problème de lien insatisfait. L'auteur a dit qu'il s'agissait d'un bogue dans la routine d'installation de l'APK.

Lorsque la copie des bibliothèques natives dans leur répertoire cible échoue pour une raison quelconque (le périphérique a manqué d'espace de stockage, peut-être aussi gâché les autorisations d'écriture de répertoire ...), le programme d'installation renvoie toujours "succès", comme si les bibliothèques natives n'étaient que des "extensions facultatives" pour Une application.

Dans ce cas, la seule solution consiste à réinstaller l'APK tout en vous assurant qu'il y a suffisamment d'espace de stockage pour l'application.

Cependant, je ne trouve plus de billet de bug Android Android ni l'article de blog original et je l'ai cherché un peu. Cette explication peut donc être dans le domaine des mythes et des légendes.

Le répertoire "armeabi-v7a" a priorité sur le répertoire "armeabi"

Cette discussion de ticket de bogue fait allusion à un "problème de concurrence" avec les bibliothèques natives installées, voir ticket de bogue Android # 9089 .

S'il existe un répertoire "armeabi-v7a" avec une seule bibliothèque native, le répertoire entier de cette architecture a priorité sur le répertoire "armeabi".

Si vous essayez de charger une bibliothèque qui est juste présente dans "armeabi", vous obtiendrez une exception UnsatisfiedLinkException. Soit dit en passant, ce bogue a été signalé comme "fonctionne comme prévu".

Solution possible

Dans les deux cas: j'ai trouvé une réponse intéressante à une question similaire ici sur SO. Tout se résume à empaqueter toutes les bibliothèques natives en tant que ressources brutes dans votre APK et à copier sur la première application, démarrez les bonnes pour l'architecture de processeur actuelle dans le système de fichiers (privé de l'application). Utilisez System.load avec les chemins de fichier complets pour charger ces bibliothèques.

Cependant, cette solution de contournement a un défaut: puisque les bibliothèques natives résideront en tant que ressources dans l'APK, Google Play ne pourra plus les trouver et créer des filtres de périphérique pour les architectures de processeur obligatoires. Il pourrait être contourné en plaçant des bibliothèques natives "factices" dans le dossier lib pour toutes les architectures cibles.

Dans l'ensemble, je pense que ce problème doit être correctement communiqué à Google. Il semble que Jelly Bean et Google Play soient défectueux.

Il est généralement utile d'indiquer au client ayant ce problème de réinstaller l'application. Ce n'est malheureusement pas une bonne solution si la perte de données d'application est un problème.

19
tiguchi

Android a commencé à empaqueter leurs bibliothèques dans/data/app-lib // quelque part autour de Sandwich à la crème glacée ou de Jellybean, je ne me souviens pas lequel.

Construisez-vous et distribuez-vous à la fois pour "arm" et "armv7a"? Ma meilleure supposition est que vous n'avez construit que pour l'une des architectures et que vous testez sur l'autre.

0
David