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
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.
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.
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.
If the 3% of users had the app crash on a device with 64-bit processors then
you should see this post on Medium.
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.
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
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.
i did a DLL that export methods from another DLL in JNI.
JNIEXPORT bool JNICALL getIsWordInPhonemListFR(const char* word)
{
isWordInPhonemListFR method = NULL;
BOOL fRunTimeLinkSuccess = FALSE;
HINSTANCE hGetProcIDDLL = LoadLibrary(L"PhoneticEngineFR2.dll");
if (!hGetProcIDDLL) {
std::cout << "kcould not load the dynamic library" << std::endl;
}else{
method = (isWordInPhonemListFR)GetProcAddress(hGetProcIDDLL, "isWordInPhonemListFR");
}
return method(word);
}
System.load in java doesn't give any errors but when i want to use my native method, i get a
java.lang.UnsatisfiedLinkError:
Both DLLs are in the same folder in my project. It was working fine when working in visual studio but there is something wrong with the export in JNI. The JNI onLoad also doesn't trigger in java.
Thanks
i am dumb, i had a Namespace over all my JNI export methods.
Therefore, they were not executed. Now it works.
I had created the dynamic library for java using swig and cmake for learning purposes. I can't call a function in java from the same libary that I created. The swig doc told me this is the result of forgeting to compile and link the swig wrapper file to my native libary, but I'm very sure that I did that with cmake build.
CMakeList.txt
cmake_minimum_required (VERSION 2.6)
FIND_PACKAGE(SWIG REQUIRED)
find_package(Java REQUIRED COMPONENTS Runtime Development)
find_package(JNI REQUIRED)
INCLUDE(${SWIG_USE_FILE})
set(JAVA ${java_include_path} )
INCLUDE_DIRECTORIES(${JAVA} ${JAVA}/win32)
INCLUDE_DIRECTORIES(${CMAKE_CURRENT_SOURCE_DIR})
SET_SOURCE_FILES_PROPERTIES(hello.i PROPERTIES CPLUSPLUS ON)
SET_SOURCE_FILES_PROPERTIES(hello.i PROPERTIES SWIG_FLAGS "-includeall")
SWIG_ADD_MODULE(hello java hello.i hello.cpp)
SWIG_LINK_LIBRARIES(hello ${Java_LIBRARIES} ${JNI_LIBRARIES} ${CMAKE_CURRENT_SOURCE_DIR})
hello.cpp
#include "hello.hpp"
int adding(const int x, const int y)
{
return y + x;
}
hello.hpp
int adding(const int x, const int y);
hello.i
%module hello
%{
#include "hello.hpp"
%}
int adding(const int x, const int y);
Can anyone tell me what I"m doing wrong when I'm creating the dynamic library? Thank you for the assistance.
The reason why I know this is due to this error message in eclipse
Exception in thread "main" java.lang.UnsatisfiedLinkError: hello.helloJNI.adding(II)I
at hello.helloJNI.adding(Native Method)
at hello.hello.adding(hello.java:14)
at hello.Main.main(Main.java:14)
Which is the same kind of error message that the docs talk about.
The missing symbol in your error message is part of the JNI wrapper, not part of your library itself.
Usually this means that you've not called System.loadLibrary() for the native part of the SWIG module, before making the first call into it. Your CMake file looks like you've correctly linked the implementation, so it's not the error case you referred to from the documentation.
As well as manually calling:
System.loadLibrary("hello"); // The name of your DLL here
from your main method I like to use the following in my SWIG interface files when I'm targeting Java:
%pragma(java) jniclasscode=%{
static {
try {
System.loadLibrary("hello"); // The name of your DLL here
} catch (UnsatisfiedLinkError e) {
System.err.println("Native code library failed to load. \n" + e);
System.exit(1);
}
}
%}
This causes the native library to be loaded automatically before it is needed, which seems most natural to Java programmers.
I need to port a CPP project to Android but I somehow got stuck because of the following things that I am not sure of:
Do I need to use some kind of java wrapper for my CPP project at all, i.e is it necessarily that I use Android SDK to integrate my application with Android? If there is another way, which one would that be?
I have seen some people claiming they have been able to manipulate their cmake file and some custom android-cmake toolchain to build their “.so” or eventually an “.apk” from their project. Would it be possible without a java wrapper to manipulate the cmake files of the cpp project to build your project? (source: Build Android NDK project with Cmake)
From my experience, I would go with using the android java entry point, otherwise you will most likely bump into problems (you can make full native android apps , but I strongly advise against it).
One of the reasons is that you will want SDK function calls from inside your CPP, and using reflection on the java environment from CPP isn't trivial.
The steps would be the following :
Create a very simple C code that will server as bridge to your CPP code . Here are some sample C functions that I've commonly used :
OnApplicationStart
OnApplicationPaused
OnApplicationResumed
OnApplicationUpdate
OnTouchReceived
Export these functions and load them in your Java code (lookup JNI on how to do this)
Handle all Android-specific actions in Java, and all application specific actions in cpp
So the answer to your 1st question is that a java wrapper isn't mandatory, but it's HIGHLY recommended.
To answer your 2nd question :
Yes, you can use cmkae, ant, cygwin , allot of command tools that will end up creating your apk (It's up to you on how you feel comfortable).You will still use the android compiler since you are targeting arm.
The difference is that you need to change in your manifest.xml the entry point of the application from a normal activity to a native activity ( http://developer.android.com/reference/android/app/NativeActivity.html ) .
As you can see, the difference isn't the build system, it's the way you define your entry point.
As a friendly advice, try using the minimal java wrapper approach. You might get to better results sooner, and it won't take you more then 1 week of research on the web on how to link java code to cpp.
EDIT :
As of demand, I will shortly explain how I would approach the process of porting a CPP application to Android :
Rethink your application to work as a shared library , using a C entry point :
a.Create a simple C application that will load yourCPPApp.dll (or .so if you are on linux)
b. In your .dll create the minimum necessary extern "C" functions to be exported in order for you to give the necessary information to your dll
For simplicity, we'll assume we have 3 methods :
void StartApplication();
bool OnApplicationUpdate();
void OnScreenTouched(int x, int y);
c. Implement the simple C project that will make the calls to these methods externaly (so the .exe will call the methods from the .dll ! )
Sample code :
#include "myCPPapp.h"
int main(int arg, char** argv)
{
StartApplication();
srand(time(NULL));
while (OnApplicationUpdate())
{
// we assume we have a 480x640 resolution
OnScreenTouched(rand()%480,rand()%640);
}
return 0;
}
Now that we have things working in full native with a .exe and a .dll , time to make it work with a .apk and a .so
a. Rename the exposed methods from myCppApp into java compatible prototypes
extern "C" {
JNIEXPORT void JNICALL Java_com_sample_nativebridge_OnApplicationStart(JNIEnv env, jobject thiz);
JNIEXPORT jboolean JNICALL Java_com_sample_nativebridge_OnApplcationUpdate(JNIEnv env, jobject thiz);
JNIEXPORT void JNICALL Java_com_sample_nativebridge_OnScreenTouched(JNIEnv env, jobject thiz, jint x, jint y);
}
b. create the java class nativebridge (case sensitive) in the package com.sample (you need t respect the names in order for correct linkage to native)
class nativebridge {
public static native void OnApplicationStart();
public static native boolean OnApplicationUpdate();
public static native void OnScreenTouched(int x, int y);
}
c. add the load library statement in the native bridge in order to have your library loaded at runtime
class nativebridge {
....
static {
System.loadLibrary("myCppApp");
// notice that the lib prefix and .so sufix aren't part of the name
}
}
d. compile your CPP code with the armeabi compiler and create libmyCPPApp.so (ndk build system , or whatever you'd like... i would go with the ndk, it's quite easy , just go into the folder with Android.mk and call $ANDROID_NDK_BUILD_PATH/build )
At this point you will need to createa a .mk file that will compile your myCppApp code, but this is out of scope, you will need to do this research on your own (it's quite trivial once you get the hang of it).
c. use the native methods from the bridge inside your java app wherever you see fit.
A very good tip would be to go through a hello world sample of ndk :
http://www.ntu.edu.sg/home/ehchua/programming/android/android_ndk.html
Enjoy.
I am trying to call a simple native method in Java from C++, to achieve this I do the following:
Create a simple class as shown below:
public class HelloNative{
public native void printString(String str);
static{
System.out.println("Current Directory is: " + System.getProperty("user.dir"));
System.load(System.getProperty("user.dir") + "/libhellonative.so");
}
public static void main(String[] args){
System.out.println("Calling Native Libraray (libhellonative.so) method printString");
new HelloNative().printString("Message from Java to C");
}
}
Create .h file for the native method using javah -jni which create the following declaration:
JNIEXPORT void JNICALL Java_HelloNative_printString(JNIEnv *, jobject, jstring);
Implement the native function in .cpp file as:
JNIEXPORT void JNICALL Java_HelloNative_printString(JNIEnv* jni_env, jobject java_obj, jstring msg){
printf("inside native method\n");
jboolean iscopy;
const char *message = (jni_env)->GetStringUTFChars( msg, &iscopy);
printf("%s", message);
}
And finally create the .so file using:
g++ HelloNative.cpp -o libhellonative.so -shared -Wl,-soname,libhellonative.so -static -lc -I /usr/lib/jvm/java-6-sun-1.6.0.26/include -I /usr/lib/jvm/java-6-sun-1.6.0.26/include/linux
But when I compile and run the .java file it's giving me Runtime Exception:
Current Directory is: /home/gmuhammad/Projects/test
Calling Native Libraray (libhellonative.so) method printString
Exception in thread "main" java.lang.UnsatisfiedLinkError: HelloNative.printString(Ljava/lang/String;)V
at HelloNative.printString(Native Method)
at HelloNative.main(HelloNative.java:16)
Ok, I got this to work. It has nothing to do with loading the library, but with actually calling the method from that library.
I created .java, .h and .cpp files by copy/paste from your question and ran them (I had to add #include <jni.h> in the .cpp file) - and got exactly the same error as you did.
Then I edited the .cpp file to include the generated .h file. Also, as maba indicated in his answer, you need to call (jni_env)->ReleaseStringUTFChars(msg, message); to release the object. My full .cpp file now looks like this:
#include "HelloNative.h"
JNIEXPORT void JNICALL Java_HelloNative_printString(JNIEnv* jni_env, jobject java_obj, jstring msg){
printf("inside native method\n");
jboolean iscopy;
const char *message = (jni_env)->GetStringUTFChars( msg, &iscopy);
printf("%s", message);
(jni_env)->ReleaseStringUTFChars(msg, message);
}
I re-compiled the library, ran the java code - and voila! everything works. This way, it works regardless of which way you load the library, with load or loadLibrary.
Give it a try.
I recommend to load the library slightly differently.
Place your libmynative.so into whatever directory you like.
Add that directory to LD_LIBRARY_PATH variable.
In your java code, change System.load call to be System.loadLibrary("mynative") (note the lack of full path, prefix lib and extension .so)
Run your java code.
Have a look here for more details: http://java.sun.com/developer/onlineTraining/Programming/JDCBook/jniexamp.html
It is totally OK to use the System.load() but you will have to be sure that your shared libraries really are where you say they should be.
Current Directory is: /home/gmuhammad/Projects/test
Your code is trying to access the shared library in that directory. Are you sure that the libhellonative.so is there?
Then you can also use the System.mapLibraryName("hellonative") to get the name of the shared library for the current platform. That makes it more platform independent. The call will give you libhellonative.so on linux and hellonative.dll on windows.
Also you must call (jni_env)->GetReleaseUTFChars(msg, message); to release the objects.