Android进阶之路——NDK(二)
2015-12-03 18:03
666 查看
上一篇博客介绍了NDK简介和环境的搭建以及一个简单的Demo,这篇准备总结一下JNI调用Java对象以及在JNI中开启线程。
ps:这里说明一下,我是用Android Studio开发的,如果是用Eclipse开发的朋友,是不能直接导入我的程序,而且项目的结构和我的是有区别的。
我的项目结构:
首先在java层注册native函数
如上面代码所示,Java层与JNI层的接口代码主要封装在Native类中,该类定义了五个native函数,分别是从jni层获取字符串,完成jni库的初始化,调用jni层开启线程,调用jni层关闭线程等功能。并且提供一个回调函数(一个为在开启的线程中回调,另一个是在jni开启的线程中回调),供jni层调用,并在回调函数中打印线程的Id和传递过来的字符串。这里先不讲解如何在JNI中创建线程。
2. jni中回调java层的函数
这里创建头文件的方法和Android Studio下配置NDK的环境已经在前一篇叙述过,这里就不说了。在头文件中定义了这五个函数,MyJni.c是实现五个native函数的主要类:
这里只贴出了在主线程回调java函数的代码,这里代码很简单,就是调用了onNativeCallback,然而这个函数实在哪里实现的呢?大家可以再看一下我的项目截图,就是在CallJava.c中实现的。代码中重要的部分我都有注释。
大家可以看到,在JNI中回调Java层的函数需要四步:1、获得一个Java类的class引用(*env)->FindClass(env, “com/ndk/MyJni”),第二个参数代表这个类的相对路径。2、实例化该类,(*env)->GetMethodID(env, clazz, “”, “()V”),第二个是刚刚获得的class引用,第三个是方法的名称(这里是构造函数),最后一个就是方法的签名了(下一章节会详细介绍)。3、获取到调用该对象的方法。4、回调该方法。
这样一个主线程的回调就完成了,这里大家在使用(*env)->GetMethodID时,注意第三个和第四个参数一定要和你java中函数名称和参数类型及个数相对应,不然会报错。
在JNI中实现开启线程的代码,也就是MyJni.c,上一章节中只是贴出了主线程回调的一个函数,这里将剩下的四个本地方法都贴出来。
第一个本地方法:返回一个字符串。
第二个本地方法:是初始化一些全局变量,在开启线程时使用。
第三个本地方法:是开启线程。
第四个本地方法:是关闭线程
大部分代码是有注释的,大家应该是可以看懂的。
第一个变量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[][][],即几维数组就加几个[
通过JavaVM*和JNIEnv可以查找到jclass。
把jclass转成全局引用,使其跨线程。
然后就可以正常地调用你想调用的方法了。
用完后,别忘了delete掉创建的全局引用和调用DetachCurrentThread方法。
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方法。
相关文章推荐
- Android 检测 手机硬件状态 的Utils
- android-Menu Resource
- Android动态换肤开源库Colorful发布
- 如何优化 Android Studio 启动、编译和运行速度?
- 应用中添加QQ群(android、iOS、网页、二维码)
- Android设置Settings:PreferenceFragment【4】
- 【Android效果集】学习ExplosionField之粒子破碎效果
- Android设置Settings:ListPreference【3】
- Android动态加载黑科技 动态创建Activity模式
- Android设置Settings:预读取设置的选项和更新设置结果【2】
- Android 4.4Telephony流程分析SIM卡开机时的初始化
- Android Studio系列教程六--Gradle多渠道打包
- Android Crash日志抓取及保存
- android一款软件的反编译实现(APKtool)
- Android 4.4Telephony流程分析SIM卡开机时的数据加载
- Android Studio系列教程五--Gradle命令详解与导入第三方包
- Android Studio系列教程四--Gradle基础
- 对Android源码中常见的一些flag的运算的理解
- Android Studio系列教程三--快捷键
- Android设置Settings实现:PreferenceActivity【1】