JNI设计机制

Posted by Haiden on April 5, 2019

一、设计机制

1.1 JNI接口函数和指针

本地代码通过调用JNI函数来访问Java 虚拟机的特性。JNI函数可通过接口指针获得。接口指针是指向指针的指针。该指针指向一个指针数组,每个指针指向一个接口函数。

JNI接口的组织方式类似于C ++虚函数表或COM接口。使用接口表而不是硬连接函数条目的优点是JNI名称空间与本机代码分离。

JNI接口指针仅在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程。本地方法接收JNI接口指针作为参数。

1.2 编译,载入和链接本地方法

使用该System.loadLibrary方法加载本地方法。类初始化方法加载特定于平台的本地库,其中f定义了本地方法:

1
2
3
4
5
6
7
8
9
10
package pkg;

class Cls { 

     static { 
         System.loadLibrary(“pkg_Cls”); 
     } 
     
     native double fint iString s; 
} 

1.3 本地方法的命名规范

动态链接定位函数靠的是它们的函数名。 一个本地方法的函数名分为如下几个部分:

1)Java_ 的前缀

2)用 _ 为分隔符的类名全称,例如:com_package_SomeClass

3)一个 _ 分隔符

4)方法名

5)对于重载方法(overload),后面还有跟两个下划线,以及参数签名。

1.4 本地方法参数

所有本地方法的第一个参数都是JNI接口指针(JNI interface pointer),它的类型是 JNIEnv 。第二个参数根据本地方法是否为静态的而不一样。如果不是静态的本地方法,第二个参数则是一个对象的引用,如果是静态的本地方法,第二个参数则是一个Java类的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    static {
        System.loadLibrary("test1");
    }
	  //本地方法
    public native String stringTestFromJNI();
}

------------------------------------------
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring

JNICALL
Java_com_nothing_jnitest2_MainActivity_stringTestFromJNI(
        JNIEnv *env,     			 //接口指针
        jobject /* this */) {  //对象的引用
    std::string hello = "Hello from TEST";
    return env->NewStringUTF(hello.c_str());
}

1.5 引用传递Java对象

原始类型:(如整数,字符等)在Java和本地代码之间复制。

Java对象:都通过引用传递。虚拟机必须跟踪已传递给本地代码的所有对象,以便垃圾收集器不会释放这些对象。反过来,本地代码必须有一种方法来通知虚拟机它不再需要这些对象。此外,垃圾收集器必须能够移动本地代码引用的对象。

1.6 全局和局部引用

JNI将本地代码使用的对象引用分为两类:

1.局部引用:局部引用在本地代码调用的持续时间内有效,并在本地代码返回后自动释放。

2.全局引用:全局引用在显式释放之前仍然有效。

对象作为本地引用传递给本地方法。JNI函数返回的所有Java对象都是局部引用。

有时应该明确地释放局部引用。考虑以下情况:

  • 本地方法访问大型Java对象,从而创建对Java对象的局部引用。本地方法还需要在返回之前做一些其他计算,而这种因为有一个局部引用指向这个Java对象,因此垃圾回收器不能回收掉这个大的对象。

  • 一个本地方法创建了大量的局部引用,但不是同时都需要它们。因此虚拟机需要大量空间来持续跟踪这些局部引用,所以创建大量的局部引用可能会造成系统内部不足。

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

1.7 局部引用的实现

为了实现局部引用,Java 虚拟机为从Java到本地方法的每次控制转换创建了一个注册表。注册表将不可移动的本地引用映射到Java对象,并防止对象被垃圾回收。传递给本地方法的所有Java对象(包括那些作为JNI函数调用结果返回的对象)都会自动添加到注册表中。在本机方法返回后删除注册表,允许其所有条目被垃圾回收。

有不同的方法来实现注册表,例如使用表,链表或哈希表。

注意,局部引用不能通过保守扫描native stack来完全实现,因为本地代码可能将局部引用存储到全局或堆数据结构里面。

1.8 访问Java对象

JNI 提供丰富的访问函数来访问全局和局部引用。

通过不透明引用使用访问器函数的开销高于直接访问C数据结构的开销。

1.9 访问基本类型数组

对于包含许多基本数据类型的大型Java对象(例如整数数组和字符串),此开销是不可接受的。遍历一个Java数组并处理其中每个元素是非常低效的。

jni提供一组函数用来在Java数组和本地方法buffer之间复制基本类型数组的元素。如果本地方法只需要访问一个大数组中的一小部分元素则可以使用这些函数。

1.10 访问字段和方法

JNI允许本地方法来访问Java对象的字段和方法。JNI以符号名和类型签名来ID字段和方法。例如定位到一个Java方法:

1
2
3
class Cls {
  public double f(String input);
}

原生代码可先定位到该Java方法(根据其名字和签名):

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

原生代码即可使用method id来调用该方法:

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

1.11 报告编程错误

JNI不检查编程错误,例如传入NULL指针或非法参数类型。非法参数类型包括使用普通Java对象而不是Java类对象。

1.12 Java异常

JNI允许本地方法去抛出普通的Java异常。本地代码也可以处理Java方法抛出的异常。如果Java异常没有被捕捉则会重新传回给虚拟机。

1.13 异常捕捉

有两种方法在本地代码中捕捉异常:

  • 本地方法可以选择立即返回,促使异常抛会给Java方法,让其自己处理该异常。
  • 本地方法可以调用 ExceptionClear 来清除异常,然后执行自己的异常处理代码。

在一个异常被抛出时,本地代码首先必须在其他JNI调用前清除异常,当有一个待处理的异常时,JNI函数可以安全的调用如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  ExceptionOccurred()
  ExceptionDescribe()
  ExceptionClear()
  ExceptionCheck()
  ReleaseStringChars()
  ReleaseStringUTFChars()
  ReleaseStringCritical()
  Release<Type>ArrayElements()
  ReleasePrimitiveArrayCritical()
  DeleteLocalRef()
  DeleteGlobalRef()
  DeleteWeakGlobalRef()
  MonitorExit()
  PushLocalFrame()
  PopLocalFrame()