在本地复现和解决Android java.lang.unsatisfiedLinkError问题

29

我和朋友一起创建了一个Android应用程序来组织学校成绩。该应用程序在我的设备和大多数用户设备上都可以正常工作,但是有超过3%的崩溃率,大多数是由于 java.lang.UnsatisfiedLinkError 在Android版本7.0、8.1以及9上发生。

我已经在我的手机和几个模拟器上测试了这个应用程序,包括所有架构。我将应用程序上传到应用商店作为android-app-bundle,并怀疑这可能是问题的源头。

我有点困惑,因为我已经尝试了几件事情,但到目前为止,我既没有能够减少出现次数,也没有在任何设备上重现它。非常感谢任何帮助。

我找到了这个资源,它指出Android有时无法解压外部库。因此,他们创建了一个ReLinker库,它将尝试从压缩的应用程序中获取库:

不幸的是,这并没有减少因java.lang.UnsatisfiedLinkError而导致的崩溃数量。我继续在网上研究,并发现了这篇文章,它表明问题在于64位库。因此,我删除了64位库(该应用程序仍可以在所有设备上运行,因为64位架构也可以执行32位库)。但是,错误仍以与之前相同的频率发生。

通过谷歌播放控制台,我得到了以下崩溃报告:

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)。


2019年6月4日编辑

按要求,这里是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);
}

此外,这里是入口点的C++定义,然后调用我们的核心库:

#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

我认为你可能只是忘记了调用loadLibrary https://dev59.com/oHM_5IYBdhLWcg3wfTI0#1401665 试一下这个 - luckyging3r
你能确定它是否仅发生在特定 API 级别的设备上吗? - luckyging3r
我假设你已经看过了https://developer.android.com/training/articles/perf-jni#native-libraries。 - luckyging3r
1
@luckyging3er,似乎没有关于Android版本的模式,迄今为止在所有支持的版本上都发生了:5、6、7、8以及9。感谢提供的链接,是的,我已经看过了。不过我会再花些时间仔细研究一下,确保我理解得正确。 - Philip
错误仍未解决。如果有人在这方面有经验或其他的指针,那将非常有帮助。如果有帮助的话,我很乐意发布更多信息。感谢您提前的支持。 - Philip
显示剩余8条评论
7个回答

2

UnsatisfiedLinkError是当你的代码试图调用某些原因不存在的东西时发生的错误:有关此错误的文章

以下是多dex应用程序的潜在原因之一:

现在几乎每个Android应用都使用Multidex来包含更多内容。在构建DEX文件时,构建工具尝试了解哪些类在启动时是必需的,并将其放入主dex中。但是,它们可能会遗漏某些内容,特别是在绑定JNI时。

您可以尝试手动将Wrapper类标记为必须在主DEX中:文档。如果您有一个多dex应用程序,则可以帮助它同时带来其依赖的本机库。


感谢您的帮助。我不确定我们的应用程序是否使用Multidex,至少我们没有将其包含在app/build.gradle的依赖项中。但正如我上面所述,我使用Android应用程序包将应用程序发布到Play商店 - 可能会使用multidex?根据Google开发页面,应用程序包中有一个dex文件夹。但是我还没有找出需要在哪里标记Wrapper类为必需,以便应用程序打包器能够理解。让我研究一下。 - Philip

0

从你报告的异常中查看调用堆栈:

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%的手机会失败。但我以前遇到过这个问题。

首先,在禁用所有混淆后进行测试。

如果与proguarding有关,则需要向项目添加规则。请参见此链接以获取建议:在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

你的两个本地方法在Java中被声明为static,但是在C++中相应的函数被声明为第二个参数属于类型jobject

将类型更改为jclass应该有助于解决你的问题。


谢谢指出,我会修复并告诉您结果。 - Philip

0

这个问题可能与 https://issuetracker.google.com/issues/127691101 有关。

它发生在一些LG设备或旧的三星设备上,用户将应用程序移动到SD卡中。

解决该问题的一种方法是使用Relinker库来加载您的本地库,而不是直接调用System.load方法。对于我的应用程序用例,它确实有效。

https://github.com/KeepSafe/ReLinker

另一种方法是阻止应用程序移动到SD卡。

您还可以在gradle.properties文件中保留android.bundle.enableUncompressedNativeLibs=false。但这将增加Play商店上的应用程序下载大小以及其磁盘大小。


0
如果有3%的用户在64位处理器设备上遇到应用程序崩溃问题,则您应该查看此 Medium 帖子

0

这与Proguard无关,提供的代码也不太相关。唯一需要知道的是build.gradle和目录结构。在编写Android 7、8、9时,这很可能与ARM64有关。问题还包括一个相当不准确的假设,即ARM64能够运行ARM本地汇编...因为只有将32位本地汇编放入armeabi目录中时才会出现这种情况;但是如果使用armeabi-v7a目录,则会报错UnsatisfiedLinkError。当能够为ARM64构建并将ARM64本地汇编放入arm64-v8a目录时,甚至不需要这样做。

如果这与应用程序包相关(我刚刚注意到内容标签),则很可能是将ARM64本地汇编打包到了错误的包部分中,或者ARM64平台未随该汇编一起交付。建议不要重新链接,而是仔细检查实际上已经打包了什么,并且正在传递给ARM64平台的内容。那些无法链接的型号使用的CPU也可能很有趣,只是为了看看是否存在任何模式。

获取这些有问题的模型,无论是硬件还是基于云的模拟器(最好在真实硬件上运行),可能是测试时至少复制问题最容易的方法。查找这些模型,然后去eBay搜索“二手”或“翻新”...你的测试可能未能复制该问题,因为没有安装Play Store中的捆绑包。


非常感谢您的解释和建议。 根据Google的说法,armeabi在r16中已被“弃用”,并在r17中被删除。 此外,从2019年8月开始,Google将不允许不支持64位架构的应用发布。 因此,创建一个“仅32位”的应用程序并没有太大意义,我只是想尝试一下,看看是否仍然会出现错误。 我将删除发布版本的重新链接和优化,看看是否有所帮助。 - Philip

0
要在本地重现此操作,您可以将apk[ x86 apk到arm设备或反之亦然或跨架构]侧载到手机上。 通常用户可能会使用诸如ShareIt之类的工具在手机之间传输应用程序。 在这样做时,共享手机的架构可能会不同。 这是奇怪的未满足链接异常的主要原因。
但是有一种方法可以缓解这种情况。 Play有一个api来验证是否通过PlayStore进行了安装。 这样,您就可以限制通过其他渠道安装,并因此减少未满足链接异常。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接