- 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);//静态函数
}
1234 ```cJNIEXPORT 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
|
|
GetMethodID也能取得构造函数的jmethodID. 创建一个Java对象时可以调用
指定的构造方法,这个将在下面的时候做介绍.
如:env->GetMethodID(data_Clazz,"<init>","()V");
类似Java的Reflect(反射机制)需要指定类跟属性/方法我哑取得相应的jfieldID跟jmethodID.
方法参数: jclass clazz调用指定某个类. const char* name常量字符串,代表属性或者方法的名称.
const char* sign是当类中有两个重载方法时签名.
例如: TestNative类中有两个重载方法:
|
|
然后在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
}
|
|
|
|
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中:
|
|
案例:
|
|
|
|
调用一个对象的父类的方法:
在JNI中定义的 CallNonvirtual<TYPE>Method 就能够实现子类对象调用父类方法的功能.
要使用它,首先要取得父类及要调用的父类方法的 jmethodID,方可.
案例:
|
|
|
|
在C/C++本地代码中创建Java的对象
创建Java对象有两种方式. (NewObject / AllocObject)
5.1 NewObject
使用函数 NewObject 可以用来创建对象
jobject NewObject(jclass clazz , jmethodID methodID,...)
GetMethodID能够取得构造方法的 jmethodID .如果传入的要取得的方法名称设定为"<init>"就能够取得构造方法.
构造方法的方法返回值类型的签名始终为Void.
案例:
|
|
5.2 AllocObject
使用 AllocObject 方式创建对象(创建但未初始化,使用比较少.)
使用函数AllocObject可以根据传入的jclass创建一个Java对象,但是他的状态是非初始化的,
在使用这个对象之前绝对要用 CallNonvirtualVoidMethod 来调用该jclass的建构函数.
这样可以延迟构造函数的调用.这个部分用的很少.
案例:
|
|
|
|
在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变量,即要释放的本地字符串的来源.
第二个参数就是要释放的本地字符串.
|
|
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编码的字符串始终需要进行一次拷贝,所以没有这样的函数.
|
|
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
|
|
|
|
字符串相关函数
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基本类型的数组.
|
|
|
|
处理对象类型数组
JNI没有提供直接把Java的对象类型数组(Object[])直接传到C++中的jobject[]数组的函数.
而是直接通过 Get/SetObjectArrayElement 这样的函数来对Java的Object[]数组进行操作.
使用上述的函数也不用释放任何资源.
NewObjectArray 可以通过指定长度跟初始值来创建某个类的数组.
|
|
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通常返回的是一样的值.
|
|
|
|
2.在Java类初始化时缓存(caching at the defining Class’s inititalizer)
更好的一个方式就是在任何native函数调用前把id全部存起来.
我们可以让java在第一次加载这个类的时候,首先调用本地代码初始化所以的
jfieldID/jmethodID,这样的话就可以省去多次的确定id是否存在的语句,
当然,这些jfieldID/jmethodID是定义在C/C++的全局.
使用这种方式还有好处,当Java类卸载或是重新加载的时候也会重新呼叫
该本地代码来重新计算IDs.
|
|
|
|
Demo下载
本博文暂时没有: 异常处理; C/C++如何启动JVM; JNI跟多线程;等介绍.有时间再后续…
介绍两本书:
The Java Native interface Programmer’s Guide and Specification
JNI++ User Guider