Android Studio3.0开发JNI流程------JNI接口函数和指针

Android如何访问JNI接口

通常平台相关代码是通过调用JNI函数来访问Java虚拟机功能的。JNI函数可通过接口指针来获得。接口指针是指针的指针,它指向 一个指针数组,而指针数组中的每个元素又指向一个接口函数。每个接口函数都处在数组的某个预定偏移量中。


接口指针的组织结构图

这里写图片描述

JNI接口的组织类似于C++虚拟函数表或COM接口。使用接口表而不使用硬性编入的函数表的好处是使JNI名字空间与平台相关代码分开。虚拟机可以很容易地提供多个版本的JNI函数表。例如,虚拟机可支持以下两个JNI函数表:

  • 一个表对非法参数进行全面检查,适用于调试程序。
  • 另一个表只进行JNI规范所要求的最小程度的检查,因此效率较高。

JNI接口指针只在当前线程中有效。因此,本地方法不能将接口指针从一个线程传递到另一个线程中。实现JNI的虚拟机可将本地线程的数据分配和储存在JNI接口指针所指向的区域中。本地方法将JNI接口指针当作参数来接受。虚拟机在从相同的Java线程中对本地方法进行多次调用时,保证传递给该本地方法的接口指针是相同的。但是,一个本地方法可被不同的Java线程所调用,因此可以接受不同的JNI接口指针。

加载和链接本地方法

对于本地方法的加载是通过System.loadLibrary方法实现。

// Used to load the 'native-lib' library on application startup.
static {
   System.loadLibrary("native-lib");
}
    ......

public native String stringFromJNI();

解析本地方法名

动态链接程序是根据项的名称来解析各项的。本地方法名由以下几部分串接而成:

Java_fj_clover_ndktest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

解释C++中的: Java_fj_clover_ndktest_MainActivity_stringFromJNI

Java是表示这是Java类中的native方法
下划线(“_”)是分隔符
fj_clover_ndktest 是程序中的包名:fj.clover.ndktest
MainActivity表示这个方法所在的类是MainActivity类
stringFromJNI表示这个JNI的方法名是stringFromJNI

本地方法的参数

JNI接口指针是本地方法的第一个参数。其类型是JNIEnv第二个参数随本地方法是静态还是非静态而有所不同。非静态本地方法的第二个参数是对对象的引用而静态本地方法的第二个参数是对其Java类的引用其余的参数对应于通常Java方法的参数。本地方法调用利用返回值将结果传回调用程序中。“JNI的类型和数据结构” 将描述Java类型和C类型之间的映射。

在创建支持C/C++的Android程序中添加两个方法,分别为非static和static:

//在创建支持的C/C++程序中添加两个方法,分别是非静态和静态
public native String stringFromJNI1();
public static native String stringFromJNI2();

使用Alt+Enter回车在.cpp自动生成的C++代码中查看具体的不同之处

extern "C"
JNIEXPORT jstring JNICALL
Java_fj_clover_ndktest_MainActivity_stringFromJNI1(JNIEnv *env, jobject instance) {

    // TODO
    return env->NewStringUTF(returnValue);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_fj_clover_ndktest_MainActivity_stringFromJNI2(JNIEnv *env, jclass type) {

    // TODO
    return env->NewStringUTF(returnValue);
}

可以看出第一个参数为Jni接口指针,而对于第二个参数,由于方法是非静态和静态的区别,导致参数不一样。

非static的方法参数类型是jobject instance
static的方法参数类型是jclass type

创建几个带参的方法

//创建几个不同类型的方法
public native void hiJni();
public native String stringFromJNI1(String str,int i);
public native String stringFromJNI2(String str1,int i,boolean b);

C++代码中实现体:

extern "C"
JNIEXPORT void JNICALL
Java_fj_clover_ndktest_MainActivity_hiJni(JNIEnv *env, jobject instance) {
    // TODO
}

extern "C"
JNIEXPORT jstring JNICALL
Java_fj_clover_ndktest_MainActivity_stringFromJNI1(JNIEnv *env, jobject instance, jstring str_, jint i) {
    const char *str = env->GetStringUTFChars(str_, 0);
    // TODO
    env->ReleaseStringUTFChars(str_, str);
    return env->NewStringUTF(returnValue);
}

extern "C"
JNIEXPORT jstring JNICALL
Java_fj_clover_ndktest_MainActivity_stringFromJNI2(JNIEnv *env, jobject instance, jstring str1_,jint i, jboolean b) {
    const char *str1 = env->GetStringUTFChars(str1_, 0);
    // TODO
    env->ReleaseStringUTFChars(str1_, str1);
    return env->NewStringUTF(returnValue);
}

从这些方法中可以看出除了第一个和第二个参数以外,其余参数是与Java代码参数对应的。

解释其中一些内容

extern "C"  /* 指定 C 调用约定 */
JNIEXPORT jstring JNICALL
Java_fj_clover_ndktest_MainActivity_stringFromJNI1(
        JNIEnv *env,       // JNI接口指针
        jobject instance,  // 这是非静态方法,这里表示this
        jstring str_,      // Java中native方法的第一个参数
        jint i)            // Java中native方法的第二个参数
{
    ///取得接受java传过来的参数
    const char *str = env->GetStringUTFChars(str_, 0);

    // TODO   /*处理这些参数  就是功能*/

    //完成这些传递过来的参数
    env->ReleaseStringUTFChars(str_, str);

    //返回结果
    return env->NewStringUTF(returnValue);
}

注意:我们总是用接口指针env来操作Java对象。

在C++中,JNI函数被定义为内联成员函数,它们将扩展为相应的C对应函数。

引用Java对象

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

全局和局部引用

JNI将平台相关代码使用的对象引用分成两类:局部引用全局引用

  • 局部引用在本地方法调用期间有效,并在本地方法返回后被自动释放掉。
  • 全局引用将一直有效,直到被显式释放。

对象是被作为局部引用传递给本地方法的,由JNI函数返回的所有Java对象也都是局部引用。
JNI允许程序员从局部引用创建全局引用。要求Java对象的JNI函数既可接受全局引用也可接受局部引用。本地方法将局部引用或全局引用作为结果返回。
大多数情况下,程序员应该依靠虚拟机在本地方法返回后释放所有局部引用。但是,有时程序员必须显式释放某个局部引用。

例如,考虑以下的情形:

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

JNI允许程序员在本地方法内的任何地方对局部引用进行手工删除。为确保程序员可以手工释放局部引用,JNI函数将不能创建额外的局部引用,除非是这些JNI函数要作为结果返回的引用。局部引用仅在创建它们的线程中有效。本地方法不能将局部引用从一个线程传递到另一个线程中

实现局部引用

为了实现局部引用,Java虚拟机为每个从Java到本地方法的控制转换都创建了注册服务程序。注册服务程序将不可移动的局部引用映射为Java对象,并防止这些对象被当作垃圾收集。所有传给本地方法的Java对象(包括那些作为JNI函数调用结果返回的对象)将被自动添加到注册服务程序中。本地方法返回后,注册服务程序将被删除,其中的所有项都可以被当作垃圾来收集。可用各种不同的方法来实现注册服务程序,例如,使用表、链接列表或hash表来实现。虽然引用计数可用来避免注册服务程序中有重复的项,但JNI实现不是必须检测和消除重复的项。注意,以保守方式扫描本地堆栈并不能如实地实现局部引用。平台相关代码可将局部引用储存在全局或堆数据结构中。

访问Java对象

JNI提供了一大批用来访问全局引用和局部引用的函数。这意味着无论虚拟机在内部如何表示Java对象,相同的本地方法实现都能工作。这就是为什么JNI可被各种各样的虚拟机实现所支持的关键原因。通过不透明的引用来使用访问函数的开销比直接访问C数据结构的开销来得高。我们相信,大多数情况下,Java程序员使用本地方法是为了完成一些重要任务,此时这种接口的开销不是首要问题。

访问基本类型数组

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

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

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

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

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

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

  1. 如果垃圾收集器支持钉住,且数组的布局符合本地方法的要求,则不需要进行复制。
  2. 否则,该数组将被复制到不可移动的内存块中(例如,复制到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不检查诸如传递NULL指针非法参数类型之类的编程错误。非法的参数类型包括诸如要用Java类对象时却用了普通Java对象这样的错误。JNI不检查这些编程错误的理由如下:

  1. 强迫JNI函数去检查所有可能的错误情况将降低正常(正确)的本地方法的性能。
  2. 在许多情况下,没有足够的运行时的类型信息可供这种检查使用。

大多数C库函数对编程错误不进行防范。例如,printf()函数在接到一个无效地址时通常是引起运行错而不是返回错误代码。强迫C库函数检查所有可能的错误情况将有可能引起这种检查被重复进行–先是在用户代码中进行,然后又在库函数中再次进行。

程序员不得将非法指针或错误类型的参数传递给JNI函数。否则,可能产生意想不到的后果,包括可能使系统状态受损或使虚拟机崩溃。

Java异常

JNI允许本地方法抛出任何Java异常。本地方法也可以处理突出的Java异常。未被处理的Java异常将被传回虚拟机中。

异常和错误代码

一些JNI函数使用Java异常机制来报告错误情况。大多数情况下,JNI函数通过返回错误代码并抛出Java异常来报告错误情况。错误代码通常是特殊的返回值(如 NULL),这种特殊的返回值在正常返回值范围之外。因此,程序员可以:快速检查上一个JNI调用所返回的值以确定是否出错,并通过调用函数ExceptionOccurred()来获得异常对象,它含有对错误情况的更详细说明。

在以下两种情况中,程序员需要先查出异常,然后才能检查错误代码:

  1. 调用Java方法的JNI函数返回该Java方法的结果。程序员必须调用ExceptionOccurred() 以检查在执行Java方法期间可能发生的异常。
  2. 某些用于访问JNI数组的函数并不返回错误代码,但可能会抛出ArrayIndexOutOfBoundsException或ArrayStoreException。

在所有其它情况下,返回值如果不是错误代码值就可确保没有抛出异常。

异步异常

在多个线程的情况下,当前线程以外的其它线程可能会抛出异步异常。异步异常并不立即影响当前线程中平台相关代码的执行,直到出现下列情况:该平台相关代码调用某个有可能抛出同步异常的JNI函数,或者该平台相关代码用 ExceptionOccurred() 显式检查同步异常或异步异常。

注意,只有那些有可能抛出同步异常的JNI函数才检查异步异常。本地方法应在必要的地方(例如,在一个没有其它异常检查的紧密循环中)插入ExceptionOccurred() 检查以确保当前线程可在适当时间内对异步异常作出响应。

异常的处理

可用两种方法来处理平台相关代码中的异常:

本地方法可选择立即返回,使异常在启动该本地方法调用的Java代码中抛出。
平台相关代码可通过调用ExceptionClear() 来清除异常,然后执行自己的异常处理代码。

抛出了某个异常之后,平台相关代码必须先清除异常,然后才能进行其它的JNI调用。当有待定异常时,只有以下这些JNI函数可被安全地调用:ExceptionOccurred()、ExceptionDescribe()ExceptionClear()。ExceptionDescribe()函数将打印有关待定异常的调试消息。

本文内容主要参考http://blog.csdn.net/yanbober/article/details/45310365,感谢博主的辛劳……

你可能感兴趣的:(Android,Studio,Java,jni)