使用 JNI 在 Java 调用之间将本机对象保存在内存中

Posted

技术标签:

【中文标题】使用 JNI 在 Java 调用之间将本机对象保存在内存中【英文标题】:Keeping a native object in memory between calls from Java using JNI 【发布时间】:2016-01-19 19:09:14 【问题描述】:

我有一个 android 应用,需要使用 C 库。 我正在使用JNI 与之交互。该库使用一个结构(我们称之为foo)。 foo 使用一组初始参数,其中包括指向 C 函数的指针,它用于从我的应用程序请求更多数据,并将这些数据合并到计算过程中。一旦它拥有所需的一切,它就会通过C 回调函数返回一个结果,它还需要一个指向该函数的指针。我需要将所有这些C 回调函数挂钩到我的应用程序以从用户那里获取更多数据,将这些数据返回到foo,最后通过最终回调函数在我的应用程序中向用户显示结果。

我创建了foo_callbacks - 刚刚定义了静态C 函数,我在初始化时传递给foo,在这些函数中我使用JNI 再次调用我的应用程序(尚未对此进行测试,但我还保留对jvm 的引用并从中获取JNIEnv 并附加到当前线程like this)。

但是发生了什么:

    调用JNI 以使用来自foo_callbacks 的指向静态函数的指针初始化foo。我保留了对foo 的静态引用。 单独调用 JNI 使用现有的 foo 对象启动计算过程 但是当foo需要使用我之前传递的回调函数时,我得到了这个消息:A/libc: Fatal signal 11 (SIGSEGV), code 1, fault addr 0x4 in tid 14244

在谷歌上搜索时,似乎我正在尝试访问isn't owned by my app anymore 的内存。所以我认为那些对回调函数的引用不再有效。所以我的问题是,如何在JNI 调用之间将本机对象保留在内存中?还是有另一种方法来解决这个问题?谢谢。

这里有一些示例代码:

FooManager.java

...
static 
    System.loadLibrary("FooLib");


//initialized Foo library
private native int fooLibInit();

//start the process 
public native int fooStart(String message);

//continue the process after a delay 
public native int fooContinue(String message);

//retrieve milliseconds to schedule next event
public native long fooGetMsToNextEvent();

//method that gets called from native code
public static long getCurrentTime()
    return System.currentTimeMillis();


//method that gets called from native code, returning results
public static void deliverResult(String result)
    //display result to the user


...

FooScheduler.java

...
public static void kickItOff(String message)
    FooManager.fooLibInit();
    long timeToWait = FooManager.fooGetMsToNextEvent();
    //this call figures out what step it is and gets some data
    SchedulerUtil.scheduleCallBack(timeToWait);


//this is a callback function that gets called after given about of time by SchedulerUtil
public static void callBack(int step, String message)
    if(step == 1)
        FooManager.fooStart(message)
    else FooManager.fooContinue(message);

...

FooLib.cpp

#include <string.h>
#include <jni.h>
#include <android/log.h>

extern "C" 
    #include "blackbox.h"
    #include "foo_wrapper.h"


extern "C" 

    static jboolean isJni();
    //struct defined in blackbox.h
    static foo foo_obj;

    JNIEXPORT jint
    JNI_OnLoad(JavaVM *vm, void *reserved) 
        //defined in foo_wrapper.h
        set_java_env(vm);
        return JNI_VERSION_1_6;
    

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooInit(JNIEnv * env, jobject obj)
        //foo_get_global_time_wrapper and foo_return_result_wrapper functions is defined in foo_wrapper.h.
        //those pointers are actually a member variables of foo_obj,
        //they gets assigned in the fooInit() so foo_obj can use them later. fooInit is defined in blackbox.h
        int resultInit = fooInit(&foo_obj, foo_get_global_time_wrapper, foo_return_result_wrapper);
        return resultInit;
    

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooStart(JNIEnv * env, jobject obj, jstring message)
        jboolean copy = isJni();
        const char *firstCharPointer = env->GetStringUTFChars(message, &copy);

        //here is where the foo_get_global_time_wrapper function is called, and
        //
        //I am getting A/libc: Fatal signal 11 (SIGSEGV) error.
        //
        //fooStart is defined in blackbox.h
        int resultCode = fooStart(&foo_obj, (uint8*)firstCharPointer, strlen(firstCharPointer));
        return resultCode;
    

    JNIEXPORT jint JNICALL
    Java_com_myapp_fooContinue(JNIEnv * env, jobject obj, jstring message)
        jboolean copy = isJni();
        const char *firstCharPointer = env->GetStringUTFChars(chunk, &copy);

        //here blackbox produces results based on the first and second messages that were passed in and calls foo_return_result_wrapper with results
        //fooContinue is defined in blackbox.h
        int resultCode = fooContinue(&foo_obj, (uint8*)firstCharPointer, strlen(firstCharPointer));
        return resultCode;
    


    static jboolean isJni()
        return JNI_TRUE;
    

foo_wrapper.c

#include "foo_wrapper.h"
#include <jni.h>
#include <string.h>

static JavaVM *JVM;

extern uint32 foo_get_global_time_wrapper() 
    JNIEnv *env;
    int result = (*JVM)->GetEnv(JVM, (void **) &env, JNI_VERSION_1_6);
    if (result != JNI_OK) 
        LOGI("couldnt get JVM.");
        return 1;
    

    jclass clazz = (*env)->FindClass(env, "com/myapp/FooManager");
    jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "getCurrentTime", "()J");
    long milliseconds;
    (*env)->CallStaticObjectMethod(env, clazz, mid, milliseconds);
    return milliseconds;


extern int foo_return_result_wrapper(const uint8 *start, uint16 length) 
    JNIEnv *env;
    int result = (*JVM)->GetEnv(JVM, (void **) &env, JNI_VERSION_1_6);
    if (result != JNI_OK) 
        LOGI("couldnt get JVM.");
        return 1;
    

    jstring result = //get jstring from parameters start and length;

    jclass clazz = (*env)->FindClass(env, "com/myapp/FooManager");
    jmethodID mid = (*env)->GetStaticMethodID(env, clazz, "deliverResult", "(LJava.lang.String;)J");

    jobject obj =   (*env)->CallStaticObjectMethod(clazz, mid, result);
    return 0;


extern void set_java_env(JavaVM *vm) 
    JVM = vm;

请注意,这不是经过测试的代码 - 它基本上是我正在尝试做的更简单的版本。

【问题讨论】:

C 中没有类之类的东西。你是指 C++ 类还是完全不同的东西? 这对一些人有帮助,但我仍然无法理解您的实际流程。一些代码可能会有所帮助。具体问题: 1. 鉴于它是 C 结构,不清楚如何“在 Java 中保持对 foo 的静态引用。2. 不清楚如何在本机端管理 foo 本身的内存。3. 不是清楚 foo_callbacks 到底是什么 - 那是一个文件还是某个结构? 4. 不太清楚你是如何开始调用你的 JVM - 我点击了链接,但细节很重要,所以完全正确 你是怎么做的可能很重要。 @Brick 够了,我稍后会发布示例代码 @Brick 好的,我已经添加了一些示例代码。 如果您查看 logcat 输出,您会发现堆栈跟踪。您可以使用ndk-stack 工具对其进行分析,并找到导致崩溃的确切代码行。 【参考方案1】:

这是服务旨在解决的问题。有三种风格: 与用户交互的前台服务;在后台做事的后台服务;和绑定服务(客户端/服务器),只要客户端应用程序绑定到它们就存在。将您的 JNI 代码实现为绑定服务,并在其上使用 Java 薄包装器;然后你就会得到你的坚持。

【讨论】:

【参考方案2】:

这是一个多线程问题。无法保证JNI 将在与Java 相同的线程上执行本机代码。使所有native 函数synchronized 解决了这个问题。

//initialized Foo library
private native synchronized int fooLibInit();

//start the process 
public native synchronized int fooStart(String message);

//continue the process after a delay 
public native synchronized int fooContinue(String message);

//retrieve milliseconds to schedule next event
public native synchronized long fooGetMsToNextEvent();

//method that gets called from native code
public static synchronized long getCurrentTime()
    return System.currentTimeMillis();


//method that gets called from native code, returning results
public static synchronized void deliverResult(String result)
    //display result to the user

【讨论】:

在 Dalvik 下,从 Java 语言代码调用的本机代码在同一个线程上运行。如果您看到一个线程踩到另一个线程,那是因为您的应用程序中有两个不同的线程,而不是一个线程在 JNI 调用中分裂。地址 0x4 的 SIGSEGV 看起来像一个空指针解引用;与记忆消失无关。 FWIW,您可以通过编写日志消息并查看线程 ID 轻松识别线程...adb logcat -v threadtime 如果您不想配置 Android Studio。我不知道为什么synchronized 会有所作为。

以上是关于使用 JNI 在 Java 调用之间将本机对象保存在内存中的主要内容,如果未能解决你的问题,请参考以下文章

如何跟踪从Java到C的JNI方法?

如何从java本机接口调用getStackTrace方法(jni)

无法在 JNI 中更新作业

JNI 通过多个 JNI 调用使 C++ 中的对象保持活动状态

使用挂起异常java.lang.ClassNotFoundException调用JNI GetMethodID

Android JNI之JAVA与C++对象建立对称关联(JNI优化设计,确保JNI调用的稳定性)