1.生成JNI头文件
javac -h . Test.java
注意正确配置Java环境变量
可使用javac -encoding utf8 -h . Test.java
指定源文件编码格式
.
表示输出c++头文件在当前目录
2.JavaVM与JNIEnv
(1)JavaVM:Java虚拟机在JNI层的代表。表示虚拟机对象指针,一个虚拟机对应一个JavaVM,Android一个进程仅一个JavaVM。
jni.h
struct JavaVM_ {
const struct JNIInvokeInterface_ *functions;
#ifdef __cplusplus
jint DestroyJavaVM() {
return functions->DestroyJavaVM(this);
}
jint AttachCurrentThread(void **penv, void *args) {
return functions->AttachCurrentThread(this, penv, args);
}
jint DetachCurrentThread() {
return functions->DetachCurrentThread(this);
}
jint GetEnv(void **penv, jint version) {
return functions->GetEnv(this, penv, version);
}
jint AttachCurrentThreadAsDaemon(void **penv, void *args) {
return functions->AttachCurrentThreadAsDaemon(this, penv, args);
}
#endif
};
(2)JNIEnv:JavaVM在线程中的代表,JNI方法。线程相关的结构体, 该结构体代表了Java在本线程的运行环境。仅在创建它的线程生效,不能跨线程传递,不同线程的 JNIEnv彼此独立,相同线程的JNIEnv也相同。Java层访问Native层时会自动创建JNIEnv,即对应的native方法自动携带了JNIEnv指针参数,且native方法执行的线程和Java访问进来的线程相同。
jni.h
/*
* We use inlined functions for C++ so that programmers can write:
*
* env->FindClass("java/lang/String")
*
* in C++ rather than:
*
* (*env)->FindClass(env, "java/lang/String")
*
* in C.
*/
struct JNIEnv_ {
const struct JNINativeInterface_ *functions;
#ifdef __cplusplus
jint GetVersion() {
return functions->GetVersion(this);
}
jclass DefineClass(const char *name, jobject loader, const jbyte *buf,
jsize len) {
return functions->DefineClass(this, name, loader, buf, len);
}
···
(3)JavaVM->AttachCurrentThread((void **)env, NULL);与JavaVM->DetachCurrentThread();
native环境中创建的线程,如果需要访问JNI(比如c++再调回java),必须要调用 AttachCurrentThread关联,线程结束后使用DetachCurrentThread解除链接。
(4)JNIEnv与JavaVM可相互获取
通过保存的JavaVM获取JNIEnv
JNIEnv *env = NULL;
int getEnvStat = javaVM->GetEnv((void **) &env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
if (g_vm->AttachCurrentThread(&env, NULL) != 0) {
return;
}
}
通过JNIEnv->GetJavaVM()获取JavaVM
JNIEXPORT void JNICALL
Java_com_test_hello_world(JNIEnv *env, jclass clazz) {
env->GetJavaVM(&javaVM);
}
3.JNI方法签名(官方文档Type Signatures)
C++调Java方法时JNIEnv->GetMethodID()第三个参数需传入Java的方法签名,用于寻找对应的Java方法。
由于函数重载所以方法返回值或者参数类型不一致也会是不同函数,JNI通过方法签名区分。
签名格式:(参数的类型签名) + 返回值类型签名
可将格式拆分为参数和返回值,将一个一个的参数进行拼接即可
(1)基本数据类型签名
基本数据类型签名一般是首字母大写,boolean例外,是因为B已经被 byte占用。long的标签不是L,因为L已经被表示为类的签名。
Java基本类型 | 签名 |
---|---|
boolean | Z |
byte | B |
char | C |
short | S |
int | I |
long | J |
float | F |
double | D |
void | V |
可以看到,基本数据类型签名一般是首字母的大写,boolean例外,是因为B已经被 byte占用。long的标签之所以不是L,是因为L表示的是类的签名。
(2)对象签名
签名格式:L+包名+类名+;
包名使用/分隔,比如java.lang.String,它的签名为Ljava/lang/String;
(3)数组签名
签名格式:[类型签名
数组 | 签名 |
---|---|
char[] | [C |
float[] | [F |
double[] | [D |
long[] | [J |
String[] | [Ljava/lang/String; |
Object[] | [Ljava/lang/Object; |
举例:
void test() 签名为 ()V
int onRecordingEnd(double[] arr1, double[] array2) 签名为 ([D[D)I
4.JNI局部引用内存溢出
这里的引用指的是通过NewObject等函数创建Java对象时返回的对象引用。
线程从Java环境切换到本地代码上下文时,JVM会分配一块内存,创建一个局部引用表来存放本次本地方法执行中创建的所有局部引用。局部引用的生命期是在本地方法的执行期,在本地方法执行完毕切换回Java方法时,所有JNI局部引用被删除,生命期结束。总结局部引用释放的四种方式如下:
- 主动调用DeleteLocalRef(obj)释放
- 本地方法执行完毕切换回Java方法
- 使用PushLocalFrame/PopLocalFrame创建/销毁局部引用栈帧
- 线程调用DetachCurrentThread与JVM断开连接
本地方法如果需要创建过多的局部引用,那么在对被引用的Java对象操作结束后,需要调用DeleteLocalRef()及时删除局部引用,否则将避免造成OOM,日志打印如下:
local reference table overflow
方法如下:
void DeleteLocalRef(JNIEnv *env, jobject localRef);
注意:局部引用不能被保存,只能在本地方法生命期内使用。
局部引用不是局部变量,局部变量存储在本地线程堆栈中,而局部引用存储在局部引用表中的。局部变量在函数退栈后被删除,局部引用则不会,直到调用DeleteLocalRef() 或者是整个本地方法执行结束。
5.JNI全局引用内存泄漏
JNI全局引用对Java对象的引用一直有效,因此它们引用的Java对象会一直存在Java堆中。
创建对obj参数引用的对象的新全局引用,obj参数可以是全局或局部引用:
jobject NewGlobalRef(JNIEnv *env, jobject obj);
删除globalRef指向的全局引用:
void DeleteGlobalRef(JNIEnv *env, jobject globalRef);
附:弱全局引用:除了全局引用和局部引用还有若全局引用,类似Java的弱引用WeakReference
同全局引用可在任何线程中使用,如果弱全局引用所引用的对象在其他位置无强引用,那么随时会被GC
env->NewWeakGlobalRef()
env->DeleteWeakGlobalRef(jweak)
6.JNI异常处理
主要JNI方法:
// jni.h
void ExceptionDescribe() // 打印异常堆栈
void ExceptionClear() // 清除当前正在抛出的异常
jboolean ExceptionCheck() // 检测是否抛出异常,通过布尔值返回是否有异常
jthrowable ExceptionOccurred() // 检测是否抛出异常,通过返回的对象确认,无异常返回空
jint Throw(jthrowable obj) // 抛出Java异常,执行后程序将Crash
jint ThrowNew(jclass clazz, const char* message) // 同上,参数不同,需构造指定的Java异常类并抛出,执行后程序将Crash
void FatalError(const char* msg) // 引发致命错误,执行后程序将Crash
代码举例:
jthrowable jthrow = env->ExceptionOccurred();
if (jthrow && env->ExceptionCheck()) {
env->ExceptionDescribe();
env->ExceptionClear();
}
------------------------------------
jclass arithmeticExceptionCls = env->FindClass("java/lang/ArithmeticException");
if (NULL == arithmeticExceptionCls) {
return;
}
jmethodID initMethodID = (env)->GetMethodID(arithmeticExceptionCls, "<init>", "(Ljava/lang/String;)V");
if (NULL == initMethodID) {
return;
}
char infoBuf[256] = {0};
sprintf(infoBuf, "Exception from native.");
jstring info = (env)->NewStringUTF(infoBuf);
jthrowable thr = (jthrowable) (env)->NewObject(arithmeticExceptionCls, initMethodID, info);
env->Throw(thr);
7.实现JNI调用与回调的例子
思路
1.JAVA写回调类,并通过Native方法传入回调类实例
2.Native方法中将传入的回调对象保存成全局引用(注意:全局引用在不使用时需主动delete释放)
3.需要回调时,通过JNIEnv从保存的全局引用中获取到要回调的类再直接调回调类的实例方法
定义Java回调类
public interface MyListener {
void callback();
}
定义native方法
public enum JNIHello {
INSTANCE;
static {
System.loadLibrary("jni_hello");
}
public static native void setListener(MyListener listener);
}
保存全局引用,Native中达到触发条件后,通过保存的全局引用进行回调
JavaVM *g_vm;
jobject g_objCallback; // 注意解注册监听后手动delete释放
JNIEXPORT void JNICALL
Java_com_hihonor_awareness_server_util_AAudioEngine_setListener(JNIEnv *env, jclass clazz, jobject jcallback) {
env->GetJavaVM(&g_vm);
g_objCallback = env->NewGlobalRef(jcallback);
if (g_objCallback == NULL) {
LOGE("NewGlobalRef g_objCallback == NULL");
return;
}
}
void MyListener::onCallback() {
JNIEnv *env = NULL;
int getEnvStat = g_vm->GetEnv((void **) &env, JNI_VERSION_1_6);
if (getEnvStat == JNI_EDETACHED) {
if (g_vm->AttachCurrentThread(&env, NULL) != 0) {
return;
}
}
// 通过强转后的jcallback获取到要回调的类
jclass javaClass = env->GetObjectClass(g_objCallback);
if (javaClass == NULL) {
LOGE("Unable to find class");
return;
}
jmethodID javaCallbackId = env->GetMethodID(javaClass, "callback",
"()V");
if ((env->ExceptionCheck()) || (javaCallbackId == NULL)) {
env->ExceptionClear();
LOGE("Unable to find method:onError");
return;
}
env->CallVoidMethod(g_objCallback, javaCallbackId);
if (env->ExceptionCheck()) {
env->ExceptionClear();
return;
}
}
也可以不传入回调类直接调用java侧方法,代码如下
jclass javaClass = env->FindClass("com/hello/world/MyClass");
jmethodID javaCallbackId = env->GetStaticMethodID(javaClass, "test","([D)V");
env->CallStaticVoidMethod(javaClass, javaCallbackId, paramData);
对应的Java类中的方法如下
private static void test(double[] values) {
// todo
}
8.回传参数
env->SetXxxField();
设置类成员值
env->SetStaticXxxField();
设置静态成员值
env->SetXxxArrayRegion();
给数组赋值
举例:回传double数组
double mlParam[10];
// ... 给数组赋值,也可以是Vector类型
jdoubleArray mlParamData = env->NewDoubleArray(10);
env->SetDoubleArrayRegion(mlParamData, 0, 10, &mlParam[0]);
jclass javaClass = env->FindClass("com/.../MyClass");
jmethodID javaCallbackId = env->GetStaticMethodID(javaClass, "test", "([D)V");
env->CallStaticVoidMethod(javaClass, javaCallbackId, mlParamData);
9.JNI线程同步
MonitorEnter(obj)函数获取此Java对象锁
MonitorExit(obj)函数释放锁
参考
JNIEnv和JavaVM
JNI方法签名
深入理解JNI内存泄漏
JNI内存管理
JNI手动释放内存(避免内存泄露)
Jni 线程JNIEnv,JavaVM,JNI_OnLoad
JNI完全指南(十)JavaVM与JNIEnv
JNI 从入门到实践
网友评论