Воспроизведение и разрешение Android java.lang.unsatisfiedLinkError локально - PullRequest
15 голосов
/ 21 мая 2019

Вместе с другом я создал приложение для Android для организации школьных оценок.Приложение отлично работает на моем устройстве и на большинстве пользовательских устройств, однако частота сбоев превышает 3%, в основном из-за java.lang.UnsatisfiedLinkError и происходит в версиях Android 7.0, 8.1, а также 9.

I 'Я тестировал приложение на моем телефоне и на нескольких эмуляторах, включая все архитектуры.Я загружаю приложение в магазин приложений в виде пакета 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

Ответы [ 5 ]

0 голосов
/ 08 июня 2019

то, что это связано с proguard, маловероятно - и предоставленный код совершенно не имеет значения. build.gradle и структура каталогов - единственное, что нужно знать. при написании Android 7,8,9 это скорее всего связано с ARM64. вопрос также содержит довольно неточное предположение о том, что ARM64 сможет запускать нативную сборку ARM ... потому что это единственный случай, когда 32-битная нативная сборка помещается в каталог armeabi; но он будет жаловаться на UnsatisfiedLinkError при использовании каталога armeabi-v7a. это даже не требуется при возможности сборки для ARM64 и переноса собственной сборки ARM64 в каталог arm64-v8a.

и если это должно быть связано с комплектом приложений (я только что заметил тег содержимого), то, вероятно, нативная сборка для ARM64 была упакована в неправильную часть комплекта - или платформа ARM64 не поставляется с этой сборкой , Я бы посоветовал не много пересоединять, а внимательно проверить, что на самом деле а) было упаковано и б) доставляется на платформу ARM64. какой процессор у тех моделей, которые не могут связать, также может быть интересным, просто чтобы посмотреть, есть ли шаблон.

Попадание в руку любой из этих проблемных моделей, будь то аппаратное обеспечение или облачный эмулятор (который предпочтительно работает на реальном оборудовании), может оказаться наиболее простым, по крайней мере, для воспроизведения проблемы во время тестирования. найдите модели, а затем перейдите на eBay, найдите "2nd hand" или "refurbished" ... возможно, ваши тесты не смогли воспроизвести проблему из-за отсутствия установки пакета из Play Store.

0 голосов
/ 08 июня 2019

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

0 голосов
/ 05 июня 2019

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

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

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

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

0 голосов
/ 07 июня 2019

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

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

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

0 голосов
/ 04 июня 2019

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

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

...