参考:
http://wiki.jikexueyuan.com/project/deep-android-v1/jni.html
JNIEnv是一个和线程相关的,代表JNI环境的结构体,图2-3展示了JNIEnv的内部结构:
从上图可知,JNIEnv实际上就是提供了一些JNI系统函数。通过这些函数可以做到:
- 调用Java的函数。
- 操作jobject对象等很多事情。
通过JNIEnv 操作jobject
除了Java中基本数据类型的数组、Class、String和Throwable外,其余所有Java对象的数据类型在JNI中都用jobject表示。
如果对象类型都用jobject表示,就好比是Native层的void*类型一样,对码农来说,是完全透明的。既然是透明的,那该如何使用和操作它们呢?
前面提到过一个问题,即Java的引用类型除了少数几个外,最终在JNI层都用jobject来表示对象的数据类型,那么该如何操作这个jobject呢?
从另外一个角度来解释这个问题。一个Java对象是由什么组成的?当然是它的成员变量和成员函数了。那么,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。所以应先来看与成员变量及成员函数有关的内容。
(1)jfieldID 和jmethodID的介绍
我们知道,成员变量和成员函数是由类定义的,它是类的属性,所以在JNI规则中,用jfieldID 和jmethodID 来表示Java类的成员变量和成员函数,它们通过JNIEnv的下面两个函数可以得到:
jfieldID GetFieldID(jclass clazz,const char*name, const char *sig);
jmethodID GetMethodID(jclass clazz, const char*name,const char *sig);
(2)使用jfieldID和jmethodID
jstring介绍
Java中的String也是引用类型,不过由于它的使用非常频繁,所以在JNI规范中单独创建了一个jstring类型来表示Java中的String类型。虽然jstring是一种独立的数据类型,但是它并没有提供成员函数供操作。相比而言,C++中的string类就有自己的成员函数了。那么该怎么操作jstring呢?还是得依靠JNIEnv提供的帮助。这里看几个有关jstring的函数:
- 调用JNIEnv的NewString(JNIEnv *env, const jchar*unicodeChars,jsize len)
,可以从Native的字符串得到一个jstring对象。其实,可以把一个jstring对象看成是Java中String对象在JNI层的代表,也就是说,jstring就是一个Java String。但由于Java String存储的是Unicode字符串,所以NewString函数的参数也必须是Unicode字符串。
- 调用JNIEnv的NewStringUTF()
将根据Native的一个UTF-8字符串得到一个jstring对象。在实际工作中,这个函数用得最多。
- 上面两个函数将本地字符串转换成了Java的String对象,JNIEnv还提供了GetStringChars()
和GetStringUTFChars()
函数,它们可以将Java String对象转换成本地字符串。其中GetStringChars()
得到一个Unicode字符串,而GetStringUTFChars()
得到一个UTF-8字符串。
- 另外,如果在代码中调用了上面几个函数,在做完相关工作后,就都需要调用ReleaseStringChars()
或ReleaseStringUTFChars()
函数对应地释放资源,否则会导致JVM内存泄露。这一点和jstring的内部实现有关系,读者写代码时务必注意这个问题。
附录:
注册JNI函数
静态注册
就是用javah
生成.h头文件,然后里面有这样的函数
#include <jni.h> //必须包含这个头文件,否则编译通不过
//processFile的JNI函数
// JNICALL 在 Windows 中的值为__stdcall,用于约束函数入栈顺序和堆栈清理的规则。
JNIEXPORT void JNICALL Java_android_media_MediaScanner_processFile (JNIEnv *, jobject, jstring,jstring, jobject);
其中
Windows 下jni_md.h
头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif
可见JNICALL
在 Windows 中的值为__stdcall
,用于约束函数入栈顺序和堆栈清理的规则。
Linux 下jni_md.h头文件内容:
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
#define JNIEXPORT
#define JNIIMPORT
#define JNICALL
typedef int jint;
#ifdef _LP64 /* 64-bit Solaris */
typedef long jlong;
#else
typedef long long jlong;
#endif
typedef signed char jbyte;
#endif
可见
从 Linux 下的jni_md.h头文件可以看出来,JNIEXPORT 和 JNICALL 是一个空定义,所以在 Linux 下 JNI 函数声明可以省略这两个宏。
参考:http://wiki.jikexueyuan.com/project/jni-ndk-developer-guide/serchnative.html
动态注册
既然Java native函数数和JNI函数是一一对应的,那么是不是会有一个结构来保存这种关联关系呢?答案是肯定的。在JNI技术中,用来记录这种一一对应关系的,是一个叫JNINativeMethod
的结构,其定义如下:
typedef struct {
//Java中native函数的名字,不用携带包的路径。例如“native_init“。
const char* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型的组合。
const char* signature;
void* fnPtr; //JNI层对应函数的函数指针,注意它是void*类型。
} JNINativeMethod;
当Java层通过System.loadLibrary()
加载完JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad()
的函数,如果有,就调用它,而动态注册的工作就是在这里完成的。
所以,如果想使用动态注册方法,就必须要实现JNI_OnLoad()
函数,只有在这个函数中,才有机会完成动态注册的工作。静态注册则没有这个要求,可我建议读者也实现这个JNI_OnLoad()
函数,因为有一些初始化工作是可以在这里做的。