静态注册和动态注册
看下基本的jni的demo,我的java代码
package zcwfeng;
import java.*;
public class Register{
static{
System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
}
public native String HelloWorld();
public static void main(String[] args){
Register r = new Register();
String re = r.HelloWorld();
System.out.println(re);
}
}
多说两句,就是可能引入jni.h 的时候不存在需要到java环境include里面jni头文件拷贝到项目,查找一下jni_md.h 也考过来,搜索一下就可以
我用的clion环境创建的工程
手动生成头文件
zcwfeng_Register.h 头文件,生成的就补贴出来了
zcwfeng_Register.c 实现,这个名字可以和头文件不保持一致,但是这样比较规范
#include "zcwfeng_Register.h"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_HelloWorld(JNIEnv *env, jobject jobject1){
return (*env)->NewStringUTF(env,"Hello David");
}
静态注册
步骤:
1)编写java类,假如是JniTest.java
2)在命令行下输入 javac JniTest.java 生成JniTest.class文件
- 在 JniTest.class 目录下 通过 javah xxx.JniTest(全类名)生成 xxx_JniTest.h 头文件
4)编写xxx_JniTest.c 源文件,并拷贝xxx_JniTest.h 下的函数,并实现这些函数,且在其中添加jni.h头文件;
5)编写 cmakelist.txt 文件,编译生成动态/静态链接库
动态注册
在Register.java,加入动态native方法声明
-> 动态注册三个方法声明
native void dynamicFun1();
native String dynamicFun2();
native int getRandom();
-> main 方法加入调用
r.dynamicFun1();
System.out.println(r.dynamicFun2());
System.out.println(r.getRandom());
为了方便,把动态注册的代码提取出来单独的c文件实现,dyReg.c
#include "jni.h"
void func1(JNIEnv *env, jobject jobject) {
printf("dynamicNative1 动态注册");
}
jstring func2(JNIEnv *env, jobject jobject) {
printf("dynamicNative2 动态注册");
return (*env)->NewStringUTF(env, "Hello dynamic native");
}
jint func3(JNIEnv *env, jobject jobject) {
return 100;
}
static const char * mClassName = "zcwfeng/Register";
static const JNINativeMethod mMethods[] = {
{"dynamicFun1","()V",(void *)func1},
{"dynamicFun2","()Ljava/lang/String;",(jstring *)func2},
{"getRandom","()I",(jint *)func3}
};
-> 这部分方法参数和名字不用记,去jni.h 拷贝就行,也就是你java调用load 函数会自动调用这里
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
//获得 JniEnv
int r = (*vm)->GetEnv(vm,(void **) &env, JNI_VERSION_1_4);
if (r != JNI_OK) {
return -1;
}
jclass mainActivityCls = (*env)->FindClass(env,mClassName); // 注册 如果小于0则注册失败
r = (*env)->RegisterNatives(env,mainActivityCls, mMethods, 3);
if (r != JNI_OK) {
return -1;
}
return JNI_VERSION_1_4;
}
完整的java调用代码
package zcwfeng;
import java.*;
public class Register{
native void dynamicFun1();
native String dynamicFun2();
native int getRandom();
-> 注意一下这里,windows 的库是dll,而mac里面是dylib
static{
System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
}
public native String HelloWorld();
public static void main(String[] args){
Register r = new Register();
String re = r.HelloWorld();
System.out.println(re);
r.dynamicFun1();
System.out.println(r.dynamicFun2());
System.out.println(r.getRandom());
}
}
CMakeList 我的配置参考
cmake_minimum_required(VERSION 3.15.3)
project(TempC)
set(CMAKE_CXX_STANDARD 11)
-> 编译生成库,这里注释掉
#add_executable(TempC zcwfeng_Register.c zcwfeng_Register.h dyReg.c src/main.cpp)
add_library(firstlib SHARED zcwfeng_Register.h zcwfeng_Register.c dyReg.c)
System.load 和 System.loadLibrary 的区别
System.load
System.load 参数必须为库文件的绝对路径,可以是任意路径,例如: System.load("C:\Documents and Settings\TestJNI.dll"); //Windows
System.load("/usr/lib/TestJNI.so"); //Linux
System.loadLibrary
System.loadLibrary 参数为库文件名,不包含库文件的扩展名。 System.loadLibrary ("TestJNI"); //加载Windows下的TestJNI.dll本地库 System.loadLibrary ("TestJNI"); //加载Linux下的libTestJNI.so本地库
注意:TestJNI.dll 或 libTestJNI.so 必须是在JVM属性java.library.path所指向的路径中。一般我们老程序员和喜欢是用命令的配置过java环境没问题
-------------------------------------忧伤的分割线------------------------
JNIEnv类型和jobject类型的解释
-> 「这里JNIEXPORT 与 JNICALL 都是JNI的关键字,表示此函数是要被JNI调用的,无需过多解释」
JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello
(JNIEnv * env, jobject obj) {
printf(hello);
}
JNIEnv* env参数的解释
JNIEnv类型 实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。
例如,创建Java类中 的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对 Java端的代码进行操作。如下代码所示
#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
......
struct JNIInvokeInterface_;
......
struct JNINativeInterface_ {
void *reserved0;
void *reserved1;
void *reserved2;
void *reserved3;
jint (JNICALL *GetVersion)(JNIEnv *env);
//全是函数指针
jclass (JNICALL *DefineClass)
(JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
jsize len);
jclass (JNICALL *FindClass)
(JNIEnv *env, const char *name);
jmethodID (JNICALL *FromReflectedMethod)
(JNIEnv *env, jobject method);
jfieldID (JNICALL *FromReflectedField)
(JNIEnv *env, jobject field);
jobject (JNICALL *ToReflectedMethod)
(JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
... }
参数:jobject obj的解释
如果native方法不是static的话,这个obj就代表这个native方法的类实例。
如果native方法是static的话,这个obj就代表这个native方法的类的class对象实例(static方法不需要类实例的,所 以就代表这个类的class对象)。
java
public native void test();
public static native void testStatic();
jni代码.h
->「JNIEnv *, jobject 代表()空参数」
JNIEXPORT void JNICALL Java_Hello_test
(JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_Hello_testStatic
(JNIEnv *, jclass);
C/C++代码调用Java代码
在JNI中还有 一个非常重要的内容,那便是在C/C++本地 代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI 在jni.h头文件中定义了jfieldId,jmethodID类型来分别代表Java端的属性和方法。
我们在访问,或者设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能在本地代码 中进行Java属性操作,同样的,我们需要呼叫Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java 方法调用。
使用JNIEnv的:GetFieldID/GetMethodID GetStaticFieldID/GetStaticMethodID 来取得相应的jfieldID和jmethodID。
下面来具体看一下这几个方法:
GetFieldID(jclass clazz,const char* name,const char* sign) 方法的参数说明:
clazz:这个简单就是这个方法依赖的类对象的class对象 name:这个是这个字段的名称 sign:这个是这个字段的签名(我们知道每个变量,每个方法都是有签名的)
怎么查看类中的字段和方法的签名
使用javap命令:
javap -s -p JniTes.class
java 字段的签名
几个demo里的方法签名,也是常用的
int->()I
String->Ljava/lang/String;
void->()V
int [ ] ->[I
float [ ]-> [F
String [ ] -> [Ljava/lang/String;
int [ ][ ] -> [[I
float[ ][ ] -> [[F
Java层方法 JNI函数签名
String test ( ) Ljava/lang/String;
int f (int i, Object object) (ILjava/lang/Object;)I
void set (byte[ ] bytes) ([B)V
对应于Java端的数据类型,我们也可以看一下下面的表:
Java 类型 类型签名
boolean Z
byte B
char C
short S
int I
long L
float F
double D
类 L全限定名;,比如String, 其签名为Ljava/lang/util/String;
数组 [类型签名, 比如 [B
JNI 访问字符串
- java内部使用的是utf-16 16bit 的编码方式
- jni 里面使用的utf-8 unicode编码方式 英文是1个字节,中文 3个字节
- C/C++ 使用 ascii编码 ,中文的编码方式 GB2312编码 中文 2个字节
public native static String sayHello(String text);
->JNI
JNIEXPORT jstring JNICALL Java_JString_sayHello
(JNIEnv * env, jclass jclaz, jstring jstr) {
const char * c_str = NULL;
char buf[128] = {0};
jboolean iscopy;
c_str = (*env)->GetStringUTFChars(env, jstr, &iscopy);
printf("isCopy:%d\n", iscopy);
if(c_str == NULL) {
return NULL;
}
printf("C_str: %s \n", c_str); sprintf(buf,"Hello, 你好 %s", c_str); printf("C_str: %s \n", buf); (*env)->ReleaseStringUTFChars(env, jstr, c_str); return (*env)->NewStringUTF(env,buf);
}
sayHello函数接收一个jstring类型的参数text,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型 char*不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字 符串数据结构。
异常处理
if(c_str == NULL) {
return NULL;
}
以上代码是很关键的,调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空
间,
当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个 OutOfMemoryError异常。
JNI的异常和Java中的异常处理流程是不一样的,
Java遇到异常如果没有捕获,程序会立即停止 运行。
而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非 常危险的,
因此,我们需要用return语句跳过后面的代码,并立即结束当前方法。
释放字符串
在调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串 的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用 ReleaseStringUTFChars函数
通知JVM这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用 了GetXXX
就必须调用ReleaseXXX
,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一 样。
访问Java 成员变量
非静态变量
java代码
public int property;
Jni代码
JNIEXPORT void JNICALL Java_Hello_testField(JNIEnv *env, jobject jobj) {
->「首先调用GetObjectClass函数获取ClassField的Class引用」
jclass claz = (*env)->GetObjectClass(env,jobj);
-> 「然后调用GetFieldID函数从Class引用中获取字段的ID(property是字段名,I是字段的签名)」
jfieldID jfid = (*env)->GetFieldID(env, claz, "property","I");
-> 「最后调用GetIntField函数,传入实例对象和字段ID,获取属性的值」
jint va = (*env)->GetIntField(env,jobj, jfid);
printf("va: %d", va);
-> 「最后调用GetIntField函数,传入实例对象和字段ID,获取属性的值」
(*env)->SetIntField(env, jobj, jfid, va + 10086);
}
访问静态变量
访问静态变量和实例变量不同的是,获取字段ID使用GetStaticFieldID,获取和修改字段的值使用
Get/SetStaticXXXField系列函数,比如获取和修改静态变量num:
num = (*env)->GetStaticIntField(env,clazz,fid);
// 4.修改静态变量num的值
(*env)->SetStaticIntField(env, clazz, fid, 80);
总结下
1、由于JNI函数是直接操作JVM中的数据结构,不受Java访问修饰符的限制。即,在本地代码中可以调用JNI函数可以
访问Java对象中的非public属性和方法
2、访问和修改实例变量操作步聚:
1>、调用GetObjectClass函数获取实例对象的Class引用
2>、调用GetFieldID函数获取Class引用中某个实例变量的ID
3>、调用GetXXXField函数获取变量的值,需要传入实例变量所属对象和变量ID
4>、调用SetXXXField函数修改变量的值,需要传入实例变量所属对象、变量ID和变量的值
3、访问和修改静态变量操作步聚:
1>、调用FindClass函数获取类的Class引用
2>、调用GetStaticFieldID函数获取Class引用中某个静态变量ID
3>、调用GetStaticXXXField函数获取静态变量的值,需要传入变量所属Class的引用和变量ID
4>、调用SetStaticXXXField函数设置静态变量的值,需要传入变量所属Class的引用、变量ID和变量的值
访问Java中的函数
Java成员函数一般有两类:静态和非静态。所以在JNI中对这两种不同的类型就有了两种不太相同的调用方法,这两 种不同类型虽然他们的调用方式有些许不同,但是,他们的实质上是一样的。只是调用的接口的名字有区别,而对于 流程是没有区别的。
private static void callStaticMethod(String str, int i) {
System.out.format("ClassMethod::callStaticMethod called!-->str=%s,"
+" i=%d\n", str, i);
JNI
JNIEXPORT void JNICALL Java_JniTest_callJavaStaticMethod(JNIEnv *env, jobject jobje){
->「1:从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象」
jclass clz = (*env)->FindClass(env,"ClassMethod");
if(clz == NULL) {
printf("clz is null");
return;
}
->「2:从clazz类中查找callStaticMethod方法」
jmethodID jmeid = (*env)->GetStaticMethodID(env, clz, "callStaticMethod", "
(Ljava/lang/String;I)V");
if(jmeid == NULL) {
printf("jmeid is null");
return;
}
jstring arg = (*env)->NewStringUTF(env, "我是静态类");
->「2:从clazz类中查找callStaticMethod方法」
(*env)->CallStaticVoidMethod(env,clz,jmeid,arg,100);
(*env)->DeleteLocalRef(env,clz);
(*env)->DeleteLocalRef(env,arg);
}
JNI引用
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用
(Weak Global Reference)。
局部引用
通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC 回收所引用的对象,不能本地函数中跨函数使用,不能跨线程使用。
函数返回后局部引用所引用的对象会被JVM自动 释放,或调用DeleteLocalRef释放。 (*env)->DeleteLocalRef(env,local_ref)
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建 ...
释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放; VM会自动释放局部引用,
为什么还需要手动释放呢?
因为局部引用会阻止它所引用的对象被GC回收。
全局引用
调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,
必须调用DeleteGlobalRef手动释放 (*env)->DeleteGlobalRef(env,g_cls_string);
tic jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}
弱全局引用
调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使 用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用 DeleteWeakGlobalRef手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)
static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
jclass cls_string = (*env)->FindClass(env, "java/lang/String");
g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}
拼接字符串例子
->java
public static native String sayHello(String ret);
->jni
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_sayHello(JNIEnv *env,jobject obj,jstring str){
->「将jstring转换成c可以用的char *」
const char * c_str = NULL;
jboolean isCopy;
c_str = (*env)->GetStringUTFChars(env,str,&isCopy);
if(c_str == NULL)
return NULL;
-> 1. malloc 2.初始化 3.释放----buf[128] = {0} 相当于1,2
char buf[128] = {0};
-> c里面的拼接,std::copy函数c++
sprintf(buf,"Hello,你好:%s",c_str);
-> 注意这里千万别忘记
(*env) -> ReleaseStringUTFChars(env,str,c_str);
return (*env)->NewStringUTF(env, buf);
}
获取Field实例
package zcwfeng;
public class FieldTest{
static{
System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
}
public int property = 1;
public static int statProperty = 8;
public native void testField();
public native void testStaticField();
public static void main(String[] args){
FieldTest t = new FieldTest();
t.testField();
t.testStaticField();
System.out.println(t.property);
System.out.println(t.statProperty);
}
}
-> JNI
JNIEXPORT void JNICALL Java_zcwfeng_FieldTest_testField(JNIEnv *env,jobject obj){
// (*env)->FindClass(env,)
jclass clazz = (*env)->GetObjectClass(env,obj);
jfieldID jfid = (*env)->GetFieldID(env,clazz,"property","I");
jint value = (*env)->GetIntField(env,obj,jfid);
printf("java 原始值:%d",value);
(*env)->SetIntField(env,obj,jfid,value + 10000);
}
JNIEXPORT void JNICALL Java_zcwfeng_FieldTest_testStaticField(JNIEnv *env,jobject obj){
// (*env)->FindClass(env,)
jclass clazz = (*env)->GetObjectClass(env,obj);
jfieldID jfid = (*env)->GetStaticFieldID(env,clazz,"statProperty","I");
jint value = (*env)->GetStaticIntField(env,obj,jfid);
printf("java 原始值:%d",value);
(*env)->SetStaticIntField(env,obj,jfid,value + 10000);
}
获取方法实例
-> java 静态方法和非静态方法
package zcwfeng;
public class ClassMethod{
private static void callStaticMethod(String str,int i){
System.out.format("ClassMethod::callStaticMethod called!-str=%s, " + "i=%d\n",str,i);
}
private void callInstanceMethod(String str,int i){
System.out.format("ClassMethod::callInstanceMethod called!-str=%s, " + "i=%d\n",str,i);
}
}
-> JNI
// 调用java构造方法
JNIEXPORT void JNICALL Java_zcwfeng_Register_callJavaStaticMethod(JNIEnv *env,jobject obj){
jclass jclz = (*env) -> FindClass(env,"zcwfeng/ClassMethod");
if(jclz == NULL){
printf("jclz is null");
return;
}
jmethodID jconstructorId = (*env) -> GetMethodID(env,jclz,"<init>","()V");
if(jconstructorId == NULL){
printf("jconstructorId is null");
return;
}
jobject jobj = (*env)->NewObject(env,jclz,jconstructorId);
jmethodID jmid = (*env) -> GetStaticMethodID(env,jclz,"callStaticMethod","(Ljava/lang/String;I)V");
jstring jstr = (*env)->NewStringUTF(env,"I am from JNI zcwfeng");
-> 「注意一下这里CallStaticVoidMethod,参数是class而不是jobject」
(*env)->CallStaticVoidMethod(env,jclz,jmid,jstr,1008600);
(*env)->DeleteLocalRef(env,jclz);
(*env)->DeleteLocalRef(env,jobj);
(*env)->DeleteLocalRef(env,jstr);
}
// 调用java构造方法
JNIEXPORT void JNICALL Java_zcwfeng_Register_callJavaInstanceMethod(JNIEnv *env,jobject obj){
jclass jclz = (*env) -> FindClass(env,"zcwfeng/ClassMethod");
if(jclz == NULL){
printf("jclz is null");
return;
}
jmethodID jconstructorId = (*env) -> GetMethodID(env,jclz,"<init>","()V");
if(jconstructorId == NULL){
printf("jconstructorId is null");
return;
}
jobject jobj = (*env)->NewObject(env,jclz,jconstructorId);
jmethodID jmid = (*env) -> GetMethodID(env,jclz,"callInstanceMethod","(Ljava/lang/String;I)V");
jstring jstr = (*env)->NewStringUTF(env,"I am from JNI David\n");
(*env)->CallVoidMethod(env,jobj,jmid,jstr,10086);
(*env)->DeleteLocalRef(env,jclz);
(*env)->DeleteLocalRef(env,jobj);
(*env)->DeleteLocalRef(env,jstr);
}
野指针实例
特别注意局部引用static的情况释放
DeleteLocalRef
并将对应的 引用智控
区别什么是引用,GetMethodID方法不是
-> java 本地方法
public native String JCallC();
public native String newString(int len);
。。。
Register r = new Register();
r.JCallC();
r.JCallC();
-> 逻辑是 JCallC()调用一个jni方法,jni内部调用newString方法
-> JNI
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_newString(JNIEnv *env, jobject obj, jint len) {
jcharArray elementArray;
jstring j_str = NULL;
static jclass cls_string = NULL;
static jmethodID jmetid = NULL;
if (cls_string == NULL) {
printf("cls_string is null \n");
cls_string = (*env)->FindClass(env, "java/lang/String");
if (cls_string == NULL) {
return NULL;
}
}
if (jmetid == NULL) {
printf("jmetid is null \n");
jmetid = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
if (jmetid == NULL) {
return NULL;
}
}
printf("this is a line ----------------------- \n");
elementArray = (*env)->NewCharArray(env, len);
printf("this is a line2 ----------------------- \n");
printf("this is a %d,%d,%d\n", cls_string, jmetid, elementArray);
j_str = (*env)->NewObject(env, cls_string, jmetid, elementArray);
printf("this is a line3 ----------------------- \n");
(*env)->DeleteLocalRef(env, elementArray);
elementArray = NULL;
(*env)->DeleteLocalRef(env, cls_string);
-> 「static静态引用,注释掉这句,下次在调用JCallC,就会导致野指针问题,因为开始判断为NULL,创建,现在没有=NULL,就会导致:指针指向的内存块内容为空」
cls_string = NULL;
-> 「这句是错的因为jmetid---GetMethodID 不是LocakRef 他不是引用类型」
//////// (*env)->DeleteLocalRef(env,jmetid);
jmetid = NULL;
printf("end of function\n");
return j_str;
}
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_JCallC(JNIEnv *env, jobject obj) {
return Java_zcwfeng_Register_newString(env, obj, 1024);
}
网友评论