Reproducing and resolving Android java.lang.unsatisfiedLinkError locally

你离开我真会死。 提交于 2021-02-07 11:13:44

问题


Together with a friend I have created an Android app to organize school grades. The app works fine on my device and on most user devices, however there is a crashing rate over 3 percent, mostly because of java.lang.UnsatisfiedLinkError and occurring on Android versions 7.0, 8.1 as well as 9.

I've tested the app on my phone and on several emulators, including all the architectures. I upload the app to the app store as an android-app-bundle and suspect that this could be the source of the problem.

I am a bit lost here, because I've tried already several things but so far I was not able to either reduce the number of occurrences nor to reproduce it on any of my devices. Any help will be highly appreciated.

I have found this resource which points out that Android sometimes fails to unpack external libraries. Therefore they created a ReLinker library which will try to fetch the libraries from the compressed app:

Unfortunately, this did not reduce the amount of crashes due to java.lang.UnsatisfiedLinkError. I continued my online research and found this article, which suggests that the problem lies in the 64-bit libraries. So I removed the 64bit libraries (the app still runs on all devices, because 64-bit architectures can also execute 32-bit libraries). However, the error still occurs in the same frequency like before.

Through the google-play-console I got the following crash report:

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)

The Wrapper.java is the class which calls our native library. The line it points to however just reads as follows:

import java.util.HashMap;

The ch.fidelisfactory.pluspoints.Core.Wrapper.callCoreEndpointJNI is the entry point to our native cpp library.

In the native cpp library we use some external libraries (curl, jsoncpp, plog-logging, sqlite and tinyxml2).


Edit 4th June 2019

As requested, here the code of 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);
}

Additionally,here the cpp definition of the entrypoint that then calls our core library:

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

Here, the pluspoints.h file:

/**
 * 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:


UnsatisfiedLinkError happens when your code tries to call smth that doesn't exist for some reason: post about it

Here is one of potential reasons for multidex apps:

Nowadays almost every Android app uses Multidex to be able to include more stuff in it. When building the DEX file, build tools try to understand which classes are required on the start and puts them to the main dex. However, they can miss smth, especially when JNI is bound.

You can try manually marking the Wrapper class as required to be in the main DEX: docs. It may help it to bring its dependent native library as well in case if you have a multidex app.




回答2:


Your two native methods are declared static in Java, but in C++ the corresponding functions are declared with the second parameter belonging to type jobject.

Changing the type to jclass should help solving your problem.




回答3:


Looking at the call stack you reported in the 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)

It looks obfuscated (ProGuarded)? After all, the trace should involve executeEndpoint(String, HashMap<String, Serializable>) according to your pasted code.

It could be that the lookup of the native method is failing as the strings no longer match. It's just a suggestion - I don't see why it would fail on just 3% of phones. But I came across this problem before.

First off, test after you disable all obfuscation.

If it is related to proguarding, then you will want to add rules to the project. See this link for suggestions: In proguard, how to preserve a set of classes' method names?

Another thing, a quick check that can be useful to prevent unsightly crashes - add upon start-up whether the package name and method that is later causing the UnsatisfiedLinkError can be resolved.

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

If you get a negative result, warn the user of a problem, and fail gracefully.




回答4:


If the 3% of users had the app crash on a device with 64-bit processors then you should see this post on Medium.




回答5:


that this has to do with proguard is unlikely - and the code provided is quite irrelevant. the build.gradle and the directory structure would be the only thing one would need to know. when writing Android 7,8,9 this is most likely ARM64 related. the question also features the fairly inaccurate assumption, that ARM64 would be able to run ARM native assembly... because this is only the case, when dropping the 32bit native assembly into the armeabi directory; but it will complain about an UnsatisfiedLinkError, when using the armeabi-v7a directory. this is not even required, when being able to build for ARM64 and dropping the ARM64 native assembly into arm64-v8a directory.

and if this should be app bundle related (I've just noticed the content tag), it appears likely the the native assembly for ARM64 had been packaged into the wrong bundle part - or ARM64 platform is not being delivered with that assembly. would suggest not to re-link much, but closely inspect what actually a) had been packaged and b) is being delivered to ARM64 platform. which CPU those models have which fail to link might also be interesting, just to see if there is any pattern.

getting your hands on any of these problematic models, either in form of hardware or a cloud-based emulator (which preferably runs on real hardware), might be the most easy to at least reproduce the problem while testing. lookup the models and then go to eBay, search "2nd hand" or "refurbished"... your tests might have failed to reproduce the problem, because not installing the bundle from Play Store.




回答6:


To reproduce this locally, you can side load an apk[x86 apk to arm device or vice versa or cross architecture] to your phone. Usually users might use tools like ShareIt to transfer apps between phones. When done so, the architectures of the sharing phones might be different. This is the majority of cause of the strange unsatisfied link exception.

There is a way that you can mitigate this though. Play has an api to verify if an installation happened through PlayStore. This way you can restrict installs through other channels and hence reducing unsatisfied link exceptions.

https://developer.android.com/guide/app-bundle/sideload-check



来源:https://stackoverflow.com/questions/56231121/reproducing-and-resolving-android-java-lang-unsatisfiedlinkerror-locally

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!