Воспроизведение и разрешение Android java.lang.unsatisfiedLinkError локально

Вместе с другом я создал приложение для 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

Ответ 1

Ваши два нативных метода объявлены static в Java, но в C++ соответствующие функции объявлены со вторым параметром, принадлежащим типу jobject.

Изменение типа на jclass должно помочь решить вашу проблему.

Ответ 2

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

Вот одна из потенциальных причин для мультидексных приложений:

В настоящее время почти каждое Android-приложение использует Multidex, чтобы иметь возможность включать в него больше материала. При сборке файла DEX инструменты сборки пытаются понять, какие классы требуются при запуске, и помещают их в основной код. Тем не менее, они могут пропустить что-то, особенно когда JNI связан.

Вы можете попробовать вручную пометить класс Wrapper как это требуется в главном DEX: docs. Это может помочь ему принести свою зависимую нативную библиотеку также в случае, если у вас есть мультидексное приложение.

Ответ 3

Рассматривая стек вызовов, о котором вы сообщили в исключении:

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

Если вы получите отрицательный результат, предупредите пользователя о проблеме и изящно завершите работу.

Ответ 4

Если у 3% пользователей произошел сбой приложения на устройстве с 64-разрядными процессорами, вы должны увидеть этот пост на Medium.

Ответ 5

то, что это связано с 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.