【问题标题】:Reproducing and resolving Android java.lang.unsatisfiedLinkError locally在本地重现和解决 Android java.lang.unsatisfiedLinkError
【发布时间】:2019-10-07 10:12:05
【问题描述】:

我和朋友一起创建了一个 Android 应用来组织学校成绩。该应用程序在我的设备和大多数用户设备上运行良好,但崩溃率超过 3%,主要是因为 java.lang.UnsatisfiedLinkError 并且发生在 Android 版本 7.0、8.1 和 9 上。

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

我在这里有点迷茫,因为我已经尝试了几件事,但到目前为止,我既无法减少出现次数,也无法在我的任何设备上重现它。任何帮助将不胜感激。

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

不幸的是,这并没有减少由于java.lang.UnsatisfiedLinkError 而导致的崩溃数量。我继续我的在线研究,发现this article,这表明问题出在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)。


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

另外,这里是入口点的 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

【问题讨论】:

  • 你能发布你的 Wrapper.java 吗?
  • @Zaartha 我刚刚编辑了我的帖子并添加了 Wrapper.java 以及入口点的 cpp 端。提前感谢您的帮助!
  • 不确定这是否有帮助 developer.android.com/topic/performance/…,将其设为 android:extractNativeLibs="true" 看看效果如何。
  • 请把pluspoints.h的内容也贴出来。
  • @luckingging3er 似乎没有关于 Android 版本的模式,它发生在迄今为止所有受支持的版本上:5、6、7、8 和 9。谢谢你的链接,是的,我已经看过那个了。将再次解决它,以确保我以预期的方式得到了一切。

标签: java android java-native-interface unsatisfiedlinkerror android-app-bundle


【解决方案1】:

UnsatisfiedLinkError 发生在您的代码尝试调用由于某种原因不存在的 smth 时:post about it

这是使用 multidex 应用程序的潜在原因之一:

如今,几乎每个 Android 应用都使用 Multidex 来在其中包含更多内容。在构建 DEX 文件时,构建工具会尝试了解开头和puts them to the main dex 需要哪些类。但是,它们可能会错过任何东西,尤其是在绑定 JNI 时。

您可以尝试手动将 Wrapper 类标记为主 DEX 中的要求:docs。如果您有一个 multidex 应用程序,它也可以帮助它带来其依赖的本机库。

【讨论】:

  • 感谢您的帮助。我不是 100% 确定我们的应用程序是否使用 Multidex,至少我们没有将它包含在 app/build.gradle 的依赖项中。但正如我上面所描述的,我使用 Android 应用程序包将应用程序发布到 Play 商店 - 可能这使用了 multidex?根据google dev page,应用程序包中有一个 dex 文件夹。但我还没有弄清楚,我需要在哪里将 Wrapper 类标记为 required s。吨。 app bundler 会理解的。让我研究一下。
【解决方案2】:

您的两个本机方法在 Java 中声明为 static,但在 C++ 中,相应的函数使用属于类型 jobject 的第二个参数声明。

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

【讨论】:

  • 感谢您指出这一点,将解决该问题并让您知道结果。
【解决方案3】:

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

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)?毕竟,根据你粘贴的代码,trace应该涉及executeEndpoint(String, HashMap&lt;String, Serializable&gt;)

可能是本地方法的查找失败,因为字符串不再匹配。这只是一个建议——我不明白为什么只有 3% 的手机会失败。但是我之前遇到过这个问题。

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

如果它与proguarding有关,那么您将需要将规则添加到项目中。请参阅此链接以获取建议:In proguard, how to preserve a set of classes' method names?

另一件事,快速检查可用于防止难看的崩溃 - 在启动时添加是否可以解决后来导致 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;
 }

如果你得到一个否定的结果,警告用户一个问题,然后优雅地失败。

【讨论】:

    【解决方案4】:

    如果 3% 的用户在使用 64 位处理器的设备上遇到应用崩溃,那么 你应该see this post on Medium

    【讨论】:

      【解决方案5】:

      这不太可能与 proguard 相关 - 并且提供的代码完全无关紧要。 build.gradle 和目录结构将是唯一需要知道的事情。在编写 Android 7、8、9 时,这很可能与 ARM64 相关。该问题还具有相当不准确的假设,即 ARM64 将能够运行 ARM 本机程序集......因为只有将 32 位本机程序集放入 armeabi 目录时才会出现这种情况;但它会在使用armeabi-v7a 目录时抱怨UnsatisfiedLinkError。当能够为 ARM64 构建并将 ARM64 本机程序集放入 arm64-v8a 目录时,这甚至不是必需的。

      如果这应该与应用程序包相关(我刚刚注意到内容标签),则似乎 ARM64 的本机程序集已被打包到错误的包部分中 - 或者 ARM64 平台没有与该程序集一起交付.建议不要重新链接太多,但仔细检查 实际上 a) 已打包和 b) 正在交付到 ARM64 平台。那些模型链接失败的 CPU 可能也很有趣,只是看看是否有任何模式。

      获得这些有问题的模型中的任何一个,无论是硬件形式还是基于云的仿真器(最好在真实硬件上运行),至少在测试时重现问题可能是最容易的。查找模型,然后去 eBay,搜索“2nd hand”或“refurbished”...您的测试可能无法重现问题,因为没有从 Play 商店安装捆绑包。

      【讨论】:

      【解决方案6】:

      要在本地重现此内容,您可以将 apk[x86 apk 到 arm 设备或反之亦然或跨架构] 侧加载到您的手机。 通常用户可能会使用 ShareIt 等工具在手机之间传输应用程序。 这样做后,共享手机的架构可能会有所不同。 这是奇怪的未满足链接异常的主要原因。

      有一种方法可以缓解这种情况。 Play 有一个 api 来验证是否通过 PlayStore 进行了安装。 通过这种方式,您可以限制通过其他渠道进行的安装,从而减少不满意的链接异常。

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

      【讨论】:

        【解决方案7】:

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

        在用户已将应用移至 SD 卡的部分 LG 设备或旧三星设备上会发生这种情况。

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

        https://github.com/KeepSafe/ReLinker

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

        您还可以在 gradle.properties 文件中保留 android.bundle.enableUncompressedNativeLibs=false。但它会增加 Play Store 上的应用下载大小以及磁盘大小。

        【讨论】:

          猜你喜欢
          • 2011-03-16
          • 2015-05-31
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-03-04
          • 1970-01-01
          相关资源
          最近更新 更多