黑马程序员技术交流社区

标题: java-基础-JNI本地栈 [打印本页]

作者: 小江哥    时间: 2018-5-13 11:01
标题: java-基础-JNI本地栈
本帖最后由 小江哥 于 2018-5-13 11:06 编辑

前几日有个同学问答JNI本地栈,这几天就整理了一些资料,咋这里分享给大家
jni全称是Java Native Interface是在JAVA和Native层(包括但不限于C/C++)相互调用的接口规范。

JNI在JAVA1.1中正式推出,在JAVA1.2版本中加入了JNI_OnLoad,JNI_OnUnload方法,这两个方法还是很有用的,后面再说。

JNI基础篇

Java通过JNI调用本地方法的过程大致是

其中第三步在Java1.2中增加了JNI_OnLoad方法之后有另一种实现方式(后面说)。

javah生成的头文件大致是这样的:


[Java] 纯文本查看 复制代码
/* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_young_soundtouch_SoundTouch */ #ifndef _Included_com_young_soundtouch_SoundTouch #define _Included_com_young_soundtouch_SoundTouch #ifdef __cplusplus extern "C" { #endif #undef com_young_soundtouch_SoundTouch_SETTING_USE_AA_FILTER #define com_young_soundtouch_SoundTouch_SETTING_USE_AA_FILTER 0L /* * Class: com_young_soundtouch_SoundTouch * Method: native_getDefaultSampleElementSize * Signature: ()I */ JNIEXPORT jint JNICALL Java_com_young_soundtouch_SoundTouch_native_1getDefaultSampleElementSize (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif

文件开头就是普通的头文件,但是可以发现:

下面在C/C++中实现这个方法就行啦。但是在动手前现大致了解以下jni.h制定的游戏规则。

[url=]类型转换[/url]

javah生成的头文件里面使用的类型都是jni.h定义的,目的是做到平台无关,比如保证在所有平台上jint都是32位的有符号整型。

基本对应关系如下:


[/table]

引用类型对应关系:


通过表格发现,除了上面定义的String,Class,Throwable,其他的类(除了数组)都是以jobject的形式出现的!事实上jstring, jclass也都是object的子类。所以这里还是和java层一样,一切皆jobject。(当然,如果jni在C语言中编译的话是没有继承的概念的,此时jstring,jclass等其实就是jobject!用了typedef转换而已!!)

接下来是JNIEnv *这个指针,他提供了JNI中的一系列操作的接口函数。

JNI中操作jobject

其实也就是在native层操作java层的实例。 要操作一个实例无疑是:

所以问题来了:(挖掘机技术哪家强?! o(*≧▽≦)ツ┏━┓ )

怎么得到field 和 method?

通过使用jfieldID和jmethodID: 在JNI中使用类似于放射的方式来进行field和method的操作。JNI中使用jfieldID和jmethodID来表示成员变量和成员方法,获取方式是:

[Java] 纯文本查看 复制代码
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig); jfieldID GetStaticFieldID(jclass clazz, const char *name, const char *sig); jmethodID GetMethodID(jclass clazz, const char *name, const char *sig); jmethodID GetStaticMethodID(jclass clazz, const char *name, const char *sig) ;

其中最后一个参数是签名。 获取jclass的方法除了实用上面静态方法的第二个参数外,还可以手动获取。 jclass FindClass(const char *name) 需要注意的是name参数,他是一个类包括包名的全称,但是需要把包名中的点.替换成斜杠/。(好吧,事实上我不是太明白为啥要这么做。)

有了jfieldID和jmethodID就知道狗蛋住哪了,现在去狗蛋家找他玩 ♪(^∇^*)

成员变量:

成员方法:

调用方法自然要把方法的参数传递进去,JNI中实现了三种参数的传递方式:

然后问题又来了: 如果传进来的参数和java声明的参数的不一致会怎么样!(即不符合方法签名)这里文档中没用明确解释,但是说道:

Exceptions raised during the execution of the Java method.

[Java] 纯文本查看 复制代码
typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue;

数组的操作

数组是一个很常用的数据类型,在但是在JNI中并不能直接操作jni数组(比如jshortArray,jfloatArray)。使用方法是:

有点要说明的:

关于引用与垃圾回收

比如上面有个方法传了一个jobject进来,然后我把她保存下来,方便以后使用。这样做是不行哒!因为他是一个LocalReference,所以不能保证jobject指向的真正的实例不被回收。也就是说有可能你用的时候那个指针已经是个野指针的。然后你的程序就直接Segment Fault了,呵呵。。。

在JNI中提供了三种类型的引用:

Glocal Reference:
1. 创建:jobject NewGlobalRef(jobject lobj);
2. 释放:void DeleteGlobalRef(jobject gref);

Local Reference:
LocalReference也有一个释放的函数:void DeleteLocalRef(jobject obj),他会立即释放Local Reference。 这个方法可能略显多余,其实也是有它的用处的。刚才说Local Reference会再函数返回后释放掉,但是假如函数返回前就有很多引用占了很多内存,最好函数内就尽早释放不必要的内存。

关于JNI_OnLoad

开头提到JNI_OnLoad是java1.2中新增加的方法,对应的还有一个JNI_OnUnload,分别是动态库被JVM加载、卸载的时候调用的函数。有点类似于WIndows里的DllMain。
前面提到的实现对应native的方法是实现javah生成的头文件中定义的方法,这样有几个弊端:

现在有了JNI_OnLoad,情况好多了。你不光能在其中完成动态注册native函数的工作还可以完成一些初始化工作。java对应的有了jint RegisterNatives(jclass clazz, const JNINativeMethod *methods,jint nMethods)函数。参数分别是:

JNINativeMethod:代码中的定义如下:

[Java] 纯文本查看 复制代码
/* * used in RegisterNatives to describe native method name, signature, * and function pointer. */ typedef struct { char *name; char *signature; void *fnPtr; } JNINativeMethod;

所以他有三个字段,分别是


于是现在你可以不用导出native函数了,而且可以随意给函数命名,唯一要保证的是参数及返回值的统一。然后需要一个const JNINativeMethod *methods数组来完成映射工作。

看起来大概是这样的:

[Java] 纯文本查看 复制代码
//只需导出JNI_OnLoad和JNI_OnUnload(这个函数不实现也行) /** * These are the exported function in this library. */ JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved); JNIEXPORT void JNICALL JNI_OnUnload(JavaVM *vm, void *reserved); //为了在动态库中不用导出函数,全部声明为static //native methods registered by JNI_OnLoad static jint native_newInstance (JNIEnv *env, jclass); //实现native方法 /* * Class: com_young_soundtouch_SoundTouch * Method: native_newInstance * Signature: ()I */ static jint native_newInstance (JNIEnv *env, jclass ) { int instanceID = ++sInstanceIdentifer; SoundTouchWrapper *instance = new SoundTouchWrapper(); if (instance != NULL) { sInstancePool[instanceID] = instance; ++sInstanceCount; } LOGDBG("create new SouncTouch instance:%d", instanceID); return instanceID; } //构造JNINativeMethod数组 static JNINativeMethod gsNativeMethods[] = { { "native_newInstance", "()I", reinterpret_cast<void *> (native_newInstance) } }; //计算数组大小 static const int gsMethodCount = sizeof(gsNativeMethods) / sizeof(JNINativeMethod); //JNI_OnLoad,注册native方法。 JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved) { JNIEnv* env; jclass clazz; LOGD("JNI_OnLoad called"); if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; } //FULL_CLASS_NAME是个宏定义,定义了对应java类的全名(要把包名中的点(.)_替换成斜杠(/)) clazz = env->FindClass(FULL_CLASS_NAME); LOGDBG("register method, method count:%d", gsMethodCount); //注册JNI函数 env->RegisterNatives(clazz, gsNativeMethods, gsMethodCount); //必须返回一个JNI_VERSION_1_1以上(不含)的版本号,否则直接加载失败 return JNI_VERSION_1_6; }

实战技巧篇

这里主要是巧用C中的宏来减少重复工作:

迅速生成全名
[Java] 纯文本查看 复制代码
//修改包名时只需要改以下的宏定义即可 #define FULL_CLASS_NAME "com/young/soundtouch/SoundTouch" #define func(name) Java_ ## com_young_soundtouch_SoundTouch_ ## name #define constance(cons) com_young_soundtouch_SoundTouch_ ## cons

比如func(native_1newInstance)展开成:Java_com_young_soundtouch_SoundTouch_native_1newInstance即JNI中需要导出的函数名(不过用动态注册方式没太大用了)

constance(AUDIO_FORMAT_PCM16)展开成com_young_soundtouch_SoundTouch_AUDIO_FORMAT_PCM16这个着实有用。

而且如果包名改了也可以很方便的适应之。

[Java] 纯文本查看 复制代码
//define __USE_ANDROID_LOG__ in makefile to enable android log #if defined(__ANDROID__) && defined(__USE_ANDROID_LOG__) #include <android/log.h> #define LOGV(...) __android_log_print((int)ANDROID_LOG_VERBOSE, "ST_jni", __VA_ARGS__) #define LOGD(msg) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d %s", __LINE__, msg) #define LOGDBG(fmt, ...) __android_log_print((int)ANDROID_LOG_DEBUG, "ST_jni_dbg", "line:%3d " fmt, __LINE__, __VA_ARGS__) #else #define LOGV(...) #define LOGD(fmt) #define LOGDBG(fmt, ...) #endif

通过这样的宏定义在打LOGD或者LOGDBG的时候还能自动加上行号!调试起来爽多了!

C++中清理内存的方式

由于C++里面需要手动清楚内存,因此我的解决方案是定义一个map,给每个实例一个id,用id把java中的对象和native中的对象绑定起来。在java层定义一个release方法,用来释放本地的对象。 本地的 KEY-对象 映射 static std::map<int, SoundTouchWrapper*> sInstancePool;

关于NDK

因为安卓的约定是把本地代码放到jni目录下面,但是假如有多个jni lib的时候会比较混乱,所以方案是每一个lib都在jni里面建一个子目录,然后jni里面的Android.mk就可以去构建子目录中的lib了。

jni/Android.mk如下(超级简单):

[Java] 纯文本查看 复制代码
LOCAL_PATH := $(call my-dir) include $(call all-subdir-makefiles)

然后在子目录soundtouch_module中的Android.mk就可以像一般的Android.mk一样书写规则了。

同时记录一下在Andoroid.mk中使用makefile内建函数wildcard的方法。 有时候源文件是一个目录下的所有.cpp/.c文件,这时候wildcard来统配会很方便。但是Android.mk与普通的Makefile的不同在于:

因为1,直接使用相对路径会导致wildcard匹配不到源文件。所以最好这么写FILE_LIST := $(wildcard $(LOCAL_PATH)/soundtouch_source/source/SoundTouch/*.cpp)。然而又因为2,这样还是不行的。所以还需要匹配之后把$(LOCAL_PATH)的部分去掉,因此还得这样$(FILE_LIST:$(LOCAL_PATH)/%=%).

还有个小tip:LOCAL_CFLAGS中最好加上这个定义-fvisibility=hidden这样就不会在动态库中导出不必要的函数了。

附录[url=]签名[/url]

JAVA中的函数签名包括了函数的参数类型,返回值类型。因此即使是重载了的函数,其函数签名也不一样。java编译器就会根据函数签名来判断你调用的到地址哪个方法。 签名中表示类型是这样的

1.基本类型都对应一个大写字母,如下:

[td][table=98%,rgb(245, 245, 245)]

2.如果是类则是: L + 类全名(报名中的点(.)用(/)代替)+ ; 比如java.lang.String 对应的是 Ljava/lang/String;

3.如果是数组,则在前面加[然后加类型签名,几位数组就加几个[ 比如int[]对应[I,boolean[][] 对应 [[Z,java.lang.Class[]对应[Ljava/lang/Class;

可以通过javap命令来获取签名(javah生成的头文件注释中也有签名):javap -x -p <类全名> 坑爹的是java中并不能通过反射来获取方法签名,需要自己写一个帮助类。


转载自:https://blog.csdn.net/jianghuxiaojin/article/details/52069420
作者: Yin灬Yan    时间: 2018-5-13 18:55
我来占层楼啊   




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2