您的位置:首页 > 移动开发 > Android开发

Android进阶之路——NDK(二)

2015-12-03 18:03 666 查看
  上一篇博客介绍了NDK简介和环境的搭建以及一个简单的Demo,这篇准备总结一下JNI调用Java对象以及在JNI中开启线程。

  ps:这里说明一下,我是用Android Studio开发的,如果是用Eclipse开发的朋友,是不能直接导入我的程序,而且项目的结构和我的是有区别的。

点击下载

一、JNI实现回调

  通过JNI在Native层调用JAVA层的方法,来实现Native层向JAVA层传递消息。

我的项目结构:



首先在java层注册native函数

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, "<init>", "()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中函数名称和参数类型及个数相对应,不然会报错。

  

二、在JNI中开启子线程

在java层注册native函数,与上一章节中的第一步一样,这里就不再赘述。

在JNI中实现开启线程的代码,也就是MyJni.c,上一章节中只是贴出了主线程回调的一个函数,这里将剩下的四个本地方法都贴出来。

#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[][][],即几维数组就加几个[



四、总结

在JNI_OnLoad中,保存JavaVM*,这是跨线程的,持久有效的,而JNIEnv*则是当前线程有效的。一旦启动线程,用AttachCurrentThread方法获得env。

通过JavaVM*和JNIEnv可以查找到jclass。

把jclass转成全局引用,使其跨线程。

然后就可以正常地调用你想调用的方法了。

用完后,别忘了delete掉创建的全局引用和调用DetachCurrentThread方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: