Android NDK开发(二)——JNI访问和引用JAVA对象

引用 Java 对象

基本类型(如整型、字符型等)在 Java 和平台相关代码之间直接进行复制。而 Java 对象由引用来传递。虚拟机必须跟踪传到平台相关代码中的对象,以使这些对象不会被垃圾收集器释放。反之,平台相关代码必须能用某种方式通知虚拟机它不再需要那些对象,同时,垃圾收集器必须能够移走被平台相关代码引用过的对象。

全局和局部引用

JNI 将平台相关代码使用的对象引用分成两类:局部引用和全局引用。局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。全局引用将一直有效,直到被显式释放。

对象是被作为局部引用传递给本地方法的,由 JNI 函数返回的所有 Java 对象也都是局部引用。JNI 允许程序员从局部引用创建全局引用。要求 Java 对象的 JNI 函数既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作为结果返回。

大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。但是,有时程序员必须显式释放某个局部引用。例如,考虑以下的情形:

  • 本地方法要访问一个大型 Java 对象,于是创建了对该 Java 对象的局部引用。然后,本地方法要在返回调用程序之前执行其它计算。对这个大型 Java 对象的局部引用将防止该对象被当作垃圾收集,即使在剩余的运算中并不再需要该对象。
  • 本地方法创建了大量的局部引用,但这些局部引用并不是要同时使用。由于虚拟机需要一定的空间来跟踪每个局部引用,创建太多的局部引用将可能使系统耗尽内存。例如,本地方法要在一个大型对象数组中循环,把取回的元素作为局部引用,并在每次迭代时对一个元素进行操作。每次迭代后,程序员不再需要对该数组元素的局部引用。

JNI 允许程序员在本地方法内的任何地方对局部引用进行手工删除。为确保程序员可以手工释放局部引用,JNI 函数将不能创建额外的局部引用,除非是这些 JNI 函数要作为结果返回的引用。

局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程传递到另一个线程中。

实现局部引用

为了实现局部引用,Java 虚拟机为每个从 Java 到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用映射为 Java 对象,并防止这些对象被当作垃圾收集。所有传给本地方法的 Java 对象(包括那些作为 JNI 函数调用结果返回的对象)将被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。

可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或 hash 表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但 JNI 实现不是必须检测和消除重复的项。

注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在全局或堆数据结构中。

访问 Java 对象

JNI 提供了一大批用来访问全局引用和局部引用的函数。这意味着无论虚拟机在内部如何表示 Java 对象,相同的本地方法实现都能工作。这就是为什么 JNI 可被各种各样的虚拟机实现所支持的关键原因。

通过不透明的引用来使用访问函数的开销比直接访问 C 数据结构的开销来得高。我们相信,大多数情况下,Java 程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。

访问基本类型数组

对于含有大量基本数据类型(如整数数组和字符串)的 Java 对象来说,这种开销将高得不可接受 (考虑一下用于执行矢量和矩阵运算的本地方法的情形便知)。对 Java 数组进行迭代并且要通过函数调用取回数组的每个元素,其效率是非常低的。

一个解决办法是引入“钉住”概念,以使本地方法能够要求虚拟机钉住数组内容。而后,该本地方法将接受指向数值元素的直接指针。但是,这种方法包含以下两个前提:

  • 垃圾收集器必须支持钉住。
  • 虚拟机必须在内存中连续存放基本类型数组。虽然大多数基本类型数组都是连续存放的,但布尔数组可以压缩或不压缩存储。因此,依赖于布尔数组确切存储方式的本地方法将是不可移植的。

我们将采取折衷方法来克服上述两个问题。

首先,我们提供了一套函数,用于在 Java 数组的一部分和本地内存缓冲之间复制基本类型数组元素。这些函数只有在本地方法只需访问大型数组中的一小部分元素时才使用。

其次,程序员可用另一套函数来取回数组元素的受约束版本。记住,这些函数可能要求 Java 虚拟机分配存储空间和进行复制。虚拟机实现将决定这些函数是否真正复制该数组,如下所示:

  • 如果垃圾收集器支持钉住,且数组的布局符合本地方法的要求,则不需要进行复制。
  • 否则,该数组将被复制到不可移动的内存块中(例如,复制到 C 堆中),并进行必要的格式转换,然后返回指向该副本的指针。

最后,接口提供了一些函数,用以通知虚拟机本地方法已不再需要访问这些数组元素。当调用这些函数时,系统或者释放数组,或者在原始数组与其不可移动副本之间进行协调并将副本释放。

这种处理方法具有灵活性。垃圾收集器的算法可对每个给定的数组分别作出复制或钉住的决定。例如,垃圾收集器可能复制小型对象而钉住大型对象。

JNI 实现必须确保多个线程中运行的本地方法可同时访问同一数组。例如,JNI 可以为每个被钉住的数组保留一个内部计数器,以便某个线程不会解开同时被另一个线程钉住的数组。注意,JNI 不必将基本类型数组锁住以专供某个本地方法访问。同时从不同的线程对 Java 数组进行更新将导致不确定的结果。

访问域和方法

JNI 允许本地方法访问 Java 对象的域或调用其方法。JNI 用符号名称和类型签名来识别方法和域。从名称和签名来定位域或对象的过程可分为两步。例如,为调用类 cls 中的 f 方法,平台相关代码首先要获得方法 ID,如下所示:

 jmethodID mid = 

     env->GetMethodID(cls, "f", "(ILjava/lang/String;)D");

然后,平台相关代码可重复使用该方法 ID 而无须再查找该方法,如下所示:

jdouble result = env->CallDoubleMethod(obj, mid, 10, str);

域 ID 或方法 ID 并不能防止虚拟机卸载生成该 ID 的类。该类被卸载之后,该方法 ID 或域 ID 亦变成无效。因此,如果平台相关代码要长时间使用某个方法 ID 或域 ID,则它必须确保:

  • 保留对所涉及类的活引用;
  • 或重新计算该方法 ID 或域 ID。

JNI 对域 ID 和方法 ID 的内部实现并不施加任何限制。

你可能感兴趣的:(JNI与NDK开发深入探究)