Ответ 1
Ваши два нативных метода объявлены static
в Java, но в C++ соответствующие функции объявлены со вторым параметром, принадлежащим типу jobject
.
Изменение типа на jclass
должно помочь решить вашу проблему.
Вместе с другом я создал приложение для Android для организации школьных оценок. Приложение отлично работает на моем устройстве и на большинстве пользовательских устройств, однако частота сбоев превышает 3%, в основном из-за java.lang.UnsatisfiedLinkError
и возникающих в версиях Android 7.0, 8.1 и 9.
Я протестировал приложение на своем телефоне и на нескольких эмуляторах, включая все архитектуры. Я загружаю приложение в магазин приложений в виде пакета android-app-bundle и подозреваю, что это может быть источником проблемы.
Я немного растерялся, потому что я уже пробовал несколько вещей, но до сих пор я не смог ни уменьшить количество вхождений, ни воспроизвести его ни на одном из моих устройств. Любая помощь будет высоко оценена.
Я нашел этот ресурс, который указывает, что Android иногда не может распаковать внешние библиотеки. Поэтому они создали библиотеку ReLinker, которая будет пытаться извлечь библиотеки из сжатого приложения:
К сожалению, это не уменьшило количество сбоев из-за java.lang.UnsatisfiedLinkError
. Я продолжил свои онлайн-исследования и нашел эту статью, которая предполагает, что проблема заключается в 64-битных библиотеках. Поэтому я удалил 64-битные библиотеки (приложение все еще работает на всех устройствах, потому что 64-битные архитектуры также могут выполнять 32-битные библиотеки). Однако ошибка по-прежнему возникает на той же частоте, что и раньше.
Через google-play-console я получил следующий отчет о сбое:
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)
Wrapper.java
- это класс, который вызывает нашу нативную библиотеку. Строка, на которую он указывает, однако, выглядит следующим образом:
import java.util.HashMap;
ch.fidelisfactory.pluspoints.Core.Wrapper.callCoreEndpointJNI
является точкой входа в нашу собственную библиотеку cpp.
В нативной библиотеке cpp мы используем несколько внешних библиотек (curl, jsoncpp, plog-logging, sqlite и tinyxml2).
Изменить 4 июня 2019
Как и просили, вот код 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);
}
Кроме того, здесь определение cpp точки входа, которая затем вызывает нашу основную библиотеку:
#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);
}
Здесь файл 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
Ваши два нативных метода объявлены static
в Java, но в C++ соответствующие функции объявлены со вторым параметром, принадлежащим типу jobject
.
Изменение типа на jclass
должно помочь решить вашу проблему.
UnsatisfiedLinkError
происходит, когда ваш код пытается вызвать что-то, чего по какой-то причине не существует: напишите об этом
Вот одна из потенциальных причин для мультидексных приложений:
В настоящее время почти каждое Android-приложение использует Multidex, чтобы иметь возможность включать в него больше материала. При сборке файла DEX инструменты сборки пытаются понять, какие классы требуются при запуске, и помещают их в основной код. Тем не менее, они могут пропустить что-то, особенно когда JNI связан.
Вы можете попробовать вручную пометить класс Wrapper
как это требуется в главном DEX: docs. Это может помочь ему принести свою зависимую нативную библиотеку также в случае, если у вас есть мультидексное приложение.
Рассматривая стек вызовов, о котором вы сообщили в исключении:
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)
Это выглядит запутанным (ProGuarded)? В конце концов, трассировка должна включать executeEndpoint(String, HashMap<String, Serializable>)
соответствии с вашим вставленным кодом.
Может случиться так, что поиск нативного метода завершится неудачей, так как строки больше не совпадают. Это всего лишь предложение - я не понимаю, почему он потерпит неудачу только на 3% телефонов. Но я сталкивался с этой проблемой раньше.
Во-первых, проверьте после того, как вы отключите все запутывание.
Если это связано с охраной, то вы захотите добавить правила в проект. Посмотрите эту ссылку для предложений: В Proguard, как сохранить набор имен методов классов?
Еще одна вещь, быстрая проверка, которая может быть полезна для предотвращения неприглядных сбоев - добавьте при запуске, можно ли разрешить имя пакета и метод, который позже вызывает UnsatisfiedLinkError
.
//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;
}
Если вы получите отрицательный результат, предупредите пользователя о проблеме и изящно завершите работу.
Если у 3% пользователей произошел сбой приложения на устройстве с 64-разрядными процессорами, вы должны увидеть этот пост на Medium.
то, что это связано с proguard, маловероятно - и приведенный код совершенно не имеет значения. build.gradle
и структура каталогов - это единственное, что нужно знать. при написании Android 7,8,9 это скорее всего связано с ARM64. вопрос также содержит довольно неточное предположение, что ARM64 сможет запускать нативную сборку ARM... потому что это единственный случай, когда 32- armeabi
нативная сборка armeabi
каталог armeabi
; но он будет жаловаться на UnsatisfiedLinkError
, при использовании armeabi-v7a
каталога. это даже не требуется при возможности сборки для ARM64 и переноса собственной сборки arm64-v8a
каталог arm64-v8a
.
и если это должно быть связано с комплектом приложений (я только что заметил тег содержимого), то, скорее всего, нативная сборка для ARM64 была упакована в неправильную часть комплекта - или платформа ARM64 не поставляется с этой сборкой. Я бы посоветовал не связывать много ссылок, но внимательно проверить, что на самом деле а) было упаковано и б) доставляется на платформу ARM64. какой процессор у тех моделей, которые не могут связать, также может быть интересно, просто посмотреть, есть ли шаблон.
Попасть в руки любой из этих проблемных моделей, будь то в виде аппаратного обеспечения или облачного эмулятора (который предпочтительно работает на реальном оборудовании), может быть проще всего, по крайней мере, воспроизвести проблему во время тестирования. ищите модели, а затем переходите на eBay, ищите "2nd hand" или "refurbished"... ваши тесты не смогли воспроизвести проблему из-за отсутствия установки пакета из Play Store.