JNI

  • 1.JNI简介
  • 2.最简单的Java调用C/C++代码的步骤
  • 3.在C/C++本地代码中访问Java代码简介

    • 3.1 JNIEnv 类型
    • 3.2 jobject
    • 3.3 jclass
    • 3.4 Java的类型在C/C++中的映射关系
  • 4.在C/C++本地代码中访问Java类中的属性与方法

    • 4.1 jfieldID/jmethodID
    • 4.2 Sign签名
    • 4.3 使用Javap命令来产生Sign签名
    • 4.4 取得Java属性/设定Java属性值
    • 4.5 Java方法的调用
  • 5.在C/C++本地代码中创建Java的对象
    • 5.1 NewObject
    • 5.2 AllocObject
  • 6.在C/C++本地代码中访问/创建Java的String字符串对象

    • 6.1 GetStringChars / GetStringUTFChars / ReleaseStringChars / ReleaseStringUTFChars
    • 6.2 GetStringCritical / ReleaseStringCritical
    • 6.3 GetStringRegion / GetStringUTFRegion
    • 6.4 字符串相关函数
  • 7.在C/C++本地代码中操作Java的数组对象

    • 7.1处理基本类型数组
    • 7.2处理对象类型数组
  • 8.全局引用/局部引用/弱全局引用

    • 8.1局部引用
    • 8.2全局引用
    • 8.2全局引用
    • 8.4关于引用的一些函数
  • 9.JNI优化


JNI简介

Java是跨平台的语言,但是在有些时候仍然是有需要调用要地代码(这些代码通常是由C/C++编写).
原Sun公司现Oracle公司提供的JNI是Java平台的一个功能强大的接口.
这个JNI接口提供了Java与操作系统本地代码互相调用的功能.


最简单的Java调用C/C++代码的步骤:

1.首先在Java类中声明一个native的方法  
2.使用javah命令生成包含native方法声明的C/C++头文件  
3.按照生成的C/C++头文件来写C/C++源文件.  
4.将C/C++源文件编译成动态连接库(DLL , OS)  
5.把DLL文件加入到Path环境变量下.  
6.Java类中加载DLL,然后调用声明的native方法  

在C/C++本地代码中访问Java代码简介

在被调用的C/C++函数中也可以反过来访问Java程序中的类
javah工具生成的C/C++函数声明中,可以看到有头两个参数 JNIEnv,jobject/jclass


package com.tu.hellojni.jni;
public class Port {
public native String sayHello();//非静态函数
public static native void sayHi(String str);//静态函数
}
1
2
3
4
```c
JNIEXPORT jstring JNICALL Java_com_tu_hellojni_jni_Port_sayHello (JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_com_tu_hellojni_jni_Port_sayHi (JNIEnv *, jclass, jstring);


JNIEnv 类型

JNIEnv类型实际上代表了Java环境,通过这个JNIEnv*指针,就可以对Java端的代码进行操作.

例如,创建Java类的对象,调用Java对象的方法,镬取Java对象的属性等等.    
    JNIEnv的指针会被JNI传入到本地方法的实现函数中来对Java端的代码进行操作.    

JNIEnv类中有很多函数可以用:  

NewObject/NewString/New<TYPE>Array 创建一个对象,创建一个字符串, 创建一个对应类型的数组   
Get/Set<TYPE>Field 获取/设置 某个类的属性     
Get/SetStatic<TYPE>Field 获取/设置某个类的静态属性    
Call<TYPE>Method/CallStatic<TYPE>Method 调用某一个类里的方法/调用某一个类的静态方法   
等等的函数  

jobject

jobject 指向Java对象的实例 , 非静态函数中时会有jobject参数.
这个jobject指向的是如: Port p = new Port; 所指向的就是p对象.


jclass

jclass 指向的是代表那个类的class对象. 静态函数中会有jclass参数.
这个jclass指向的是如: Port.sayHi(“hi”); 所指向的是Port.class对象.

jclass的取得

为了能够在C/C++中使用Java类.JNI.h头文件中专门定义了jclass类型来表示java中的Class类

JNIEnv类中有如下几个简单的函数可以取得jclass:

  • jclass FindClass(const char* clsName); 通过完整的类名取得Class
  • jclass GetObjectClass(jobject obj); 通过对象获取到class对象. 类似于 Java中的 Object中getClass方法
  • jclass GetSuperClass(jclass obj); 通过当前jclass获取到父类的class

其中FindClass 会在classPath系统环境变量下寻找类.

传入完整类名,注意包与包之间是用"/"而不是"."来分隔.
如:
jclass cls_string = env->FindClass("java/lang/String");

Java的类型在C/C++中的映射关系

Java类型 本地类型 JNI定义的别名
int long jint/jsize
long __int64 jlong
byte signed char jbyte
boolean uncigned char jboolean
char unsigned short jchar
short short jshort
float float jfloat
double double jdouble
object _jobject* jobject

在C/C++本地代码中访问Java类中的属性与方法

jfieldID/jmethodID

在C/C++本地代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了
在C/C++中表示属性和方法,JNI在Jni.h头文件中定义了jfieldID,JmethodID类型来分别代表Java类的属性和方法.

我们在访问,或是设置Java属性的时候,首先就要先在本地代码获取代表该Java属性的jfieldID,
然后才能在本地代码进行Java属性操作.
同样的,我们需要呼叫Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java方法调用.

使用JNIEnv的
    GetFieldID/GetMethodID 取得属性/方法ID ( jfieldID / jmethodID)
    GetStaticFieldID/GetStaticMethodID 取得静态属性/方法ID ( jfieldID / jmethodID )

    来取得相应的jfieldID和jmethodID
1
2
3
4
jfieldID GetFieldID(jclass clazz,const char* name,const char* sign);
jfieldID GetStaticFieldID(jclass clazz,const char* name,const char* sign);
jmethodID GetMethodID(jclass clazz,const char* name,const char* sign);
jmethodID GetStaticMethodID(jclass clazz,const char* name,const char* sign);
GetMethodID也能取得构造函数的jmethodID. 创建一个Java对象时可以调用
指定的构造方法,这个将在下面的时候做介绍.
    如:env->GetMethodID(data_Clazz,"<init>","()V");

类似Java的Reflect(反射机制)需要指定类跟属性/方法我哑取得相应的jfieldID跟jmethodID.

方法参数: jclass clazz调用指定某个类. const char* name常量字符串,代表属性或者方法的名称.
    const char* sign是当类中有两个重载方法时签名.

例如: TestNative类中有两个重载方法:
1
2
3
4
5
6
7
8
9
10
//Java
package com.tu.hellojni.jni;
public class TestNative {
public void function(int i) {
System.out.println("TestNative.function(int i)" + i);
}
public void function(double d) {
System.out.println("TestNative.function(double d)" + d);
}
}
然后在C/C++代码中需要调用其中一个function方法的话.
//首先取得要调用的方法所在的类

jclass clazz_TestNative = env->FindClass("com/tu/hellojni/jni/TestNative");
//取得jmethodID之后才进行调用
jmethodID id_func = env->GetMethodID(clazz_testNative,"function","??????");

但是到底取得的是
void function(int i )还是
void function(double d)的jmethodID呢?
这就是sign的作用了.它用于指定要取得的属性/方法类型.
这里的sign如果指定为"(I)V"则取回void function(int)的jmethodID
        如果指定为(D)V"则取回void function(double)的jmethodID

Sign签名

用来表示要取得的属性/方法的类型
类型 相应的签名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
object L用/分隔包的完整类名: Ljava/lang/String;
Array [签名 如[I 整形数组 如[Ljava/lang/Object; 对象数组
Method (参数1类型签名,参数2类型签名…)返回值类型签名 如:(ILjava/util/Date;[I)I

使用Javap命令来产生Sign签名

从上面代码中的签名可以看出.如果是比较复杂的签名编写起来还是比较麻烦的.
所以JDK也提供了一个工具javap来查看一个类的声明,其中就可以设置输出每个方法/属性的签名.

语法: 
    javap -s -p [full Class Name]
    -s 表示输出签名信息
    -p 同-private,输出包含private 访问权限的成员信息

使用方法:
    1.cmd定位到工程目录下.
    2.使用javap输入信息.


C:\Users\ComTu>cd E:\ComTu_Design\workspace\workspace-Android\HelloJ
C:\Users\ComTu>E:
E:\ComTu_Design\workspace\workspace-Android\HelloJNI>cd bin/classes

E:\ComTu_Design\workspace\workspace-Android\HelloJNI\bin\classes>jav
Compiled from "Port.java"
public class com.tu.hellojni.jni.Port extends java.lang.Object{
public int property;
  Signature: I
public com.tu.hellojni.jni.Port();
  Signature: ()V
public native java.lang.String sayHello();
  Signature: ()Ljava/lang/String;
public static native void sayHi(java.lang.String);
  Signature: (Ljava/lang/String;)V
public native void testSign();
  Signature: ()V
public void functionOverride();
  Signature: ()V
public void functionOverride(int);
  Signature: (I)V
public int function(int, java.util.Date, int[]);
  Signature: (ILjava/util/Date;[I)I
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[在Eclipse中配置Javap快捷操作](/blog/2014/10/28/Android_NDK_IDE_environment_one.html)
---
## 取得Java属性/设定Java属性值
取得了相应属性的 jfieldID 之后就可以用
Set<TYPE>Field
Get<TYPE>Field
SetStatic<TYPE>Filed
GetStatic<TYPE>File等函数来对Java属性进行操作了. -->TYPE泛指各种数据类型.
如: Boolean , Byte , Double , Float , Int , Long , Short , Object
怎样获取数组属性呢?
可以使用GetObjectField来取得数组类型的属性.
案例:
```java
//Java
public class Port{
/** C进行get/set成员变量 */
public int property = 55;
/** C进行Get/Set静态成员变量 */
public static boolean isShow = false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//C
jclass port_clazz = env->GetObjectClass(obj);
//获取java成员变量
//public int property;
jfieldID fieldId_prop = env->GetFieldID(port_clazz, "property", "I");
jint prop = env->GetIntField(obj, fieldId_prop);//取值
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-int: %d", prop);
//给java成员变量赋值
env->SetIntField(obj, fieldId_prop, 100L);//赋值100L表示c中的100常整数类型.
//获取静态变量
jfieldID fieldId_isShow = env->GetStaticFieldID(port_clazz, "isShow", "Z");
jboolean jb = env->GetStaticBooleanField(port_clazz, fieldId_isShow);
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-boolean: %d", jb);
jboolean b = !jb;
env->SetStaticBooleanField(port_clazz, fieldId_isShow, b);//给静态变量赋值

Java方法的调用

JNIEnv 提供了众多的Call<TYPE>Method 跟 CallStatic<TYPT>Method,还有
CallNonvirtual<TYPE>Method函数.需要通过 GetMethodID 取得相应方法的 jmethodID来
传入到函数参数中.

调用实例方法的三种形式:
    //第一种:最常用的方式
    Call<TYPE>Method(jobject obj,jmethodID id,...); 
    //第二种:当调用这个函数的时候有一个指向参数表的va_list变量时使用的_很少使用到这种方式
    Call<TYPE>MethodV(jobject obj,jmethodID id, va_list lst);
    //第三种:当调用这个函数的时候有一个指向jvalue或者jvalue数组的指针时用的
    Call<TYPE>MethodA(jobject obj,jmethodID id, jvalue * v);

调用静态方法的三种形式:
    CallStatic<TYPE>Method(jclass class , jmethodID id, ...);
    CallStatic<TYPE>MethodV(jclass class , jmethodID id, va_list lst);
    CallStatic<TYPE>MethodA(jclass class , jmethodID id, jvalue * v);

第三种中的 jvalue 这个类型是一个联合体.
源代码jni.h中:
1
2
3
4
5
6
7
8
9
10
11
12
//C
typedef union jvalue {
jboolean z;
jbyte b;
jchar c;
jshort s;
jint i;
jlong j;
jfloat f;
jdouble d;
jobject l;
} jvalue;
案例: 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//Java
public class Port {
/** C调用重载成员函数_非静态方法 */
public void functionOverride() {
System.out.println("Port.functionOverride()--参数:property>" + property + "--isShow>" + isShow);
}
/** C调用重载成员函数_非静态方法 */
public void functionOverride(int i) {
System.out.println("Port.functionOverride(int i)-->" + i + "--参数:property>" + property + "--isShow>" + isShow);
}
/** C调用多参数签名_非静态方法 */
public int function(int foo, Date date, int[] arr) {
System.out.println("Port.function()foo>" + foo + "===date>" + date + "==arr>" + arr);
return foo;
}
/** C调用成员函数 的三种形式 */
public boolean function(int i, double d, char c) {
System.out.println("===>Port.function()i>" + i + "===d>" + d + "==c>" + c);
return true;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//C
jclass port_clazz = env->GetObjectClass(obj);
//调用调用Java里的Port类的void functionOverride()重载函数_非静态函数
jmethodID id_func1 = env->GetMethodID(port_clazz, "functionOverride", "()V");
env->CallVoidMethod(obj, id_func1);//调用
//调用调用java里的Port类的void functionOverride(int )C调用重载函数_非静态函数
jmethodID id_func2 = env->GetMethodID(port_clazz, "functionOverride", "(I)V");
env->CallVoidMethod(obj, id_func2, 1L);//调用,并需要一个参数
//调用调用java里的Port类的public int function(int foo, Date data, int[] arr) -->(ILjava/util/Date;[I)I
jmethodID methodID_func = env->GetMethodID(port_clazz, "function", "(ILjava/util/Date;[I)I");
env->CallIntMethod(obj, methodID_func, 100L, NULL, NULL);//调用Port的function方法,后面跟随三个参数
jmethodID methodID_func_IDC = env->GetMethodID(port_clazz, "function", "(IDC)Z");
//调用实例方法的第一种形式: Call<TYPE>Method(jobject obj,jmethodID id,...);
env->CallBooleanMethod(obj, methodID_func_IDC, 100L, 3.14, L'1');// 100L长整型,3.14double, L'1'宽字符
//调用实例方法的第三种形式:
jvalue * j = new jvalue[3];
j[0].i = 50L;
j[1].d = 3.1415;
j[2].c = L'5';
env->CallBooleanMethodA(obj, methodID_func_IDC, j);
delete[] j;//删除回收
调用一个对象的父类的方法:
    在JNI中定义的 CallNonvirtual<TYPE>Method 就能够实现子类对象调用父类方法的功能.

    要使用它,首先要取得父类及要调用的父类方法的 jmethodID,方可.

案例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Java
public class Father {
public void function() {
System.out.println("===>Father.function()");
}
}
public class Child extends Father {
@Override
public void function() {
System.out.println("===>Child.function()");
}
}
public class Port {
/** C调用_子父类关系类 */
public Father father = new Child();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//C
//调用子父类的成员变量方法:
jfieldID id_father = env->GetFieldID(port_clazz, "father", "Lcom/tu/hellojni/jni/Father;");
jobject father = env->GetObjectField(obj, id_father);
jclass clazz_Father = env->FindClass("com/tu/hellojni/jni/Father");
jmethodID id_father_function = env->GetMethodID(clazz_Father, "function", "()V");
env->CallVoidMethod(father, id_father_function);
// 以上代码相当于:
// Port port = new Port();
// Father f = port.father; //-->father成员变量是:public Father father = new Child();
// f.function();
//运行结果: ===>Child.function()
// 调用父类的方法
env->CallNonvirtualVoidMethod(father, clazz_Father, id_father_function);
//运行结果:===>Father.function()

在C/C++本地代码中创建Java的对象

创建Java对象有两种方式. (NewObject / AllocObject)

5.1 NewObject

使用函数 NewObject 可以用来创建对象

jobject NewObject(jclass clazz , jmethodID methodID,...)

GetMethodID能够取得构造方法的 jmethodID .如果传入的要取得的方法名称设定为"<init>"就能够取得构造方法.  
构造方法的方法返回值类型的签名始终为Void.

    案例:
1
2
3
4
5
6
7
8
9
10
//C
//创建一个日期类Date对象并调用getTime方法
jclass clazz_date = env->FindClass("java/util/Date");
jmethodID mid_date = env->GetMethodID(clazz_date,"<init>","()V");
jobject now = env->NewObject(clazz_date,mid_date);
jmethodID jmethodID_getTime = env->GetMethodID(clazz_date,"getTime","()J");
jlong time = env->CallLongMethod(now,jmethodID_getTime);
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-创建Date对象并调用getTime方法: %d", time);

5.2 AllocObject

使用 AllocObject 方式创建对象(创建但未初始化,使用比较少.)
使用函数AllocObject可以根据传入的jclass创建一个Java对象,但是他的状态是非初始化的,
在使用这个对象之前绝对要用 CallNonvirtualVoidMethod 来调用该jclass的建构函数.
这样可以延迟构造函数的调用.这个部分用的很少.

案例: 
1
2
3
4
5
//Java
public class Port {
/** C进行创建并初始化 */
public static String STATIC_STR;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//C
//---> AllocObject 创建延迟初始化的对象
jclass clazz_str = env->FindClass("java/lang/String");
jmethodID methodID_str = env->GetMethodID(clazz_str, "<init>", "([C)V");//通过字符数组构造函数
//预先创建一个没有初始化的字符串
jobject string = env->AllocObject(clazz_str);
//创建一个5个元素的字符数组,然后以'c','o','m','t','u'赋值
jcharArray arg = env->NewCharArray(5);
jchar buf[5];
buf[0] = 67;
buf[1] = 'o';
buf[2] = 'm';
buf[3] = 't';
buf[4] = 'u';
env->SetCharArrayRegion(arg, 0, 5, buf);
//呼叫构建函数
env->CallNonvirtualVoidMethod(string, clazz_str, methodID_str, arg);
jclass clazz_this = env->GetObjectClass(obj);
jfieldID fieldID_str = env->GetStaticFieldID(clazz_this, "STATIC_STR", "Ljava/lang/String;");
env->SetStaticObjectField(clazz_this, fieldID_str, string);//给java静态变量赋值

在C/C++本地代码中访问/创建Java的String字符串对象

Java字符串<-->C/C++字符串

    在Java中,使用的字符串String对象是Unicode(UTF-16)码, 
        即每个字符不论是中文英文还是符号,一个字符总是占两个字节.

Java通过JNI接口可以将Java的字符串转换到C/C++中的宽字符串(wchar_t*),
或者传回一个UTF-8的字符串(char*)到C/C++. 
返过来,C/C++可以通过一个宽字符串,或者一个UTF-8编码的字符串来创建一个Java端的String对象.


获取Java中的String对象有如下几种函数:

GetStringChars / GetStringUTFChars / ReleaseStringChars / ReleaseStringUTFChars .

const jchar* GetStringChars (jstring str , jboolean* copied)
    可以取得UTF-16编码的宽字符串(jchar*)
    开新内存,然后把Java中的String拷贝到这个内存中,然后返回指向这个内存地址的指针.

const char* GetStringUTFChars (jstring str , jboolean* copied)
    可以取得UTF-8编码的字符串(char*)
    直接返回指向Java中String的内存的指针,这个时候
    千万不要改变这个内存的内容,这将破坏String在Java中始终是常量这个原则.

第二个参数jboolean* copied是用来标示是否对Java的String对象进行拷贝的.
如果传入的这个jboolean指针不是 NULL ,则他会给该指针所指向的内存传入 JNI_TRUE
或 JNI_FALSE 指示是否进行拷贝.
传入 NULL 表示不关心是否拷贝字符串,它就不会被jboolean* 指向的内存赋值

使用了如上这两个函数取得的字串,在不使用的时候,要使用
ReleaseStringChars / ReleaseStringUTFChars 来释放拷贝的内存,或者释放对Java的String对象的引用.

ReleaseStringChars (jstring jstr, const jchar* str); 
ReleaseStringUTFChars (jstring jstr, const char* str);

    第一个参数指定一个jstring变量,即要释放的本地字符串的来源.
    第二个参数就是要释放的本地字符串.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void function(JNIEnv * env, jobject obj) {
//-------- GetStringChars / ReleaseStringChars / NewString -------------
//获取jmethodID
jfieldID jfie_msg = env->GetFieldID(env->GetObjectClass(obj), "message", "Ljava/lang/String;");
//获取到String属性
jstring j_msg = (jstring) env->GetObjectField(obj, jfie_msg);
//UTF-16编码的宽字符串(jchar*) const jchar* == const wchar_t*
// Java-String->C-jchar*转换
const jchar* jstr = env->GetStringChars(j_msg, NULL);//j_msg不能为null否则会抛异常
//---------拷贝数据start--------------
jsize size = env->GetStringLength(j_msg);
//转换成宽字符串
// wstring wstr((const wchar_t*) jstr);
jchar* jstrTemp = (jchar*) jstr;
//---------拷贝数据end--------------
//回收本地字符串 _释放指针
env->ReleaseStringChars(j_msg, jstr);
//---------处理数据start--------------
//倒序函数迭代字符串
// std::reverse(wstr.begin(), wstr.end());
//---------处理数据end--------------
//创建String对象并赋予Port的message变量中.
// jstring j_new_str = env->NewString((const jchar*) wstr.c_str(), (jint) wstr.size());
jstring j_new_str = env->NewString(jstrTemp, size);
//赋值给Java的Port的message变量
env->SetObjectField(obj, jfie_msg, j_new_str);
}
```
```c
//拷贝
char* cc2c(const char* str) {
char* strtemp = new char[strlen(str) + 1];
strcpy(strtemp, str);
return strtemp;
}
//简单倒序算法
char* myReverse(const char* str) {
char* strtemp = cc2c(str);
int len = strlen(strtemp);
char t;
for (int i = 0; i < len / 2; i++) {
t = strtemp[i];
strtemp[i] = strtemp[len - i - 1];
strtemp[len - i - 1] = t;
}
return strtemp;
}
void function(JNIEnv * env, jobject obj) {
//------------------ GetStringUTFChars / ReleaseStringUTFChars / NewStringUTF -------
jfieldID jfie_msg = env->GetFieldID(env->GetObjectClass(obj), "message", "Ljava/lang/String;");
jstring j_msg = (jstring) env->GetObjectField(obj, jfie_msg);
// Java-String->C-jchar*转换
const char* str = env->GetStringUTFChars(j_msg, NULL);
char* strtemp = myReverse(str);
env->ReleaseStringUTFChars(j_msg, str);
jstring j_new_str = env->NewStringUTF(strtemp);
delete[] strtemp;
env->SetObjectField(obj, jfie_msg, j_new_str);
}

GetStringCritical / ReleaseStringCritical .

为了增加直接传回指向Java字符串的指针的可能性(而不是拷贝),JDK1.2之后出来新的函数

const jchar* GetStringCritical (jstring str , jboolean* copied)
void ReleaseStringCritical (jstring jstr,const jchar* str);

注意:
在 GetStringCritical / ReleaseStringCritical 方法之间是一个关键区.
在这关键区之中绝对不能呼叫(call)JNI的其它函数,会造成当前线程中断或是会让当前线程
等待的任何本地代码.否则将造成关键区代码执行期间垃圾回收器停止动作,任何触发垃圾
回收的线程也会暂停.其它的触发垃圾回收器的线程不能前进,直到当前线程结束而激活垃圾回收器.

在关键区中千万不要出现中断操作,或者在JVM中分配任何新对象.否则会造成JVM死锁

虽说这个函数会增加直接传回指向Java字符串的指针的可能性,不过还是会根据情况传回拷贝过的字符串.

不支持 GetStringUTFCritical ,没有这样一个函数,由于Java字符串用的是UTF16,要转
成UTF8编码的字符串始终需要进行一次拷贝,所以没有这样的函数.
1
2
3
4
5
6
7
8
9
10
11
12
13
//--------------- GetStringCritical / ReleaseStringCritical ----------
void function(JNIEnv * env, jobject obj) {
// 基本与 GetStringChars 一样
jfieldID jfie_msg = env->GetFieldID(env->GetObjectClass(obj), "message", "Ljava/lang/String;");
jstring j_msg = (jstring) env->GetObjectField(obj, jfie_msg);
// Java-String->C-jchar*转换
const jchar* jstr = env->GetStringCritical(j_msg, NULL);//jstr_msg不能为null复杂会抛异常
wstring wstr((const wchar_t*) jstr);
env->ReleaseStringCritical(j_msg, jstr);
std::reverse(wstr.begin(), wstr.end());
jstring j_new_str = env->NewString((const jchar*) wstr.c_str(), (jint) wstr.size());
env->SetObjectField(obj, jfie_msg, j_new_str);
}

GetStringRegion / GetStringUTFRegion .

Java 1.2之后出来的函数,这个函数的动作,是把Java字符串的内容直接拷贝到C/C++
的字符数组中,在呼叫这个函数之前必须有一个C/C++分配出来的字符串,然后传入到
这个函数中进行字符串的拷贝.

由于C/C++中分配内存开销相对小,而且Java中的String内容拷贝的开销可以忽略,
更好的一点是此函数不分配内存,不会抛OutOfMemoryError异常.

//拷贝Java字符串并以UTF-8编码传入bufer
GetStringUTFRegion(jstring str , jsize start , jsize len , char* buffer);

//拷贝Java字符串并以UTF-16编码传入buffer
GetStringRegion(jstring str, jsize start , jsize len, jchar* buffer);

Java的String str:
    A  B  C  D  E  F  G  H  I  J  K  L  M  N
             ↑― start = 3  ↓  len = 10 ―↑ 
    env->GetStringRegion(str,3,10,buffer);//copy至buffer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//---------------- GetStringRegion / NewString ----------------
void function(JNIEnv * env, jobject obj) {
jfieldID jfie_msg = env->GetFieldID(env->GetObjectClass(obj), "message", "Ljava/lang/String;");
jstring j_msg = (jstring) env->GetObjectField(obj, jfie_msg);
jsize jm_len = env->GetStringLength(j_msg);
//创建一个数组长度为message字符串的长度+1(结尾符)
jchar* jstr = new jchar[jm_len + 1];
jstr[jm_len] = L'\0';//C++中都会有结尾符,不然会出现乱码.
//拷贝到字符数组里
env->GetStringRegion(j_msg, 0, jm_len, jstr);
// wstring wstr((const wchar_t*) jstr);
//std::reverse(wstr.begin(), wstr.end());
jstring j_new_str = env->NewString((const jchar*) jstr,jm_len);
env->SetObjectField(obj, jfie_msg, j_new_str);
//删除不必要的内存
delete[] jstr;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//---------------- GetStringUTFRegion / NewStringUTF ----------------
void function(JNIEnv * env, jobject obj) {
jfieldID jfie_msg = env->GetFieldID(env->GetObjectClass(obj), "message", "Ljava/lang/String;");
jstring j_msg = (jstring) env->GetObjectField(obj, jfie_msg);
jsize jm_len = env->GetStringLength(j_msg);
char* buffer = new char[jm_len + 1];
env->GetStringUTFRegion(j_msg, 0, jm_len, buffer);
// wstring wstr((const wchar_t*) jstr);
//std::reverse(wstr.begin(), wstr.end());
jstring j_new_str = env->NewStringUTF((const char*) buffer);
env->SetObjectField(obj, jfie_msg, j_new_str);
delete[] buffer;
}

字符串相关函数

jstring NewString(const jchar* str, jsize len); //创建一个字符串
jstring NewStringUTF(const char* str); //创建一个UTF8的字符串
jsize GetStringLength(jstring str);// 字符串的长度
jsize GetStringUTFLength(jstring str);//UTF会占用多少个字节

7.在C/C++本地代码中操作Java的数组对象

1.基本类型的数组
2.对象类型(Objcet[])的数组

一个能通用于两种不同类型数组的函数
GetArrayLength(jarray aray);//获取数组长度

处理基本类型数组

Get<TYPE>ArrayElements(<TYPE>Array arr, jboolean* iscopied);
    这类函数可以把Java基本类型的数组转换到C/C++中的数组,有两种处理方式,
    1.拷贝一份传回本地代码,
    2.把指向Java数组的指针直接传回到本地代码.
    处理完本地代码的数组后,通过 Release<TYPE>ArrayElements来释放数组.

Release<TYPE>ArrayElements(<TYPE>Array arr,<TYPE>* array,jint mode)
    用这个函数可以选择将如何处理Java跟C++的数组,是提交,撤消,内存释放,还是不释放等.
    mode可以取下面的值:
    0          -> 对Java的数组进行更新并释放C/C++的数组.
    JNI_COMMIT -> 对Java的数组进行更新但不释放C/C++的数组.
    JNI_ABORT  -> 对Java的数组不进行更新,释放C/C++的数组.

GetPrimitiveArrayCritical(jarray arr , jboolean* isCopied);
ReleasePrimitiveArrayCritical(jarray arr , void* array,jint mode);
    也是JDK1.2出来的,为了增加直接传回指向Java数组的指针而加入的函数,同样的,
    也会有同 GetStringCritical 的死锁的问题.

Get<TYPE>ArrayRegion(<TYPE>Array arr, jsize start , jsize len , <TYPE>* buffere);
    在C/C++预先开辟一段内存,然后把Java基本类型的数组拷贝到这段内存中.
    跟 GetStringRegion原理类似

Set<TYPE>ArrayRegion(<TYPE>Array arr , jsize start, jsize len , const <TYPE>* buffer);
    把Java基本类型的数组中的指定范围的元素用C/C++的数组中的元素来赋值.

<TYPE>Array New<TYPE>Array(jsize sz)
    指定一个长度然后返回相应Java基本类型的数组.
1
2
3
//java
/** C调用Get/Set成员变量 数组 */
public int[] ints = new int[] { 67, 111, 109, 116, 117 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//c
//---- GetIntArrayElements / ReleaseIntArrayElements / ReleaseIntArrayElements / ReleaseIntArrayElements ----
//基本数据类型数组
void function(JNIEnv * env, jobject obj) {
//取得属性Id
jfieldID fieldId = env->GetFieldID(env->GetObjectClass(obj), "ints", "[I");
//通过属性id取得数组变量
jintArray ints = (jintArray) env->GetObjectField(obj, fieldId);
//转取得本地数组
jint* int_arr = env->GetIntArrayElements(ints, NULL);
jsize len = env->GetArrayLength(ints);
//C基本库 . 对数组进行排序,迭代器需要两个参数,第一个是array首地址,第二个是array尾地址
std::sort(int_arr, int_arr + len);
for (jsize i = 0; i < len; ++i) {
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-intArray[]: %d", int_arr[i]);
}
//对Java的数组进行更新并释放C/C++的数组.
env->ReleaseIntArrayElements(ints, int_arr, 0);
//对Java的数组进行更新但不释放C/C++的数组.
// env->ReleaseIntArrayElements(ints, int_arr, JNI_COMMIT);
//对Java的数组不进行更新,释放C/C++的数组.
// env->ReleaseIntArrayElements(ints, int_arr, JNI_ABORT);
}

处理对象类型数组

JNI没有提供直接把Java的对象类型数组(Object[])直接传到C++中的jobject[]数组的函数.
而是直接通过 Get/SetObjectArrayElement 这样的函数来对Java的Object[]数组进行操作.


使用上述的函数也不用释放任何资源.

NewObjectArray 可以通过指定长度跟初始值来创建某个类的数组.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//c
// ----- GetObjectArrayElement / NewObjectArray / SetObjectArrayElement ------
//对象数据类型数组
void function(JNIEnv * env, jobject obj) {
jfieldID fieldStrId = env->GetFieldID(jclazz, "strs", "[Ljava/lang/String;");
//获取java的对象数组
jobjectArray str_arr = (jobjectArray) env->GetObjectField(obj, fieldStrId);
int lenStr = env->GetArrayLength(str_arr);
printf("\n c-stringArray :");
int i = 0;
//读Java的以String数组
for (i = 0; i < lenStr; i++) {
jobject obj = env->GetObjectArrayElement(str_arr, i);
jstring str = (jstring) obj;
const char * szStr = env->GetStringUTFChars(str, 0);
printf(" %d-%s ", i, szStr);
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-StringArray[]: %d - %s", i, szStr);
const jchar * chars = (const jchar *) szStr;
env->ReleaseStringChars(str, chars);
}
//创建一个对象数组 , 为Java对象数组赋值并更新.
jstring str;
jobjectArray args = 0;
jsize size = 7;
char* sa[] = { "Hello,", "world!", "c", "o", "m", "t", "u" };
int j = 0;
jclass objClass = env->FindClass("java/lang/String");
args = env->NewObjectArray(size, objClass, 0);
for (j = 0; j < size; j++) {
str = env->NewStringUTF(sa[j]);
env->SetObjectArrayElement(args, j, str);
if (j < lenStr)//给Java数组赋数组下标越界抛异常 ,
env->SetObjectArrayElement(str_arr, j, str);//自动更新Java类中的strs对象数组
}
// 不需要释放任何资源.
//args 新创建的 String 数组.
}

8.全局引用/局部引用/弱全局引用

从Java虚拟机创建的对象传到本地C/C++代码时会产生引用.根据Java的垃圾回收机制,只要有引用
存在就不会触发该引用指向的Java对象的垃圾回收.

这些引用在JNI中分三种:
    全局引用(Global Reference)
    局部引用(Local Reference)
    弱全局引用(Weak Global Reference ) Since JDK1.2

局部引用

最常见的引用类型,基本上通过JNI返回来的引用都是局部引用.
    例如使用NewObject就会返回创建出来的实例的局部引用.局部引用只在该
    native函数中有效,所有在该函数中产生的局部引用,都会在函数返回的时候
    自动释放(freed).也可以使用 DeleteLocalRef 函数手动释放该引用.

实际上局部引用存在,就会防止其指向的对象被垃圾回收,尤其是当一个局部引用
指向一个很庞大的对象,或是在一个循环中生成了局部引用,最好的做法就是在使用完该
对象后,或在该循环尾部把这个引用释放掉,以确保在垃圾回收器被触发的时候被回收.

在局部引用的有效期中,可以传递到别的本地函数中,要强调的是他的有效期仍然只在
一次的Java本地函数调用中,所以千万不能用C++全局变量保存他或是把他定义为C++
静态局部变量.

env->NewLocalRef(_jobject* localRef);
env->DeleteLocalRef(_jobject* localRef);

全局引用

全局引用可以跨越当前线程,在多个native函数中有效,不过需要编程人员手动
来释放该引用.全局引用存在期间会防止在Java的垃圾回收的回收.

与局部引用不同,全局引用的创建不是由JNI自动创建的,全局引用是需要
调用 NewGlobalRef 函数,而释放他需要使用 DeleteGlobalRef 函数.

env->NewGlobalRef(_jobject* obj);
env->DeleteGlobalRef(_jobject * globalRef);

弱全局引用

Java 1.2后出来的功能,与全局引用相似,创建跟删除都需要由编程人员来进行.
这种引用与全局引用一样可以在多个本地代码有效,也跨越多线程有效,不一样的是,
这种引用将不会阻止垃圾回收器回收这个引用所指向的对象.

使用 NewWeakGlobalRef 跟 DeleteWeakGlobalRef 来产生和解除引用.

env->NewWeakGlobalRef(_jobject * obj);
env->DeleteWeakGlobalRef(_jobject* obj);

关于引用的一些函数

jobject NewLocalRef( jobject obj);
jobject NewGlobalRef( jobject obj);
jboject NewWeakGlobalRef( jobject obj);
void DeleteLocalRef( jobject obj);
void DeleteGlobalRef( jobject obj);
void DeleteWeakGlobalRef( jobject obj);
//比较两个引用是否指向同一个Java对象
jboolean IsSameObject(jobject obj1,Object obj2);
这个函数对于弱全局引用还有一个特别的功能.
把 NULL 传入要比较的对象中,就能判断弱全局引用所指向的Java对象是否被回收.

9.JNI优化

缓存 jfieldID/jmethodID

    取得 jfieldID跟jmethodID的时候会通过该属性/方法名称加上签名来查询相应的
    jfieldID/jmethodID.这种查询相对来说开销较大.我们可以将这些FieldID/MethodID
    缓存起来,这样只需要查询一次,以后就使用缓存起来的FieldID/MethodID了.

    两种缓存的方式:
        1.在用的时候缓存 (caching at the point of use)

            在Native Code 中使用static局部变量来保存已经查询过的id.这样就不会在每次
            函数调用时查询,而只要第一次查询成功后就保存起来了.

            不过在这种情况下就不得不考虑多线程同时呼叫此函数时可能会导致同时查询的危机.
            不过这种情况是无害的,因为查询同一个属性/方法的ID通常返回的是一样的值.
1
2
3
/**缓存id*/
public String cacheJfieldID = null;
public String cacheJmethodID = null;
1
2
3
4
5
6
static jfieldID fieldID_string = NULL;
jclass clazz = env->GetObjectClass(obj);
if (fieldID_string == NULL) {
fieldID_string = env->GetFieldID(clazz, "cacheJfieldID", "Ljava/lang/String;");
}
//other code...

2.在Java类初始化时缓存(caching at the defining Class’s inititalizer)

更好的一个方式就是在任何native函数调用前把id全部存起来.

我们可以让java在第一次加载这个类的时候,首先调用本地代码初始化所以的
jfieldID/jmethodID,这样的话就可以省去多次的确定id是否存在的语句,
当然,这些jfieldID/jmethodID是定义在C/C++的全局.

使用这种方式还有好处,当Java类卸载或是重新加载的时候也会重新呼叫
该本地代码来重新计算IDs.
1
2
3
4
5
6
7
8
9
10
11
public class Port {
public int property = 55;
public String message = "123456";
/**缓存id*/
static{
initNativeIDs();//初始化id
}
public static native void initNativeIDs();
//other code...
}
1
2
3
4
5
6
7
8
9
10
//C
jfieldID g_propInt_id = 0;
jfieldID g_propStr_id = 0;
/**Java初始化的时候缓存*/
JNIEXPORT void JNICALL Java_com_tu_hellojni_jni_Port_initNativeIDs(JNIEnv * env, jclass clazz) {
__android_log_print(ANDROID_LOG_INFO, "JNIMsg", "\n c-message: %s", "load.initNativeIDs..");
g_propInt_id = env->GetFieldID(clazz, "property", "I");
g_propStr_id = env->GetFieldID(clazz, "message", "Ljava/lang/String;");
}
//other code...

Demo下载

Demo

本文Demo
本文Demo源代码

本博文暂时没有: 异常处理; C/C++如何启动JVM; JNI跟多线程;等介绍.有时间再后续…
介绍两本书:
The Java Native interface Programmer’s Guide and Specification
JNI++ User Guider

坚持原创技术分享,您的支持将鼓励我继续创作!