"JNI"解决了一个问题,类似于(similar to)通过被其他语言支持的互操作机制来解决的问题。然而,在"JNI"和在许多其他语言中的互操作机制之间有一明显的不同。"JNI"不是为一个特别的"Java"虚拟器的实现设计的。而是,一个本地接口能过被每个"Java"虚拟器的实现支持的。
1.JNI设计目的:
- "JNI"设计的最重要目标是确保它在一个给定的主机环境上不同的"Java"虚拟器实现中提供二进 制兼容性。同样的本地库的二进制不需要在编译(without the need for recompileation)将运行在一 个给定的主机环境上的不同的虚拟器实现上;
- "JNI"设计的第二个目的是效率(efficiency);
-
"JNI"必须功能完整(functionally complete)。必须导出足够"Java"虚拟器功能(functionality)来使本地方法和应用程序能完成有用的任务。
2.载入本地库:
当一个应用能调用一个本地方法前,虚拟器必须定位和载入一个本地库,它包含本地方法的实现。
- 类的载入:
类载入器提供名字空间的隔离,被需要在一样虚拟器的一个实例中运行多个控件(例如载入来自不同 Web 站点的"applets")。通过映射类或接口名字到在"Java"虚拟器中被表示为对象的实际类或接口类型的过程,一个类载入器保持一个隔离名字空间。每个类或接口类型和它定义的载入 器相关,载入器初始时读类文件和定义类或接口对象。只有当他们有一样的名字和一样的定义 载入器时,两个类或接口类型是一样的。
- 类的载入器和本地库:
为确保每个 C 类链接到正确的本地函数上,每个类载入器必须它自己的一套本地库;
因为每个类载入器维持一套本地库,所以编程者可以使用功能一个单一库来保存所有的被任何类请求的本地方法,只要这些类有一样定义的载入器;
当它们对应的类载入器被垃圾收集时,本地库将自动地被虚拟器载出。
- 定位本地库:
通过"System.loadLibrary"方法来载入本地库;
当"Java"虚拟器启动,它构建一个目录列表,被使用来为应用程序类定位的本地库。这列表的内 容依赖于主机环境和虚拟器实现;
如果它载入命名的本地库失败,"System.loadLibrary"抛出一 个"UnsatisfiedLinkError";
- 类型安全限制:
虚拟器不允许一个给定的本地库被不止一个类载入器载入。通过多个类载入器,尝试载入一样的本地库导致一个"UnsatisfiedLink Error"异常被抛出。这个限制的目的是确保,基于类载入器的名字空间隔离被保存在本地库中。没有这个限制,通过本地方法,比较容易错误地混用来自不 同的类载入器的类和接口。
- 载出本地库:
在虚拟器垃圾收集和本地库相关的类载入器,虚拟器载出一个本地库。因为类使用它们定义的载入器,这暗示虚拟器也已经载出了类,这个类有静态初始化调用"System.loadLibrary"和载入本地库。
- 链接本地方法步骤:
确定定义本地方法的类的类载入器;
搜索和这个类载入器相关的本地库集来定位实现本地方法的本地函数;
建立内部数据结构,使本地方法的所有将来调用将直接地跳到本地函数。
- 调用协议:
调用协议决定一个本地函数怎样接受参数和返回结果。在不同本地语言中,或相同语言的不同实现中,没有标准调用协议。
3."JNIEnv"接口指针:
通过"JNIEnv"接口到处的不同函数的调用,本地代码来访问虚拟器功能。
- "JNIEnv"接口指针的组织:
一个"JNIEnv"接口指针是一个线程局部数据的指针,它又包含一个函数表的值指针。每个接口函数是在表中一个预定义偏移上。"JNIEnv"接口的组织像一个 "C++ "虚拟函数表,同时也像一个"Microsoft COM"接口。
-
接口指针的好处:
"JNI"函数表是作为一个参数传递给每个本地方法,本地库不必和一个"Java"虚拟器的特殊实现链接;
通过不适用硬函数实体,虚拟器实现可以选择提供多个版本的"JNI"函数表;
多个"JNI"函数表可以支持将来的类似"JNIEnv"接口的多个版本。
4.传递数据:
-
基本数据类型:
被在"Java"虚拟器和本地 代码之间复制;
-
传递引用:
对象通过引用传递。每个引用包含一个直接的指向底层对象的指 针。对象的指针不能被本地代码直接使用。来自本地代码视角,引用是透明的;
传递引用,替代直接的对象直接指针,使虚拟器能用更灵活的方法来管理对象。当本地代码持有一个引用时,虚拟器可以执行一个垃圾收 集,导致一个对象被从记忆体的一块地方复制到另一块地发。虚拟器能自动地更新引用的内容, 使即使对象已经移动了,引用任然是有效的。
- 全局引用和局部引用:
局部引用对于一个本地方法调用的期间是有效的,同时在本地方法返回后自动释放;
全局引用保持有效,直到他们被明显地释放;
- 实现局部引用:
为实现局部引用,为每个从虚拟器到一个本地方法的过渡控制,"Java"虚拟器创建一个登记;
一个登记映射不能移动局部引用的对象指针。在登记中对象不能被垃圾收集。被传递给本地方法的所有对象,包括作为"JNI"函数调用的结果被返回的对象,被自动地加入登记。 在方法返回后登记被删除,允许它的实体被垃圾收集;
有不同的方法来实现一个登记,例如使用一个栈,一个表,一个链接列,或一个哈希(hash)表。
- 弱全局引用:
不像一般全局引用,一个弱全 局引用允许一个引用对象被垃圾收集。在底层对象的垃圾收集后,一个弱引用被清除。本地代 码能够通过使用"IsSameObject"来比较引用和 NULL 来检测一个弱引用是不是被清除。
5.访问对象:
"JNI"提供为引用到对象提供了一套丰富的访问函数。这意味着无论虚拟器内部怎样表示对象,都有一样本地函数方法实现。
- 访问基本类型数据:
-
"JNI"提供一套函数(例如,GetIntArrayRegion and SetIntArrayRegion)来在一段基本类型数组和一个本地内存缓存之间复制基本类型数组元素。如果本地方法需要访问在一个大的数组中 的一部分元素,或者如果本地方法需要复制一份这个数组,使用这些函数;
-
使用另一套函数(例如,"GetIntArrayElements")来尝试得到一个"pinned"版本的数组元素。然而,依赖于虚拟器的实现,这些函数可以引起存储的分配和复制;
-
两个新函数:"GetPrimitiveArrayCritical and ReleasePrimitiveArrayCritical";
-
成员域和方法:
"JNI"允许本地代码访问在"Java"编程语言中定义的成员域和方法。"JNI"通过他们的符号名字和 类型描述符来标记方法和成员域。一个两步处理分解出从它的名字和描述符定位成员域或方法的成本。
6.错误和异常:
在"JNI"程序中出现的错误不同于在"Java"虚拟器实现中发生的异常。编程者错误是"JNI"函数的错误使用引起的。
- 对编程错误的不检查:
"JNI"函数不检查编程的错误。
- Java虚拟器异常:
"JNI"在本地编程语言中不依赖域异常处理机制。通过调用"Throw"或"ThrowNew",本地代码可以引起"Java"虚拟器抛出一个异常。在当前线程中记录一个 悬而未决的异常。不像在"Java"编程语言中抛出的异常,在本地代码中抛出的异常不会立即使当前执行中断。
在有一个悬而未决(pending)异常时,能过安全地调用的"JNI"函数的完成列表:
ExceptionOccurred
ExceptionDescribe
ExceptionClear
ExceptionCheck
ReleaseStringChars
ReleaseStringUTFchars
ReleaseStringCritical
Release<Type>ArrayElements
ReleasePrimitive ArrayCritical
DeleteLocalRef
DeleteGlobalRef
DeleteWeakGlobalRef
MonitorExit
- 异步异常:
在另一个线程中通过调用"Thread.stop",一个线程可能产生一个异步异常。一个异步异常不会影 响在当前线程中的本地代码的执行,直到: 本地代码调用一个"JNI"函数,它能产生同步异常;本地代码使用"ExceptionOccurred"来明确地检查同步或异步异常。
7.JNI类型:
在引用这些类型前,"C and C++"代码应该包含头文件"jni.h"。
- 基本的引用类型:
- 引用类型(C语言):
- 引用类型(C++):
-
class _jobject{} ; class _jclass: public _jobject{} ; class _jthrowable:public _jobject{} ; class _jstring:public _jobject{} ; class _jarray:public _jobject{} ; class _jbooleanArray:public _jarray{} ; class _jbyteArray:public _jarray{} ; class _jcharArray:public _jarray{} ; class _jshortArray:public _jarray{} ; class _jintArray:public _jarray{} ; class _jlongArray:public _jarray{} ; class _jfloatArray:public _jarray{} ; class _jdoubleArray:public _jarray{} ; class _jobjectArray:public _jarray{} ; typedef _jobject *jobject ; typedef _jclass *jclass ; typedef _jthrowable *jthrowable ; typedef _jstring *jstring ; typedef _jarray *jarray ; typedef _jbooleanArray *jbooleanArray ; typedef _jbyteArray *jbyteArray ; typedef _jcharArray *jcharArray ; typedef _jshortArray *jshortArray ; typedef _jintArray *jintArray ; typedef _jlongArray *jlongArray ; typedef _jfloatArray *jfloatArray ; typedef _jdoubleArray *jdoubleArray ; typedef _jobjectArray *jobjectArray ;
- "jvalue"类型:"jvalue"类型是一个引用类型和基本类型的联合体。
-
typedef union jvalue{ jboolean z ; jbyte b ; jchar c ; jshort s ; jint i ; jlong j ; jfloat f ; jdouble d ; jobject l ; } jvalue ;
-
成员域和方法 IDs:
-
//方法和成员域 IDs 是规则的"C"指针类型: struct _jfieldID ; typedef struct _jfieldID *jfieldID ; struct _jmethodID ; typedef struct _jmethodID *jmethodID ;
8.字符串格式:
"JNI"使用"C"字符串来表示类名字,成员域和方法名字,和成员域和方法的描述。这些字符串 是"UTF-8"格式。
-
UTF-8 字符串:
"UTF-8"字符串被编码,使包含非空"ASCII"码的字符串序列能够被每个字符只适用一个"byte"来表示,但是要表示高达"16 bits"的字符串。在"\u0001"到"\u007f"之间的所有字符都是用单一"byte"来表示的。
9.类的描述:
一个类的描述表示一个类或一个接口的名字。
- 类的描述:"java.lang.String"的类的描述符是:"java/ lang/String ";
-
数组类是使用后跟元素类型的成员域描述符的"["字符表示的。"int[]"的类描述符是: "[I","double[][][]"的类描述符是:"[[[D";
-
成员域的描述:
引用类型的成员域描述符从"L"字符开始的,后面跟着类描述符,和以";"字符结束:
- 常量:
"JNIEXPORT"和 "JNICALL"是宏,被用来指明 "JNI"函数和本地方法实现的调用和链接约定。在函数返回类型前面,必须放置"JNIEXPORT"宏,同时在函数名字和返回类型之间放置"JNICALL"。
-
//"C"函数的原型,实现了"pkg.Cls.f" JNIEXPORT jint JNICALL Java_pkg_Cls_f(JNIEnc *env, jobject this) ; //函数指针变量,它能被赋予"Java_pkg_Cls_f"函数 jint (JNICALL *f_ptr)(JNIEnv *env, jobject this) ; //"JNI_FALS E"和 "JNI_TRUE"是个常数,为"jboo le an"类型定义的 #define JNI_FALSE 0 #define JNI_TRUE 1 //"JNI_OK"表示"JNI"函数的成功返回值,同时"JNI_ERR"有时被用来表示错误的情况 #define JNI_OK 0 #define JNI_ERR (-1)