JNI开发流程:
1. 编写JAVA代码,并且在JAVA代码中申明需要用到的native方法
2. 编译JAVA代码,生成字节码文件 (javac)
3. 导出JAVA代码中的native方法,生成头文件 (javah)
4. 新建cpp源文件,含生成的头文件,在cpp源代码中实现native方法
5. 使用cpp源文件和头文件编译出so动态库
6. 在JAVA代码中通过System.LoadLibrary加载so文件
JavaVM: 在JNI中JavaVM代表的是Java虚拟机,每个Java进程有且只有一个全局的JavaVM对象,JavaVM对象可以跨线程共享
JNIEnv: 代表Java运行环境(上下文),一般是对应了Java的线程上下文,JNIEnv对象不能跨线程共享
JavaVM和JNIEnv内部都包含了一组Java虚拟机内部的函数指针,只是JavaVM和JNIEnv包含的函数指针不同。
JavaVM和JNIEnv只是一个统称,具体不同的JAVA虚拟机都有自己的实现,而对于外部,无论是C还是C++,都是以JJNIInvokeInterface和NINativeInterface指针的形式开放给用户的。
/*
* JavaVM
*/
struct JNIInvokeInterface {
// 一系列函数指针
jint (*DestroyJavaVM)(JavaVM*);
jint (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
jint (*DetachCurrentThread)(JavaVM*);
jint (*GetEnv)(JavaVM*, void**, jint);
jint (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};
/*
* JNIEnv
*/
struct JNINativeInterface {
// 一系列函数指针
jint (*GetVersion)(JNIEnv *);
jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
jclass (*FindClass)(JNIEnv*, const char*);
...
};
字符串:
1. JNIEnv提供GetStringUTFChars JNI方法将jstring引用转换为c/c++字符串,使用完后调用ReleaseStringChars释放
2. JNIEnv提供NewStringUTF JNI方法构造一个新的JAVA String字符串对象,不需要通过ReleaseStringChars释放
数组:
1. 基本类型有自己对应的array类型(jintArray, jbooleanArray等)
2. 引用类型的array类型为jobjectArray
3. JNIEnv提供了一组NewXXXArray(NewIntArray, NewBooleanArray...)接口来创建JAVA中的基本类型的数组
4. JNIEnv提供了一组GetXXXArrayElements(GetIntArrayElements,GetBooleanArrayElements等)接口用于将JAVA类型的数组转换为C/C++的数组
JNI中创建JAVA数组对象+赋值修改+返回的操作流程
1. 通过JNIEnv的NewXXXArray接口创建JAVA数组对象
2. 通过JNIEnv的GetXXXArrayElements(isCopy参数要设置为JNI_False,即复用模式,会修改原JAVA数组的内容)接口将刚才创建的JAVA数组对象转换成C/C++的数组(两者实际指向的是同一个对象)
3. 修改C/C++对象(例如赋值)
4. 通过JNIEnv的Release接口释放,同时因为isCopy设置为JNI_False,修改的内容会写回JAVA数组对象
5. 返回第一步创建的JAVA数组对象
JAVA中的属性和方法在class文件中都是由三个部分组成的(访问控制符 + 简单名称 + 描述符),其中最重要的是名称和描述符
名称:顾名思义就是函数名成/属性名称
描述符:分为字段描述符和方法描述符
字段描述符:对应JAVA数据类型,每个JAVA数据类型都有对应的描述符
Java 类型 | 描述符 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
floag | F |
double | D |
void | V |
引用类型 | 以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String; |
方法描述符:通过字段描述符来完整描述方法的入参以及返回值,例如 ()V代表void func(), (ZLjava/lang/String)V代表void func(boolean, String)
JNI访问JAVA对象中的字段(属性)
获取属性的ID (JNIEnv::GetFieldId(jclass, 字段名称, 字段描述符),如果是静态字段则需要用JNIEnv::GetStaticFieldId
通过字段ID访问字段 (JNIEnv::GetXXXField(jobject thiz, 字段ID),GetXXXField根据要获取的字段的类型,例如GetIntField, GetBooleanField, GetStaticIntField等
通过字段ID修改字段 (JNIEnv::SetXXXField(jobject thiz, 字段ID, value), SetXXXField类似GetXXXField
步骤:
1. 通过JNI接口传入的jobject获取到this引用 jobject thiz
2. 使用JNIEnv::GetObjectClass获取this对象的类型对象jobject clz
3. 使用JNIEnv::GetFieldId从clz中获取属性的ID
4. 使用JNIEnv::GetXXXField/SetXXXField接口(传参 thiz + 属性ID)获取和设置属性值
JNI调用JAVA对象中的方法
同访问JAVA对象的属性类似,也是需要先获取方法ID,然后才能调用方法
1. JNIEnv::GetMethodId
2. JNIEnv::Call<Type>Method (Type对应返回类型)
引用:
JNI中的引用分为全部引用以及局部引用,这个引用就是java中的引用类型,引用了某个具体的java对象,释放引用类似与java语言中的=null操作
局部引用只在JNI方法内部有效,可以在方法结束前手动调用DeleteLocalRef函数释放对对象的局部引用,或者等方法结束时会自动释放该应用
全局引用在整个JVM中有效,因此,除非调用DeleteGlobalRef,否则引用一直存活(导致引用的对象不被GC回收)
局部引用不能跨方法,更不能跨线程访问
例如,有这样的场景,我们的某个业务JAVA类有配套的JNI层的cpp文件,在这个文件中希望保存所以有调用过该cpp文件中JNI函数的JAVA类的对象,那么就需要在JNI函数中,通过JNIEnv的NewGlobalRef函数,将局部应用(JNI函数的jobject参数)转换成JNI层的全局引用。
那么后面如果想要在JNI层的cpp文件中判断调用JNI接口的JAVA类对象是否之前已经调用过该JNI接口,那么需要通过JNIEnv的IsSameObject方法比对此次调用传入的jobject引用(引用了本次调用的java对象)和之前保存在JNI层的全局引用是否有匹配的。
JAVA ByteBuffer转C++字符串:
void saveString(JNIEnv* env, jobject thiz, jobject buffer, int size) // 第三个jobject就是JAVA层传入的ByteBuffer, 第四个参数是ByteBuffer的长度
{
char* nativeMessage = reinterpret_cast<char*>(env->GetDirectBufferAddress(buffer);
if (nativeMessage) {
std::string msg(nativeMessage, nativeMessage + size);
}
...
}
JAVA String转C++字符串:
void getJavaString(JNIEnv* env, jobject thiz, jstring str)
{
const char* nativeString = env->GetStringUTFChars(str, nullptr);
...
env->ReleaseStringUTFChars(str, nativeString); // 不做这一步其实也可以,因为除了函数引用会被删除,不会导致java层的字符串无法被GC回收
}