JNI的优缺点
JNI的使用步骤
- 在Java中声明Native方法(即需要调用的本地方法)
- 编译上述 Java源文件javac(得到 .class文件)
- 通过 javah 命令导出JNI的头文件(.h文件)
- 使用 Java需要交互的本地代码 实现在 Java中声明的Native方法
- 编译.so库文件
- 通过Java命令执行 Java程序,最终实现Java调用本地代码
动态库和静态库的区别
动态库:在编译用户程序时不会将用户程序内使用的库函数连接到用户程序的目标代码中,只有在运行时,且用户程序执行到相关函数时才会调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。 静态库:在编译用户程序时会将其内使用的库函数连接到目标代码中,程序运行时不再需要静态库。使用静态库生成可执行文件比较大。
JavaVM和JniEnv:
JavaVM 是Java虚拟机在 JNI 层的代表,一个进程只有一个 JavaVM,所有的线程共用一个 JavaVM。JNIEnv 表示 Java 调用Native语言的环境,是一个封装了几乎全部 JNI 方法的指针。
使用javah生成的JNI方法中第一个参数永远是JNIEnv指针:
JNIEXPORT jobject JNICALL Java_com_xxx_object2struct_JniTransfer_getJavaBeanFromNative (JNIEnv *, jclass);
使用这个JNIEnv指针在该线程中调用FindClass、GetObjectClass、GetStringUTFChars等方法操作jstring等java层传递的数据,JNIEnv指针指向JNI.h的方法表,所以用env->GetStringUTFChars的形式来调用JNI.h提供的方法
C++层的原生代码没有JNIEnv指针,那如何从C++层调用Java层的方法?
看这个方法:
JNIEnv *detectJniEnv()
{
JNIEnv *env;
//获取当前native线程是否有没有被附加到jvm环境中
int getEnvStat = g_JavaVM->GetEnv((void **)&env,JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
//如果没有, 主动附加到jvm环境中,获取到env
if (g_JavaVM->AttachCurrentThread(&env,NULL) != 0) {
return NULL;
}
}
return env;
}
当C++层需要JNIEnv时,可以调用
JNIEnv *env = detectJniEnv();
获取JNIEnv指针,这是怎么做到的呢?
其实是通过调用g_JavaVM->AttachCurrentThread(&env,NULL)来获取JNIEnv,也就是通过g_JavaVM获取的,这个g_JavaVM是个JavaVM的全局变量,代表一个Java进程,把native线程Attach到Java虚拟机之后,就得到了一个JNIEnv指针,这个指针是线程独立的。
g_JavaVM全局变量是哪儿来的?
方法一:
在库加载的时候,下面这个函数会被调用:
jint JNI_OnLoad(JavaVM* vm, void* reserved);
可以在这个函数中把参数vm
缓存下来,每个进程只允许有一个JavaVM
的实例,所以把它当成全局变量cache
下来应该是安全的。
(Java JNI有两种方法,一种是通过javah,获取一组带签名函数,然后实现这些函数。这种方法很常用,也是官方推荐的方法。还有一种就是JNI_OnLoad方法。)
方法二:
在Java调用native代码的某个init方法时,通过*env获得。
JNIEXPORT jint JNICALL Java_com_***_init(JNIEnv *env, jclass type)
{
if (g_JavaVM == NULL) {
env->GetJavaVM(&g_JavaVM);
}
使用Java层传入对象和基本类型数据
获取对象?通过JNI传入的对象是jobject类型的,C++代码无法直接使用,所以C++需要创建自己的对象,把jobject对象中的参数copy进来,看一个例子:
#define PARSER_INFO_CALSS "com/fetal/healthcloud/fetalparser/FetalInfo"
JNIEXPORT void JNICALL Java_com_fetal_healthcloud_fetalparser_FetalParserLib_startMonitor(JNIEnv *env, jclass type, int channelId, jobject info)
{
jclass objectClass = (env)->FindClass(PARSER_INFO_CALSS);
jfieldID modelField = (env)->GetFieldID(objectClass,"detectModel","I");
jfieldID userInfoIdField = (env)->GetFieldID(objectClass,"userInfoId","Ljava/lang/String;");
jstring userInfoId = (jstring)env->GetObjectField(info, userInfoIdField);
jint model =env->GetIntField(info, modelField);
jboolean copy = false;
UserInfo userInfo;
userInfo.userInfoId = env->GetStringUTFChars(userInfoId, ©);
userInfo.detectModel = (DetectModel_enum)model;
FetalParserManager::sharedInstance()->channelParser(channelId)->startAyalyse(userInfo);
}
jclass objectClass = (env)->FindClass(PARSER_INFO_CALSS);
得到一个jclass对象,PARSER_INFO_CALSS是java类的完整名(包名+类名)(env)->GetFieldID(objectClass,"detectModel","I")
得到一个objectClass中的detectModel变量的ID,这个变量是java int类型的,所以第三个参数是"I",这是int 的签名。jfieldID userInfoIdField = (env)->GetFieldID(objectClass,"userInfoId","Ljava/lang/String;")
中的userInfoId是String类型的,所以签名是"Ljava/lang/String;"。- 我们第一步得到了objectClass ,第二步得到了这个objectClass的某个FieldID,现在就把这个FieldID对应的值取出来:
jstring userInfoId = (jstring)env->GetObjectField(info, userInfoIdField);
从info这个jobject对象中,把fieldId为userInfoIdField的变量值赋给userInfoId -
jboolean copy = false; UserInfo userInfo; //创建C++对象 userInfo.userInfoId = env->GetStringUTFChars(userInfoId, ©); //把取到的值传给C++对象,copy决定是否要拷贝一份,因为java的字符串是不可变的,所以C++不拷贝的话就不能修改这个String
C++调用Java方法
看一个调用静态方法的例子:
void jniReceiveParentHeartRate(int channelId, short heartRate, long long time)
{
JNIEnv *env = detectJniEnv();
if (env == NULL) {
return;
}
jclass objectClass = env->GetObjectClass(g_FetalLib);
jmethodID methodId = env->GetStaticMethodID(objectClass, "onReceiveParentHeartRate", "(ISJ)V");
env->CallStaticVoidMethod(objectClass, methodId, channelId, heartRate, time);
env->DeleteLocalRef(objectClass);
}
- detectJniEnv()上面说过,获取了JNIEnv *,用来调用JNI方法
- 如果调用静态方法,获取objectClass,不需要创建对象
- 获取objectClass中的methodId,第二个参数是方法名,第三个参数是方法签名,这个java方法名是这样的:
`public static void onReceiveParentHeartRate(int channelId, short heartRate, long time)
,所以(ISJ)V就是指int short long类型的参数,返回void的方法。
最后调用env->CallStaticVoidMethod(objectClass, methodId, channelId, heartRate, time);
再写个调用非静态方法的例子:
JNIEXPORT jobject JNICALL Java_com_dongnaoedu_jni_JniTest_accessConstructor
(JNIEnv *env, jobject jobj){
jclass cls = (*env)->FindClass(env, "java/util/Date");
//jmethodID
jmethodID constructor_mid = (*env)->GetMethodID(env, cls, "<init>", "()V");
//实例化一个Date对象
jobject date_obj = (*env)->NewObject(env, cls, constructor_mid);
//调用getTime方法
jmethodID mid = (*env)->GetMethodID(env, cls, "getTime", "()J");
jlong time = (*env)->CallLongMethod(env, date_obj, mid);
printf("\ntime:%lld\n",time);
return date_obj;
}
关于Java和C++的String转换
java内部是使用的16bit的unicode编码(utf-16)来表示字符串的,无论英文还是中文都是2字节;
◆ jni内部是使用utf-8编码来表示字符串的,utf-8是变长编码的unicode,一般ascii字符是1字节,中文是3字节;
◆ c/c++ 使用的是原始数据,ascii就是一个字节,中文一般是GB2312编码,用2个字节表示一个汉字。
jni的中文字符串处理
先从字符流的方向分别对java-->C++ 和C++-->java进行分析
◆ java-->C++
这种情况下,java调用的时候使用的是utf-16编码的字符串,jvm把这个参数传递给jni,C++ 得到的输入是jstring,此时,可以利用jni提供的两种函数,一个是GetStringUTFChars,这个函数将得到一个UTF-8编码的字符串;另一个是GetStringChars这个将得到UTF-16编码的字符串。无论那个函数,得到的字符串如果含有中文,都需要进一步转化成GB2312的编码。
◆ c/c++ –> java
jni返回给java的字符串,c/c++ 首先应该负责把这个字符串变成UTF-8或者UTF-16格式,然后通过NewStringUTF或者NewString来把它封装成jstring,返回给java就可以了。
如果字符串中不含中文字符,只是标准的ascii码,那么用GetStringUTFChars/NewStringUTF就可以搞定了,因为这种情况下,UTF-8编码和ascii编码是一致的,不需要转换。
但是如果字符串中有中文字符,那么在c/c++ 部分进行编码转换就是一个必须了。我们需要两个转换函数,一个是把UTF8/16的编码转成GB2312;一个是把GB2312转成UTF8/16。