Android JNI : C++调用Java

转载请标明出处:https://blog.csdn.net/qq_29621351/article/details/79870319

    通过这段时间接触JNI的过程,觉得JNI里面的坑还是挺多的,我接触的还是很少,有的地方理解的也不是很周到,发现理解错的地方,也请大家能够指出,我会立即改正,如果没接触过JNI有疑问的话,请随时提问,我会尽我所能为大家解释。

JNI可以理解为三层:Java层,JNI层,C++层。

    Java层就是Java语言编写的程序,C++层是纯粹的C/C++语言,而JNI层是函数映射层类似于下面这种函数形式,这种形式的函数对应Java中的一个具体函数,如果对应Java中的具体函数是静态函数则第二个参数就和下面一样,如果是非静态函数则第二个参数换为jobject obj。

JNIEXPORT jint JNICALL someMethod(JNIEnv* env, jclass cls,...) // ...表示其它参数,而不是变长函数参数。

JNI层直接调用Java

    如果想在JNI层直接调用Java层是很方便的,JNIEnv* env这个类型的变量直接提供了调用Java函数的方式,JNIEnv的含义是Java虚拟机的执行环境,可以通过它来操纵Java层中的类方法、对象方法,而jclass表示Java中的类,JNI层映射函数传入的jclass和jobject表示该方法的类实例或对象实例,简单的调用方法如下所示:

jclass clazz = env -> FindClass("utils/LogWriter");
jmethodID mid = env -> GetStaticMethodID(clazz,"aMethod","(Ljava/lang/String;Ljava/lang/String;)V");
env -> CallStaticVoidMethod(clazz,mid, strArgs1,strArgs2);

步骤1:表示获得Java虚拟机中的类,参数"utils/LogWriter"表示Java中一个自定义的类,也可以是Java类库中的一个类,包名的使用类定义文件中开头 package 后面的路径加上"/类名"。简单地说就是跟 import 这个类所带有的参数是一样的。

步骤2:在获取Java中的一个类后,要继续得到这个类的方法,通过得到的类clazz和环境变量env、方法名aMethod来得到方法的id,类型为jmethodID,还要描述方法返回值和传入参数,本例中的描述方式以"(Ljava/lang/String;Ljava/lang/String;)V"表示,读者可自行查阅描述方式的规则。

步骤3:通过环境变量env调用Java的方法,CallStaticVoidMethod为调用静态方法的方式。strArgs1和strArgs2为Java方法传入参数,在传入之前先自己设定其值。

主线程中的C++层调用Java

    对于C++来讲在主线程中调用Java和在子线程中调用Java的方式是不完全一样的,对C++层来说,一般不会有传入的JNIEnv*,只能在需要调用Java的地方获取,获取时用到了C++中的一些函数,想要使用这些函数,先要导入"jni.h",

    既然没有JNIEnv*的变量那就只能自己获取,因为它是调用Java的唯一途径,获取的方式是通过JavaVM* vm获取的,JavaVM和JNIEnv一样都是JNI机制中非常重要的一个变量,JavaVM表示Java虚拟机,但是JavaVM* vm是在什么地方获取的?这里有一个误区,网上的很多博客都说用JNI_CreateJavaVM函数,这在纯粹的Java语言中是可以的,但是在Android中不能这么使用,因为Java语言在一个进程中可以创建多个虚拟机变量,而在Android虚拟机在一个进程中只能创建唯一一个虚拟机变量,并且因为Android在运行的时候就已经创建了一个虚拟机变量,所以绝不会允许再创建一个。不能创建虚拟机变量但是可以设置指针指向已存在的虚拟机。我们可以使用

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)

    这个函数是一个系统调用,我们在Java中执行System.loadLibrary("xxx.so")函数时,该函数就会自动被调用,并且调用参数中有一个JavaVM*类型的指针。我们可以通过一个全局变量把它保存下来,并用它创建JNIEnv*,然后再使用JNIEnv*。这种方式的过程如下(注意这个函数一定要返回一个JNI版本的宏):

JavaVM* jvm = nullptr;
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if(vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    return JNI_VERSION_1_6;
}

然后在其它函数中获取JNIEnv*变量

JNIEnv* env = nullptr;
if(jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
{
    return JNI_ERR;
}

    此函数虽然定义为JNI层函数的模式,但是写在C++层中也是没有问题的。获取JNIEnv*变量后我们就可以以JNI层一样的方式来调用Java语言,还有就是这里获得JNIEnv*变量是通过jni.h中提供的GetEnv函数获得的。

子线程中的C++层调用Java

    在子线程中调用Java,又是不一样的情况。但无论如何,只要想调用Java,就绝对少不了JNIEnv*变量的存在,事实上,也正是因为JNIEnv*变量的某种特性导致了在子线程环境中调用方式与主线程的不同。

    刚才讲到,在android中每个进程只能有一个虚拟机变量,所以不能再创建,但是可以定义多个虚拟机指针指向同一个虚拟机,JNIEnv*变量也有点相似,可以定义多个指针指向虚拟机环境,但是JNIEnv还有一个特性,它是附着在具体线程之上的,在主线程中获取JNIEnv*变量是需要调用JavaVM调用GetEnv方法,但是在子线程中要使用AttachCurrentThread方法,否则无效。

    在子线程下获取JNIEnv*变量后,如果你在C++中调用的Java本身的类库,那可能不会有问题,但若调用自定义的类,会出现找不到类的错误,因为Java虚拟机的类加载器并没有把自定义的类加载给子线程,解决的方案是我们需要在主线程中找到这个类,并把它加载为全局类,然后通过类获得方法ID,之后在子线程中使用这个全局类,如下:

JavaVM* jvm = nullptr;
jclass clazz = nullptr;
jclass global_clazz = nullptr;
jmethodID mid_static_method;

/*  executed after System.loadLibrary("xxx.so") */
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if (vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    clazz = env -> FindClass("utils/JJLogWriter");
    if (clazz == nullptr)
    {
        return JNI_ERR;
    }
    global_clazz = (jclass)env->NewGlobalRef(clazz);
    if (global_clazz == nullptr) 
    {
        return JNI_ERR;
    }
    mid_static_method = env -> GetStaticMethodID(global_clazz,"c","(Ljava/lang/String;Ljava/lang/String;)V");

    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    env -> DeleteGlobalRef(global_clazz);
//    env -> DeleteLocalRef(mid_static_method);
    return;
}

在这里调用了一个stoJstring函数,它是将const char*类型先转换为jbytearray类型,又将jbytearray类型转换为jstring类型,这样做是为了避免emoji类型的字符使jni.h中提供的NewStringUTF函数崩溃。stoJstring函数的定义如下。

jstring stoJstring(JNIEnv* env, const char* pat)
{
    jclass strClass = env->FindClass("java/lang/String");

    jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");

    jbyteArray bytes = env->NewByteArray(strlen(pat));

    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);

    jstring encoding = env->NewStringUTF("utf-8");

    jstring finalJstring = (jstring)env->NewObject(strClass, ctorID, bytes, encoding);

    env -> DeleteLocalRef(strClass);

    env -> DeleteLocalRef(bytes);

    env -> DeleteLocalRef(encoding);

    return finalJstring;
}

 以下是我当时写的一个demo,贴出来方便自己以后随意查看

/*
 * created by txc
**/
JavaVM* jvm = nullptr;
jclass clazz = nullptr;
jclass global_clazz = nullptr;
jmethodID mid_static_method;
/*  executed after System.loadLibrary("xxx.so") */

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved)
{
    if (vm == nullptr)
    {
        return JNI_ERR;
    }
    jvm = vm;
    JNIEnv* env = nullptr;
    
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    clazz = env -> FindClass("utils/JJLogWriter");
    if (clazz == nullptr)
    {
        return JNI_ERR;
    }
    global_clazz = (jclass)env->NewGlobalRef(clazz);
    if (global_clazz == nullptr) 
    {
        return JNI_ERR;
    }
    mid_static_method = env -> GetStaticMethodID(global_clazz,"c","(Ljava/lang/String;Ljava/lang/String;)V");

    return JNI_VERSION_1_6;
}

JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved)
{
    JNIEnv* env = nullptr;
    if (jvm->GetEnv((void **)&env, JNI_VERSION_1_6)!=JNI_OK)
    {
        return JNI_ERR;
    }
    env -> DeleteGlobalRef(global_clazz);
    return;
}

jstring stoJstring(JNIEnv* env, const char* pat)
{
    jclass strClass = env->FindClass("java/lang/String");

    jmethodID ctorID = env->GetMethodID(strClass, "<init>", "([BLjava/lang/String;)V");

    jbyteArray bytes = env->NewByteArray(strlen(pat));

    env->SetByteArrayRegion(bytes, 0, strlen(pat), (jbyte*)pat);

    jstring encoding = env->NewStringUTF("utf-8");

    jstring finalJstring = (jstring)env->NewObject(strClass, ctorID, bytes, encoding);

    env -> DeleteLocalRef(strClass);

    env -> DeleteLocalRef(bytes);

    env -> DeleteLocalRef(encoding);

    return finalJstring;
}

void JJLog2Java(const char* location,const char* cstr)
{
    JNIEnv* env = NULL;
    jstring strArgs = NULL;
    jstring strLocation = NULL;
    if (jvm->AttachCurrentThread((void**)&env, NULL))
    {
        return;
    }
    strArgs = stoJstring(env,cstr);
    strLocation = env -> NewStringUTF(location);
    env -> CallStaticVoidMethod(global_clazz,mid_static_method, strLocation,strArgs);
    env -> DeleteLocalRef(strArgs);
    env -> DeleteLocalRef(strLocation);
    jvm->DetachCurrentThread();
}

    此外,在JNI程序的编写中要时刻注意内存泄漏问题,不仅是C/C++语言本身的内存泄漏,JNI还有自己的内存管理方式,稍有不当就会造成内存泄漏,这里推荐一篇JNI内存泄漏的文章,讲得非常透彻。

JNI内存泄漏问题参考:https://www.ibm.com/developerworks/cn/java/j-lo-jnileak/



猜你喜欢

转载自blog.csdn.net/qq_29621351/article/details/79870319
今日推荐