NDK-JNI实战教程(二) JNI官方中文资料,ndk-jnijni
声明
该篇文章完全引用自《JNI完全手册》完整版,用来方便查询查阅,同时作为该系列教程的基础知识。感谢原文档作者。
文档所依赖的版本是比较低的,但是恰恰是低版本才能更容易上手学习。文档也有些枯燥,适合开发中参考查询和粗略概况性
的浏览掌握大局使用,也是下来几篇的基础性指导文档。下来几篇不会再解释代码简单函数释义,只会说重点,遇到不懂的来
这篇文章搜索函数名即可查阅函数详情。
设计概述
JNI接口函数和指针
平台相关代码是通过调用JNI函数来访问Java虚拟机功能的。JNI函数可通过接口指针来获得。接口指针是指针的指针,它指向
一个指针数组,而指针数组中的每个元素又指向一个接口函数。每个接口函数都处在数组的某个预定偏移量中。下图说明了接
口指针的组织结构。
JNI接口的组织类似于C++虚拟函数表或COM接口。使用接口表而不使用硬性编入的函数表的好处是使JNI名字空间与平台相关代码分开。虚拟机可以很容易地提供多个版本的JNI函数表。例如,虚拟机可支持以下两个JNI函数表:
- 一个表对非法参数进行全面检查,适用于调试程序。
- 另一个表只进行JNI规范所要求的最小程度的检查,因此效率较高。
JNI接口指针只在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程中。实现JNI的虚拟机可将本地线程的数据分配和储存在JNI接口指针所指向的区域中。本地方法将JNI接口指针当作参数来接受。虚拟机在从相同的Java线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。但是,一个本地方法可被不同的Java线程所调用,因此可以接受不同的JNI接口指针。
加载和链接本地方法
对本地方法的加载通过System.loadLibrary方法实现。下例中,类初始化方法加载了一个与平台有关的本地库,在该本地库中
给出了本地方法f的定义:
package pkg; class Cls { native double f(int i, String s); static { System.loadLibrary("pkg_Cls"); } }
System.loadLibrary的参数是程序员任意选取的库名。系统按照标准的但与平台有关的处理方法将该库名转换为本地库名。例如,Solaris系统将名称pkg_Cls转换为libpkg_Cls.so,而Win32系统将相同的名称pkg_Cls转换为pkg_Cls.dll。程序员可用单个库来存放任意数量的类所需的所有本地方法,只要这些类将被相同的类加载器所加载。虚拟机在其内部为每个类加载器保护其所加载的本地库清单。提供者应该尽量选择能够避免名称冲突的本地库名。如果底层操作系统不支持动态链接,则必须事先将所有的本地方法链接到虚拟机上。这种情况下,虚拟机实际上不需要加载库即可完成System.loadLibrary调用。程序员还可调用JNI函数RegisterNatives()来注册与类关联的本地方法。在与静态链接的函数一起使用时,RegisterNatives()函数将特别有用。
解析本地方法名
动态链接程序是根据项的名称来解析各项的。本地方法名由以下几部分串接而成:
虚拟机将为本地库中的方法查找匹配的方法名。它首先查找短名(没有参数签名的名称),然后再查找带参数签名的长名称。只有当某个本地方法被另一个本地方法重载时程序员才有必要使用长名。但如果本地方法的名称与非本地方法的名称相同,则不会有问题。因为非本地方法(Java方法)并不放在本地库中。下例中,不必用长名来链接本地方法g,因为另一个方法g不是本地方法,因而它并不在本地库中。
class Cls1 { int g(int i); native int g(double d); }
我们采取简单的名字搅乱方案,以保证所有的Unicode字符都能被转换为有效的C函数名。我们用下划线(“_”)字符来代替全限定的类名中的斜杠(“/”)。由于名称或类型描述符从来不会以数字打头,我们用 _0、…、_9 来代替转义字符序列。
本地方法和接口API都要遵守给定平台上的库调用标准约定。例如,UNIX系统使用C调用约定,而Win32系统使用 __stdcall。
本地方法的参数
JNI接口指针是本地方法的第一个参数。其类型是JNIEnv。第二个参数随本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对对象的引用,而静态本地方法的第二个参数是对其Java类的引用。其余的参数对应于通常Java方法的参数。本地方法调用利用返回值将结果传回调用程序中。下一章 “JNI的类型和数据结构” 将描述Java类型和C类型之间的映射。
代码示例说明了如何用C函数来实现本地方法f。对本地方法f的声明如下:
package pkg; class Cls { native double f(int i, String s); ... }
具有长mangled名称Java_pkg_Cls_f_ILjava_lang_String_2的C函数实现本地方法f:
jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* 接口指针 */ jobject obj, /* “this”指针 */ jint i, /* 第一个参数 */ jstring s) /* 第二个参数 */ { /* 取得Java字符串的C版本 */ const char *str = (*env)->GetStringUTFChars(env, s, 0); /* 处理该字符串 */ ... /* 至此完成对 str 的处理 */ (*env)->ReleaseStringUTFChars(env, s, str); return ... }
注意,我们总是用接口指针env来操作Java对象。可用C++将此代码写得稍微简洁一些,如代码示例所示:
extern "C" /* 指定 C 调用约定 */ jdouble Java_pkg_Cls_f__ILjava_lang_String_2 ( JNIEnv *env, /* 接口指针 */ jobject obj, /* “this”指针 */ jint i, /* 第一个参数 */ jstring s) /* 第二个参数 */ { const char *str = env->GetStringUTFChars(s, 0); ... env->ReleaseStringUTFChars(s, str); return ... }
使用C++后,源代码变得更为直接,且接口指针参数消失。但是,C++的内在机制与C的完全一样。在C++中,JNI函数被定义为内联成员函数,它们将扩展为相应的C对应函数。
引用Java对象
基本类型(如整型、字符型等)在Java和平台相关代码之间直接进行复制。而Java对象由引用来传递。虚拟机必须跟踪传到平台相关代码中的对象,以使这些对象不会被垃圾收集器释放。反之,平台相关代码必须能用某种方式通知虚拟机它不再需要那些对象,同时,垃圾收集器必须能够移走被平台相关代码引用过的对象。
全局和局部引用
JNI将平台相关代码使用的对象引用分成两类:局部引用和全局引用。局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。全局引用将一直有效,直到被显式释放。对象是被作为局部引用传递给本地方法的,由JNI函数返回的所有Java对象也都是局部引用。JNI允许程序员从局部引用创建全局引用。要求Java对象的JNI函数既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作为结果返回。
大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。但是,有时程序员必须显式释放某个局部引用。例如,考虑以下的情形:
JNI允许程序员在本地方法内的任何地方对局部引用进行手工删除。为确保程序员可以手工释放局部引用,JNI函数将不能创建额外的局部引用,除非是这些JNI函数要作为结果返回的引用。局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程传递到另一个线程中。
实现局部引用
为了实现局部引用,Java虚拟机为每个从Java到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用映射为Java对象,并防止这些对象被当作垃圾收集。所有传给本地方法的Java对象(包括那些作为JNI函数调用结果返回的对象)将被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或hash表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但JNI实现不是必须检测和消除重复的项。注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在全局或堆数据结构中。
访问Java对象
JNI提供了一大批用来访问全局引用和局部引用的函数。这意味着无论虚拟机在内部如何表示Java对象,相同的本地方法实现都能工作。这就是为什么JNI可被各种各样的虚拟机实现所支持的关键原因。通过不透明的引用来使用访问函数的开销比直接访问C数据结构的开销来得高。我们