Reproducing and resolving Android java.lang.unsatisfiedLinkError locally
Asked Answered
H

7

29

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
Hume answered 21/5, 2019 at 4:32 Comment(13)
Can you post your Wrapper.java?Mesothorax
@Mesothorax I've edited my post just now and added Wrapper.java as well as the cpp side of the entry point. Thanks in advance for your help!Hume
not sure this helps or not developer.android.com/topic/performance/…, make it be android:extractNativeLibs="true" to see how it goes.Digestible
Please post the content of pluspoints.h too.Catechu
@Digestible I will give it a try. Currently I have set android:extraxtNativeLibs="false".Hume
@Nghia Bui I added the content of the pluspoints.h file to the original post. Thanks for looking into it!Hume
I think you're probably just missing your loadLibrary call https://mcmap.net/q/502789/-jni-hello-world-unsatisfied-link-error Try this outGuyton
@Guyton no, the loadLibrary call is the first thing in the onCreate method of the main activity of our app. The error only rarely occurs on devices of our users. I was not able to reproduce it until now.Hume
Can you pinpoint if it is only happening to devices of specific API levels?Guyton
I assume you've already taken a look at developer.android.com/training/articles/…Guyton
developer.android.com/training/articles/…Guyton
@ luckyging3er there seems to be no pattern cincerning the Android version, it occurred on all supported versions so far: 5, 6 ,7 ,8 as well as 9. Thanks for the link, yeah I've seen that one already. Will work through it another time though to make sure I got everything the intended way.Hume
The error is still unresolved. If somebody out there has experience with this or another pointer that would be very helpful. I am happy to post more infos if that helps. Thanks in advance for your support.Hume
V
2

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.

Vicechairman answered 5/6, 2019 at 15:38 Comment(1)
thanks for your help. I am not 100% sure if our app is using Multidex, at least we did not include it in the dependencies of app/build.gradle. But as I described above I use the Android app bundle to publish the app to the play store - possibly this makes use of multidex? According to the google dev page there is a dex folder within the app bundle. But I did not figure out yet, where I'd need to mark the Wrapper class as required s. t. the app bundler will understand. Let me look into that.Hume
C
0

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.

Catechu answered 4/6, 2019 at 16:59 Comment(1)
thanks for pointing that out, will fix that and let you know about the result.Hume
J
0

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.

Jacobs answered 7/6, 2019 at 15:51 Comment(0)
L
0

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

Labelle answered 7/6, 2019 at 23:41 Comment(0)
B
0

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.

Bile answered 8/6, 2019 at 6:52 Comment(1)
thanks a lot for your explanations and suggestions. According to Google, armeabi is "Deprecated in r16. Removed in r17". Also, Google will not allow app releases that do not support 64-bit architecture from August 2019 on. Hence it is not really worth considering creating a "32-bit-only" app, I just wanted to try it to see if the error still occurs. I will remove the re-linking and optimization for the release version and see if it helps.Hume
M
0

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

Merralee answered 12/12, 2019 at 18:3 Comment(0)
P
0

This issue might be related to https://issuetracker.google.com/issues/127691101

It happens on some devices of LG or old Samsung devices where the user has moved the app to the SD Card.

One way of fixing the issue is to use Relinker library to load your native libraries instead of directly calling System.load method. It did work for my app's use case.

https://github.com/KeepSafe/ReLinker

Another way is to block the movement of the app to the SD card.

You can also keep android.bundle.enableUncompressedNativeLibs=false in your gradle.properties file. But it will increase the app download size on Play Store as well as its disk size.

Psephology answered 28/6, 2021 at 19:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.