JNIEnv解析

Stella981
• 阅读 732

1.关于JNIEnv和JavaVM

 JNIEnv是一个与线程相关的变量,不同线程的JNIEnv彼此独立。JavaVM是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个 JavaVM,因此该进程的所有线程都可以使用这个JavaVM。当后台线程需要调用JNI native时,在native库中使用全局变量保存JavaVM尤为重要,这样使得后台线程能通过JavaVM获得JNIEnv。

native程序中频繁使用JNIEnv*和JavaVM*。而C和C++代码使用JNIEnv*和JavaVM*这两个指针的做法是有区别的,网上大部分代码都使用C++,基本上找不到关于C和C++在这个问题上的详细叙述。

在C中:

使用JNIEnv* env要这样      (*env)->方法名(env,参数列表)

使用JavaVM* vm要这样       (*vm)->方法名(vm,参数列表)

在C++中:

使用JNIEnv* env要这样      env->方法名(参数列表)

使用JavaVM* vm要这样       vm->方法名(参数列表)

上面这二者的区别是,在C中必须先对env和vm间接寻址(得到的内容仍然是一个指针),在调用方法时要将env或vm传入作为第一个参数。C++则直接 利用env和vm指针调用其成员。那到底C中的(*env)和C++中的env是否有相同的数据类型呢?C中的(*vm) 和C++中的vm是否有相同的数据类型呢?

为了验证上面的猜测,我们可以查看JNIEnv和JavaVM的定义。他们位于头文件jni.h。我开发JNI用的是android-5平台,下面是 $NDK\platforms\android-5\arch-arm\usr\include\jni.h的部分代码

[cpp] view plaincopy

  1. struct _JNIEnv;

  2. struct _JavaVM;

  3. #if defined(__cplusplus)

  4. typedef _JNIEnv JNIEnv; //C++使用这个类型

  5. typedef _JavaVM JavaVM; //C++使用这个类型

  6. #else

  7. typedef const struct JNINativeInterface* JNIEnv; //C使用这个类型

  8. typedef const struct JNIInvokeInterface* JavaVM; //C使用这个类型

  9. #endif

  10. struct JNINativeInterface

  11. {

  12. /****省略了的代码****/

  13. jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);

  14. /****省略了的代码****/

  15. jobject     (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);

  16. /****省略了的代码****/

  17. };

  18. struct _JNIEnv

  19. {

  20. const struct JNINativeInterface* functions;

  21. #if defined(__cplusplus)

  22. /****省略了的代码****/

  23. jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)

  24. { return functions->GetMethodID(this, clazz, name, sig); }

  25. /****省略了的代码****/

  26. jobject GetStaticObjectField(jclass clazz, jfieldID fieldID)

  27. { return functions->GetStaticObjectField(this, clazz, fieldID); }

  28. /****省略了的代码****/

  29. #endif /*__cplusplus*/

  30. };

  31. struct JNIInvokeInterface

  32. {

  33. /****省略了的代码****/

  34. jint (*GetEnv)(JavaVM*, void**, jint);

  35. jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);

  36. };

  37. struct _JavaVM

  38. {

  39. const struct JNIInvokeInterface* functions;

  40. #if defined(__cplusplus)

  41. /****省略了的代码****/

  42. jint GetEnv(void** env, jint version)

  43. { return functions->GetEnv(this, env, version); }

  44. jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)

  45. { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }

  46. #endif /*__cplusplus*/

  47. };

假如我们用C编码,宏__cplusplus没有定义,那么从最上面的宏#if defined(__cplusplus)可推断

JNIEnv    代表类型 const struct JNINativeInterface*

JavaVM   代表类型 const struct JNIInvokeInterface*

那么JNIEnv* env实际上等价于声明 const struct JNINativeInterface**  env

JavaVM* vm实际上等价于声明 const struct JNIInvokeInterface ** vm

因此要调用JNINativeInterface结构体内的函数指针就必须先对env间接寻址。

(*env)的类型是const struct JNINativeInterface*(指向JNINativeInterface结构体的指针),这时候可以用这个指针调用结构体的成员函数指针, (*env)-> GetMethodID(env, jclass, const char*, const char*)。同理可分析JavaVM* vm。

----------------------------------------------------------------------------------------------------------------------------------------------

假如我们用C++编码,宏__cplusplus有定义,那么从最上面的宏#if defined(__cplusplus)可推断

JNIEnv    代表类型 struct _JNIEnv

JavaVM   代表类型 struct _JavaVM

那么JNIEnv* env实际上等价于声明 struct _JNIEnv*  env

JavaVM* vm实际上等价于声明 struct _JavaVM* vm

要调用_JNIEnv结构体内的函数指针这直接使用env而不需间接寻址, env-> GetMethodID(jclass, const char*, const char*)。同理可分析JavaVM* vm。

现在可以回答刚才的猜测了,C中的(*env)类型是const struct JNINativeInterface*,C++中的env类型是struct _JNIEnv*,因此他们的数据类型不相同(虽然都是指针,但指向不同的结构体类型)。

我们再看结构体_JNIEnv(C++的JNIEnv所代表的类型),这个结构体内有一个成员const struct JNINativeInterface* functions,再仔细看_JNIEnv内定义的函数。当调用_JNIEnv内定义的函数时,其实就是通过functions这个指针调用 JNINativeInterface内的函数指针,因此_JNIEnv的成员方法是JNINativeInterface的同名成员函数指针的包装而 已,归根结底无论在C还是C++中其实都使用了JNINativeInterface结构体。这时调用JNINativeInterface的函数指针的 第一参数是this,在C++中this代表指向当前上下文对象的指针其类型是struct _JNIEnv*(即JNIEnv*)。同理可分析_JavaVM。

2.注册和注销native函数

C和C++注册native函数的方式大致上相同,下面给出具体的代码。

[cpp] view plaincopy

  1. /* JNINativeMethod数组的定义在C和C++中都一样*/

  2. static JNINativeMethod gMethods[] = {

  3. {

  4. "jobjectProcess",

  5. "(Lcom/example/hellojni/HelloJni$Student;Ljava/lang/Integer;)V",

  6. (void*)jobjectProcess

  7. }

  8. /*被省略掉的代码*/

  9. };

  10. jint JNI_OnLoad(JavaVM* vm,void* reserved)

  11. {

  12. JNIEnv* env = NULL;

  13. jint result=-1;

  14. if( (*vm)->GetEnv(vm,(void**)&env , JNI_VERSION_1_4) != JNI_OK)

  15. return result;

  16. jclass HelloJniClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni");

  17. /* C */

  18. jint r=(*env)->RegisterNatives(env, HelloJniClazz, gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));

  19. /* C++ */

  20. r=AndroidRuntime::registerNativeMethods(env,"com/example/hellojni/HelloJni",gMethods,NELEM(gMethods));

[cpp] view plaincopy

  1. /*或者env->RegisterNatives(HelloJniClazz, gMethods, sizeof(gMethods) / sizeof(JNINativeMethod));*/

  2. if(0 == r)

  3. //注册native函数成功

  4. else

  5. //注册native函数失败

  6. return JNI_VERSION_1_4;

  7. }

  8. void JNI_OnUnload(JavaVM* vm,void* reserved)

  9. {

  10. JNIEnv* env = NULL;

  11. if( (*vm)->GetEnv(vm,(void**)&env , JNI_VERSION_1_4) != JNI_OK)

  12. return;

  13. jclass HelloJniClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni");

  14. /* C */

  15. jint r=(*env)->UnregisterNatives(env,HelloJniClazz);

  16. /* C++ */

  17. jint r= env->UnregisterNatives(HelloJniClazz)

  18. if(r == 0)

  19. //注销native函数成功

  20. else

  21. //注销native函数失败

  22. }

C和C++中都可以通过JNIEnv的RegisterNatives函数注册,而C++还提供了 AndroidRuntime::registerNativeMethods,AndroidRuntime类的 registerNativeMethods方法也可以注册。

3. 在native中向LogCat输出调试信息

在C/C++编译单元头部加上

#include <android/log.h>

#define LOG_TAG "自定义一个字符串"

log.h声明了函数int __android_log_print(int prio, const char *tag,  const char *fmt, ...)我们就是用这个函数向LogCat输出信息的。

加入了头文件后还必须给链接器指定__android_log_print函数所在的库文件liblog.so,在Android.mk文件中加上一行

LOCAL_LDLIBS := -llog

在native函数中可以用如下语句输出了

__android_log_print(ANDROID_LOG_DEBUG,LOG_TAG,"name=%s,age=%d",”卢斌晖”,28);

第一个参数ANDROID_LOG_DEBUG是枚举常量,它在log.h中定义。

[cpp] view plaincopy

  1. typedef enum android_LogPriority

  2. {

  3. ANDROID_LOG_UNKNOWN = 0,

  4. ANDROID_LOG_DEFAULT,    /* only for SetMinPriority() */

  5. ANDROID_LOG_VERBOSE,

  6. ANDROID_LOG_DEBUG,

  7. ANDROID_LOG_INFO,

  8. ANDROID_LOG_WARN,

  9. ANDROID_LOG_ERROR,

  10. ANDROID_LOG_FATAL,

  11. ANDROID_LOG_SILENT,     /* only for SetMinPriority(); must be last */

  12. } android_LogPriority;

我们可以根据调试信息的不同类别而选用不同的枚举常量。

4.关于jclass

jclass代表JAVA中的java.lang.Class。我们看jclass的定义,下面给出$NDK\platforms\android-5\arch-arm\usr\include\jni.h的部分代码

[cpp] view plaincopy

  1. #ifdef __cplusplus

  2. /*Reference types, in C++*/

  3. class _jobject {};

  4. class _jclass : public _jobject {}; /*_jclass继承_jobject*/

  5. typedef _jclass*        jclass;

  6. #else

  7. /*Reference types, in C.*/

  8. typedef void*           jobject;

  9. typedef jobject         jclass;

  10. #endif

在C中jclass代表类型void*,在C++中代表类型_jclass*。因此jclass是指针,我们能够在log中输出jclass变量值。

    __android_log_print(ANDROID_LOG_DEBUG,"native函数中输出","地址=%p",jclass变量);

当多个native函数都需要使用同一个JAVA类的jclass变量时,不能够定义jclass类型全局变量并只对其赋初值一次然后在多次JAVA对native函数调用中使用这个jclass变量。不能企图以此方式来节约获得jclass变量的开销。

每次JAVA调用native都必须重新获得jclass,上次调用native所得到的jclass在下次调用native时再使用是无效的。下面是C代码

[cpp] view plaincopy

  1. static jclass StudentClazz; //全局变量

  2. jint JNI_OnLoad(JavaVM* vm,void* reserved)

  3. {

  4. JNIEnv* env = NULL;

  5. jint result=-1;

  6. if( (*vm)->GetEnv(vm,(void**)&env , JNI_VERSION_1_4) != JNI_OK)

  7. return result;

  8. StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student"); //初始化

  9. return JNI_VERSION_1_4;

  10. }

  11. JNIEXPORT void JNICALL jobjectProcess(JNIEnv *env, jobject instance,jobject student,jobject flag)

  12. {

  13. /*StudentClazz=(*env)->FindClass(env,"com/example/hellojni/HelloJni$Student");*/

  14. __android_log_print(ANDROID_LOG_DEBUG,"在jobjectProcess中输出","StudentClazz=%p",StudentClazz);

  15. nameFieldId=(*env)->GetFieldID(env,StudentClazz,"name","Ljava/lang/String;");

  16. jstring name=(jstring)((*env)->GetObjectField(env,student,nameFieldId));

  17. }

下面是Activity的代码

[java] view plaincopy

  1. static

  2. {

  3. System.loadLibrary("hello-jni");

  4. }

  5. public native void jobjectProcess(Student student,Integer flag);

  6. public static class Student{/*省略的代码*/}

  7. protected void onResume()

  8. {

  9. jobjectProcess(new Student(),new Integer(20));

  10. super.onResume();

  11. }

上面的C代码在JNI_OnLoad函数中对StudentClazz初始化,在jobjectProcess函数内没有再对StudentClazz赋值。此时运行程序会出错并在LogCat输出如下信息:

DEBUG/在jobjectProcess中输出(8494): StudentClazz=0x44c0a8f0

WARN/dalvikvm(8286): JNI WARNING: 0x44c0a8f0 is not a valid JNI reference

WARN/dalvikvm(8286): in Lcom/example/hellojni/HelloJni;.jobjectProcess (Lcom/example/hellojni/HelloJni$Student;Ljava/lang/Integer;)V (GetFieldID)

提示StudentClazz所指向的地址(0x44c0a8f0)不是合法的JNI引用。如果把jobjectProcess函数的第一行注释解除掉,再次给StudentClazz赋值程序便正常执行。

其实不管在哪个native函数内得到的StudentClazz值都是相同的,但每次native调用还是必须执行一次FindClass重新给StudentClazz赋值。

5.native的char*和JAVA的String相互转换

首先确保C/C++源文件的字符编码是UTF-8与JAVA的class文件字符编码保持一致。如果C/C++源码含有中文,那么编译出来的so中的中文字符串也保存为UTF-8编码,这样的程序不会产生乱码。

JNI提供了jstring来引用JAVA的String类型变量,如果native函数需要返回 String或者接受String类型参数就必须使用 到jstring。而C/C++用char*引用字符串起始地址,当native函数接到jstring后要转换为char*所指向的字符串才能处理。当 我们处理完char*所指向的字符串又要转换为jstring才能返回给JAVA代码。下面给出转换的方法(下面均是C代码)。

jstring转换为char*使用JNIEnv的const char*  GetStringUTFChars(JNIEnv*, jstring, jboolean*)

JNIEnv env=//传入参数 ;  jstring name=//传入参数 ;

const char *nameStr=(*env)->GetStringUTFChars(env,name,NULL);

调用完GetStringUTFChars后必须调用JNIEnv的void ReleaseStringUTFChars(JNIEnv*, jstring, const char*)释放新建的字符串。

(*env)-> ReleaseStringUTFChars(env,name, nameStr);

char*转换为jstring使用JNIEnv的jstring  NewStringUTF(JNIEnv*, const char*);

jstring newArgName=(*env)->NewStringUTF(env, nameStr);

调用完NewStringUTF后必须调用JNIEnv的void DeleteLocalRef(JNIEnv*, jobject);释放新建的jstring。

(*env)-> DeleteLocalRef(env, newArgName);

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这