Avec un ami, j'ai créé une application Android pour organiser les notes scolaires. L'application fonctionne bien sur mon appareil et sur la plupart des appareils des utilisateurs, mais il y a un taux de plantage de plus de 3%, principalement en raison de Java.lang.UnsatisfiedLinkError
et se produisant sur Android versions 7.0, 8.1 ainsi que 9.
J'ai testé l'application sur mon téléphone et sur plusieurs émulateurs, y compris toutes les architectures. Je télécharge l'application sur l'App Store en tant qu'ensemble d'applications Android et je soupçonne que cela pourrait être la source du problème.
Je suis un peu perdu ici, car j'ai déjà essayé plusieurs choses mais jusqu'à présent je n'ai pas pu réduire le nombre d'occurrences ni le reproduire sur aucun de mes appareils. Toute aide sera grandement appréciée.
J'ai trouvé cette ressource qui souligne que Android échoue parfois à décompresser les bibliothèques externes. Par conséquent, ils ont créé une bibliothèque ReLinker qui tentera de récupérer les bibliothèques de l'application compressée:
Malheureusement, cela n'a pas réduit le nombre de plantages dus à Java.lang.UnsatisfiedLinkError
. J'ai poursuivi mes recherches en ligne et trouvé cet article , ce qui suggère que le problème réside dans les bibliothèques 64 bits. J'ai donc supprimé les bibliothèques 64 bits (l'application fonctionne toujours sur tous les appareils, car les architectures 64 bits peuvent également exécuter des bibliothèques 32 bits). Cependant, l'erreur se produit toujours à la même fréquence que précédemment.
Grâce à la console Google Play, j'ai reçu le rapport d'erreur suivant:
Java.lang.UnsatisfiedLinkError:
at ch.fidelisfactory.pluspoints.Core.Wrapper.callCoreEndpointJNI (Wrapper.Java)
at ch.fidelisfactory.pluspoints.Core.Wrapper.a (Wrapper.Java:9)
at ch.fidelisfactory.pluspoints.Model.Exam.a (Exam.Java:46)
at ch.fidelisfactory.pluspoints.SubjectActivity.i (SubjectActivity.Java:9)
at ch.fidelisfactory.pluspoints.SubjectActivity.onCreate (SubjectActivity.Java:213)
at Android.app.Activity.performCreate (Activity.Java:7136)
at Android.app.Activity.performCreate (Activity.Java:7127)
at Android.app.Instrumentation.callActivityOnCreate (Instrumentation.Java:1272)
at Android.app.ActivityThread.performLaunchActivity (ActivityThread.Java:2908)
at Android.app.ActivityThread.handleLaunchActivity (ActivityThread.Java:3063)
at Android.app.servertransaction.LaunchActivityItem.execute (LaunchActivityItem.Java:78)
at Android.app.servertransaction.TransactionExecutor.executeCallbacks (TransactionExecutor.Java:108)
at Android.app.servertransaction.TransactionExecutor.execute (TransactionExecutor.Java:68)
at Android.app.ActivityThread$H.handleMessage (ActivityThread.Java:1823)
at Android.os.Handler.dispatchMessage (Handler.Java:107)
at Android.os.Looper.loop (Looper.Java:198)
at Android.app.ActivityThread.main (ActivityThread.Java:6729)
at Java.lang.reflect.Method.invoke (Method.Java)
at com.Android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.Java:493)
at com.Android.internal.os.ZygoteInit.main (ZygoteInit.Java:876)
Le Wrapper.Java
est la classe qui appelle notre bibliothèque native. La ligne vers laquelle il pointe se lit cependant comme suit:
import Java.util.HashMap;
Le ch.fidelisfactory.pluspoints.Core.Wrapper.callCoreEndpointJNI
est le point d'entrée de notre bibliothèque cpp native.
Dans la bibliothèque native de cpp, nous utilisons certaines bibliothèques externes (curl, jsoncpp, plog-logging, sqlite et tinyxml2).
Modifier le 4 juin 2019
Comme demandé, voici le code de Wrapper.Java
:
package ch.fidelisfactory.pluspoints.Core;
import Android.content.Context;
import org.json.JSONException;
import org.json.JSONObject;
import Java.io.Serializable;
import Java.util.HashMap;
import ch.fidelisfactory.pluspoints.Logging.Log;
/***
* Wrapper around the cpp pluspoints core
*/
public class Wrapper {
/**
* An AsyncCallback can be given to the executeEndpointAsync method.
* The callback method will be called with the returned json from the core.
*/
public interface AsyncCallback {
void callback(JSONObject object);
}
public static boolean setup(Context context) {
String path = context.getFilesDir().getPath();
return setupWithFolderAndLogfile(path,
path + "/output.log");
}
private static boolean setupWithFolderAndLogfile(String folderPath, String logfilePath) {
HashMap<String, Serializable> data = new HashMap<>();
data.put("folder", folderPath);
data.put("logfile", logfilePath);
JSONObject res = executeEndpoint("/initialization", data);
return !isErrorResponse(res);
}
public static JSONObject executeEndpoint(String path, HashMap<String, Serializable> data) {
JSONObject jsonData = new JSONObject(data);
String res = callCoreEndpointJNI(path, jsonData.toString());
JSONObject ret;
try {
ret = new JSONObject(res);
} catch (JSONException e) {
Log.e("Error while converting core return statement to json.");
Log.e(e.getMessage());
Log.e(e.toString());
ret = new JSONObject();
try {
ret.put("error", e.toString());
} catch (JSONException e2) {
Log.e("Error while putting the error into the return json.");
Log.e(e2.getMessage());
Log.e(e2.toString());
}
}
return ret;
}
public static void executeEndpointAsync(String path, HashMap<String, Serializable> data, AsyncCallback callback) {
// Create and start the task.
AsyncCoreTask task = new AsyncCoreTask();
task.setCallback(callback);
task.setPath(path);
task.setData(data);
task.execute();
}
public static boolean isErrorResponse(JSONObject data) {
return data.has("error");
}
public static boolean isSuccess(JSONObject data) {
String res;
try {
res = data.getString("status");
} catch (JSONException e) {
Log.w(String.format("JsonData is no status message: %s", data.toString()));
res = "no";
}
return res.equals("success");
}
public static Error errorFromResponse(JSONObject data) {
String errorDescr;
if (isErrorResponse(data)) {
try {
errorDescr = data.getString("error");
} catch (JSONException e) {
errorDescr = e.getMessage();
errorDescr = "There was an error while getting the error message: " + errorDescr;
}
} else {
errorDescr = "Data contains no error message.";
}
return new Error(errorDescr);
}
private static native String callCoreEndpointJNI(String jPath, String jData);
/**
* Log a message to the core
* @param level The level of the message. A number from 0 (DEBUG) to 5 (FATAL)
* @param message The message to log
*/
public static native void log(int level, String message);
}
De plus, voici la définition cpp du point d'entrée qui appelle ensuite notre bibliothèque principale:
#include <jni.h>
#include <string>
#include "pluspoints.h"
extern "C"
JNIEXPORT jstring JNICALL
Java_ch_fidelisfactory_pluspoints_Core_Wrapper_callCoreEndpointJNI(
JNIEnv* env,
jobject /* this */,
jstring jPath,
jstring jData) {
const jsize pathLen = env->GetStringUTFLength(jPath);
const char* pathChars = env->GetStringUTFChars(jPath, (jboolean *)0);
const jsize dataLen = env->GetStringUTFLength(jData);
const char* dataChars = env->GetStringUTFChars(jData, (jboolean *)0);
std::string path(pathChars, (unsigned long) pathLen);
std::string data(dataChars, (unsigned long) dataLen);
std::string result = pluspoints_execute(path.c_str(), data.c_str());
env->ReleaseStringUTFChars(jPath, pathChars);
env->ReleaseStringUTFChars(jData, dataChars);
return env->NewStringUTF(result.c_str());
}
extern "C"
JNIEXPORT void JNICALL Java_ch_fidelisfactory_pluspoints_Core_Wrapper_log(
JNIEnv* env,
jobject,
jint level,
jstring message) {
const jsize messageLen = env->GetStringUTFLength(message);
const char *messageChars = env->GetStringUTFChars(message, (jboolean *)0);
std::string cppMessage(messageChars, (unsigned long) messageLen);
pluspoints_log((PlusPointsLogLevel)level, cppMessage);
}
Ici, le fichier pluspoints.h:
/**
* Copyright 2017 FidelisFactory
*/
#ifndef PLUSPOINTSCORE_PLUSPOINTS_H
#define PLUSPOINTSCORE_PLUSPOINTS_H
#include <string>
/**
* Send a request to the Pluspoints core.
* @param path The endpoint you wish to call.
* @param request The request.
* @return The return value from the executed endpoint.
*/
std::string pluspoints_execute(std::string path, std::string request);
/**
* The different log levels at which can be logged.
*/
typedef enum {
LEVEL_VERBOSE = 0,
LEVEL_DEBUG = 1,
LEVEL_INFO = 2,
LEVEL_WARNING = 3,
LEVEL_ERROR = 4,
LEVEL_FATAL = 5
} PlusPointsLogLevel;
/**
* Log a message with the info level to the core.
*
* The message will be written in the log file in the core.
* @note The core needs to be initialized before this method can be used.
* @param level The level at which to log the message.
* @param logMessage The log message
*/
void pluspoints_log(PlusPointsLogLevel level, std::string logMessage);
#endif //PLUSPOINTSCORE_PLUSPOINTS_H
En regardant la pile d'appels que vous avez signalée dans l'exception:
at ch.fidelisfactory.pluspoints.Core.Wrapper.callCoreEndpointJNI (Wrapper.Java)
at ch.fidelisfactory.pluspoints.Core.Wrapper.a (Wrapper.Java:9)
at ch.fidelisfactory.pluspoints.Model.Exam.a (Exam.Java:46)
at ch.fidelisfactory.pluspoints.SubjectActivity.i (SubjectActivity.Java:9)
at ch.fidelisfactory.pluspoints.SubjectActivity.onCreate (SubjectActivity.Java:213)
Il semble obscurci (ProGuarded)? Après tout, la trace doit impliquer executeEndpoint(String, HashMap<String, Serializable>)
selon votre code collé.
Il se peut que la recherche de la méthode native échoue car les chaînes ne correspondent plus. C'est juste une suggestion - je ne vois pas pourquoi cela échouerait sur seulement 3% des téléphones. Mais j'ai rencontré ce problème avant.
Tout d'abord, testez après avoir désactivé toute obfuscation.
Si elle est liée au proguarding, alors vous voudrez ajouter des règles au projet. Voir ce lien pour des suggestions: Dans proguard, comment conserver un ensemble de noms de méthodes de classes?
Une autre chose, une vérification rapide qui peut être utile pour éviter les plantages disgracieux - ajoutez au démarrage si le nom du package et la méthode qui provoquent plus tard le UnsatisfiedLinkError
peuvent être résolus.
//this is the potentially obfuscated native method you're trying to test
String myMethod = "<to fill in>";
boolean result = true;
try{
//set actual classname as required
String packageName = MyClass.class.getPackage().getName();
Log.i( TAG, "Checking package: name is " + packageName );
if( !packageName.contains( myMethod ) ){
Log.w( TAG, "Cannot resolve expected name" );
result = false;
}
}catch( Exception e ){
Log.e( TAG, "Error fetching package name " );
e.printStackTrace();
result = false;
}
Si vous obtenez un résultat négatif, avertissez l'utilisateur d'un problème et échouez gracieusement.
Si 3% des utilisateurs ont fait planter l'application sur un appareil doté de processeurs 64 bits, alors vous devriez voir cet article sur Medium .
Pour reproduire cela localement, vous pouvez charger latéralement un apk [apk x86 pour armer l'appareil ou vice versa ou traverser l'architecture] sur votre téléphone. Habituellement, les utilisateurs peuvent utiliser des outils tels que ShareIt pour transférer des applications entre les téléphones. Une fois cela fait, les architectures des téléphones partagés peuvent être différentes. Il s'agit de la majorité des causes de l'étrange exception de lien non satisfait.
Il existe cependant un moyen d'atténuer ce problème. Play possède une API pour vérifier si une installation s'est produite via PlayStore. De cette façon, vous pouvez restreindre les installations via d'autres canaux et réduire ainsi les exceptions de liaison non satisfaites.
https://developer.Android.com/guide/app-bundle/sideload-check
Vos deux méthodes natives sont déclarées static
en Java, mais en C++ les fonctions correspondantes sont déclarées avec le deuxième paramètre appartenant au type jobject
.
Changer le type en jclass
devrait aider à résoudre votre problème.
que cela a à voir avec proguard est peu probable - et le code fourni est tout à fait hors de propos. les build.gradle
et la structure du répertoire serait la seule chose que l'on aurait besoin de savoir. lors de l'écriture Android 7,8,9, cela est probablement lié à ARM64. la question présente également l'hypothèse assez inexacte, selon laquelle ARM64 serait capable d'exécuter ARM Assembly natif ... car ce n'est que le cas, lors du dépôt de l'assembly natif 32 bits dans le répertoire armeabi
; mais il se plaindra d'un UnsatisfiedLinkError
, lors de l'utilisation du armeabi-v7a
répertoire. cela n'est même pas nécessaire, lorsque vous pouvez générer pour ARM64 et déposer l'assembly natif ARM64 dans arm64-v8a
répertoire.
et si cela doit être lié au bundle d'application (je viens de remarquer la balise de contenu), il semble probable que l'assembly natif pour ARM64 ait été conditionné dans la mauvaise partie du bundle - ou la plate-forme ARM64 n'est pas livrée avec cet assembly. suggérerait de ne pas trop relier, mais inspecter de près ce que en fait a) avait été emballé et b) est livré sur la plate-forme ARM64. quel processeur ces modèles ne parviennent pas à lier pourrait également être intéressant, juste pour voir s'il y a un modèle.
mettre la main sur l'un de ces modèles problématiques, sous forme de matériel ou d'un émulateur basé sur le cloud (qui fonctionne de préférence sur du vrai matériel), pourrait être le plus facile à reproduire au moins le problème pendant le test. recherchez les modèles, puis allez sur eBay, recherchez "d'occasion" ou "reconditionné" ... vos tests n'ont peut-être pas reproduit le problème, car vous n'avez pas installé le bundle depuis Play Store.