上一篇博客介绍了NDK简介和环境的搭建以及一个简单的Demo,这篇准备总结一下JNI调用Java对象以及在JNI中开启线程。
ps:这里说明一下,我是用Android Studio开发的,如果是用Eclipse开发的朋友,是不能直接导入我的程序,而且项目的结构和我的是有区别的。
通过JNI在Native层调用JAVA层的方法,来实现Native层向JAVA层传递消息。
我的项目结构:
public class MyJni {
public static final String TAG = "MyNdkTest";
/**
* 载入动态库
*/
static {
System.loadLibrary("MyJni");
}
/**
* 从JNI获取字符串
*
* @return
*/
public static native String getStringFromNative();
/**
* 初始化native中创建线程需要的变量
*/
public native void nativeInitialize();
/**
* 创建native中线程
*/
public native void nativeThreadStart();
/**
* 停止native中线程
*/
public native void nativeThreadStop();
/**
* 从native代码中回调Java的方法入口
*/
public native void nativeCallback();
/**
* 从native代码中回调(非静态)
* 在Jni中开启线程
*/
public void onNativeThreadCallback(String str) {
Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
Log.i(TAG, "onNativeThreadCallback = " + str);
}
/**
* 从native代码中回调Java(非静态)
*/
public void onNativeCallback(String str) {
Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
Log.i(TAG, "onNativeCallback = " + str);
}
/**
* 从native代码中回调(非静态)
* 在Jni中开启线程
*/
public static void onNativeStaticCallback(int count) {
Log.i(TAG, "Thread ID = " + Thread.currentThread().getId());
}
}
如上面代码所示,Java层与JNI层的接口代码主要封装在Native类中,该类定义了五个native函数,分别是从jni层获取字符串,完成jni库的初始化,调用jni层开启线程,调用jni层关闭线程等功能。并且提供一个回调函数(一个为在开启的线程中回调,另一个是在jni开启的线程中回调),供jni层调用,并在回调函数中打印线程的Id和传递过来的字符串。这里先不讲解如何在JNI中创建线程。
2. jni中回调java层的函数
这里创建头文件的方法和Android Studio下配置NDK的环境已经在前一篇叙述过,这里就不说了。在头文件中定义了这五个函数,MyJni.c是实现五个native函数的主要类:
JNIEXPORT void JNICALL
Java_com_ndk_MyJni_nativeCallback(JNIEnv *env, jobject instance) {
onNativeCallback(env, "主线程中回调java函数");
}
这里只贴出了在主线程回调java函数的代码,这里代码很简单,就是调用了onNativeCallback,然而这个函数实在哪里实现的呢?大家可以再看一下我的项目截图,就是在CallJava.c中实现的。代码中重要的部分我都有注释。
#include "CallJava.h"
#include "com_ndk_MyJni.h"
/**
* C回调Java方法(非静态)
*/
void onNativeCallback(JNIEnv *env, jstring str) {
// 获取类
jclass gjclass = (*env)->FindClass(env, "com/ndk/MyJni");
if (NULL == gjclass) {
return;
}
// 实例化类对象
jobject gjobject = getInstance(env, gjclass);
if (NULL == gjobject) {
(*env)->DeleteLocalRef(env, gjclass); // 删除类指引
LOGI("删除类指引 !");
return;
}
// 获取对象callback方法
jmethodID callback = (*env)->GetMethodID(env, gjclass, "onNativeCallback",
"(Ljava/lang/String;)V");
if (NULL == callback) {
(*env)->DeleteLocalRef(env, gjclass); // 删除类指引
(*env)->DeleteLocalRef(env, gjobject); // 删除类对象指引
LOGI("删除类对象指引 !");
return;
}
// 调用非静态int方法
(*env)->CallVoidMethod(env, gjobject, callback, (*env)->NewStringUTF(env, str));
}
/**
* 实例化类对象
*/
jobject getInstance(JNIEnv *env, jclass clazz) {
// 获取构造方法
jmethodID constructor = (*env)->GetMethodID(env, clazz, "" , "()V");
if (NULL == constructor) {
return NULL;
}
// 实例化类对象
return (*env)->NewObject(env, clazz, constructor);
}
大家可以看到,在JNI中回调Java层的函数需要四步:1、获得一个Java类的class引用(*env)->FindClass(env, “com/ndk/MyJni”),第二个参数代表这个类的相对路径。2、实例化该类,(*env)->GetMethodID(env, clazz, “”, “()V”),第二个是刚刚获得的class引用,第三个是方法的名称(这里是构造函数),最后一个就是方法的签名了(下一章节会详细介绍)。3、获取到调用该对象的方法。4、回调该方法。
这样一个主线程的回调就完成了,这里大家在使用(*env)->GetMethodID时,注意第三个和第四个参数一定要和你java中函数名称和参数类型及个数相对应,不然会报错。
#include "com_ndk_MyJni.h"
JNIEXPORT jstring JNICALL
Java_com_ndk_MyJni_getStringFromNative(JNIEnv *env, jclass type) {
return (*env)->NewStringUTF(env, "I am from native");
}
/*
* Class: com_ticktick_jnicallback_Native
* Method: 设置全局变量
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeInitialize(JNIEnv *env,
jobject thiz) {
//注意,直接通过定义全局的JNIEnv和jobject变量,在此保存env和thiz的值是不可以在线程中使用的
//线程不允许共用env环境变量,但是JavaVM指针是整个jvm共用的,所以可以通过下面的方法保存JavaVM指针,在线程中使用
(*env)->GetJavaVM(env, &gJavaVM);
//同理,jobject变量也不允许在线程中共用,因此需要创建全局的jobject对象在线程中访问该对象
gJavaObj = (*env)->NewGlobalRef(env, thiz);
}
static void *native_thread_exec(void *arg) {
JNIEnv *env;
//从全局的JavaVM中获取到环境变量
(*gJavaVM)->AttachCurrentThread(gJavaVM, &env, NULL);
//获取Java层对应的类
jclass javaClass = (*env)->GetObjectClass(env, gJavaObj);
if (javaClass == NULL) {
LOGI("Fail to find javaClass");
return 0;
}
//获取Java层被回调的函数
jmethodID javaCallback = (*env)->GetMethodID(env, javaClass, "onNativeThreadCallback", "(I)V");
if (javaCallback == NULL) {
LOGI("Fail to find method onNativeCallback");
return 0;
}
LOGI("native_thread_exec loop enter");
int count = 0;
//线程循环
while (!gIsThreadExit) {
//回调Java层的函数
(*env)->CallVoidMethod(env, gJavaObj, javaCallback, count++);
//休眠1秒
sleep(1);
}
(*gJavaVM)->DetachCurrentThread(gJavaVM);
LOGI("native_thread_exec loop leave");
}
/*
* Class: com_ticktick_jnicallback_Native
* Method: 开启线程
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStart(JNIEnv *env,
jobject thiz) {
gIsThreadExit = 0;
//通过pthread库创建线程
pthread_t threadId;
if (pthread_create(&threadId, NULL, native_thread_exec, NULL) != 0) {
LOGI("native_thread_start pthread_create fail !");
return;
}
LOGI("native_thread_start success");
}
/*
* Class: com_ticktick_jnicallback_Native
* Method: NativeThreadStop
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_com_ndk_MyJni_nativeThreadStop(JNIEnv *env,
jobject thiz) {
gIsThreadExit = 1;
LOGI("native_thread_stop success");
}
第一个本地方法:返回一个字符串。
第二个本地方法:是初始化一些全局变量,在开启线程时使用。
第三个本地方法:是开启线程。
第四个本地方法:是关闭线程
大部分代码是有注释的,大家应该是可以看懂的。
JNINativeMethod的定义如下:
typedef struct {
const char* name;
const char* signature;
void* fnPtr;
} JNINativeMethod;
第一个变量name是Java中函数的名字。
第二个变量signature,用字符串是描述了函数的参数和返回值
第三个变量fnPtr是函数指针,指向C函数。
其中比较难以理解的是第二个参数,例如
“()V”
“(II)V”
“(Ljava/lang/String;Ljava/lang/String;)V”
实际上这些字符是与函数的参数类型一一对应的。
“()” 中的字符表示参数,后面的则代表返回值。例如”()V” 就表示void Func();
“(II)V” 表示 void Func(int, int);
1、方法参数或者返回值为java中的对象时,签名中必须以“L”加上其路径,不过此路径必须以“/”分开,自定义的对象也使用本规则
比如说 java.lang.String为“java/lang/String”,com.nedu.jni.helloword.Student为”Lcom /nedu/jni/helloword/Student;”
2、方法参数或者返回值为数组类型时,请前加上[
例如[I表示 int[],[[[D表示 double[][][],即几维数组就加几个[