web-dev-qa-db-ja.com

Android Java.lang.unsatisfiedLinkErrorをローカルで再現および解決する

友達と一緒にAndroidアプリを作成して学校の成績を整理しました。このアプリは私のデバイスとほとんどのユーザーデバイスで正常に動作しますが、クラッシュ率は3%を超えています。 Java.lang.UnsatisfiedLinkErrorおよびAndroidバージョン7.0、8.1、および9で発生します。

私は自分の電話といくつかのエミュレーター(すべてのアーキテクチャーを含む)でアプリをテストしました。私はアプリをAndroidアプリバンドルとしてアプリストアにアップロードしましたが、これが問題の原因である可能性があります。

私はここで少し迷っています。すでにいくつかのことを試みたためですが、これまでのところ、発生回数を減らすことも、自分のデバイスで再現することもできませんでした。どんな助けも高く評価されます。

私は this resource を見つけましたAndroidは外部ライブラリの解凍に失敗することがあります。そのため、 ReLinkerライブラリ を作成して、圧縮されたアプリからライブラリをフェッチします。

残念ながら、これによりJava.lang.UnsatisfiedLinkErrorによるクラッシュの量は減りませんでした。オンライン調査を続けたところ、 この記事 が見つかりました。これは、問題が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
26
Philip

例外で報告したコールスタックを確認します。

_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%の電話で失敗する理由がわかりません。しかし、私は以前にこの問題に出くわしました。

まず、すべての難読化を無効にした後でテストします。

保護に関連している場合は、プロジェクトにルールを追加する必要があります。提案については、このリンクを参照してください。 プロガードでは、クラスのメソッド名のセットを保持する方法は?

もう1つは、見苦しいクラッシュを防ぐのに役立つクイックチェックです。起動時に、後で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
dr_g

ユーザーの3%が64ビットプロセッサを搭載したデバイスでアプリがクラッシュした場合は、 このMediumの投稿を参照 を実行する必要があります。

0
SnakeException

これをローカルで再現するには、電話機にapk [x86 apkからarmデバイスへ、またはその逆またはクロスアーキテクチャ]をサイドロードします。通常、ユーザーはShareItなどのツールを使用して電話間でアプリを転送します。その場合、共有電話のアーキテクチャは異なる場合があります。これは、奇妙な未解決のリンク例外の原因の大部分です。

ただし、これを軽減する方法があります。 Playには、インストールがPlayStoreを介して行われたかどうかを確認するためのAPIがあります。このようにして、他のチャネルを介したインストールを制限し、満たされていないリンク例外を減らすことができます。

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

0
Dinesh

2つのネイティブメソッドはJavaではstaticとして宣言されていますが、C++では対応する関数はjobject型に属する2番目のパラメーターで宣言されています。

タイプをjclassに変更すると、問題の解決に役立ちます。

0
Nghia Bui

これがプロガードと関係しているとは考えられません-提供されたコードはまったく無関係です。 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に移動し、「中古」または「再生品」を検索します。Playストアからバンドルをインストールしていないため、テストで問題を再現できなかった可能性があります。

0
Martin Zeitler