JNI Tips

JNI Tips

本文翻译自 Android/sdk/docs/training/articles/perf-jni.html,大体为意译,如有不足之处,请指正。

JNI是Java本地接口。 它定义了一种用Java编写的托管代码与用C/C++编写的本地代码交互的方式。
它与供应商无关,支持从动态库中加载代码,虽然有时非常麻烦,但效率相当高。
如果你还不熟悉JNI,请先阅读Java Native Interface Specification
(https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html)
以了解JNI的工作方式和可用功能。 首次阅读上述文章时,可能比较抽象、难以理解,所以接下来的几节内容可能对你很有用。

JavaVM and JNIEnv
JNI定义了两个关键数据结构"JavaVM"和"JNIEnv",本质上都是指向函数表指针的指针(二级指针)。 (在C++版本中,它们是具有指向函数表、以及贯穿该表的每个JNI函数的成员函数的指针的类)
JavaVM提供了"调用接口"函数,它允许您创建和销毁一个JavaVM。从理论上讲,每个进程可以有多个JavaVM,但Android只允许一个进程有一个。

JNIEnv提供了大部分JNI函数。你的本地方法都会接收一个JNIEnv作为第一个参数。

JNIEnv用于线程本地存储。出于这个原因,你不能在线程之间共享一个JNIEnv。如果一段代码没有其他方式来获得它的JNIEnv,那么你应该共享JavaVM,并使用GetEnv来获取该线程的JNIEnv。(假设它有一个;请参阅下面的AttachCurrentThread)

JNIEnv和JavaVM的C声明与C ++声明不同。 根据是否包含在C或C++中, "jni.h"包含文件提供了不同的typedef。出于该原因,
在两种语言包含的头文件中包含JNIEnv参数是一个坏主意。(换句话说:如果你的头文件需要#ifdef __cplusplus,那么如果
该头文件中的任何内容引用了JNIEnv,你可能需要做一些额外的工作。)

Threads
所有线程都是Linux线程,由内核调度。它们通常从托管代码中启动(使用Thread.start()),但它们也可以在别处创建,然后连接到JavaVM。例如,可以使用JNI AttachCurrentThread或AttachCurrentThreadAsDaemon函数来连接到用本地方法pthread_create()创建的线程。在线程被连接之前,它没有JNIEnv,并且不能进行JNI调用。

连接一个本地创建的线程会导致构建一个java.lang.Thread对象并将其添加到"主"ThreadGroup中,使其对调试器可见。在已连接的线程上调用AttachCurrentThread是无效操作。

Android不会暂停执行本地代码的线程。如果正在进行垃圾回收(GC),或者调试器发出暂停请求,则Android将在下一次JNI调用时暂停该线程。

通过JNI连接的线程在退出之前必须调用DetachCurrentThread。 如果直接对此编码比较困难,在Android 2.0(Eclair)及更高版本中,您可以使用pthread_key_create来定义一个析构函数,它将在线程退出之前调用,然后从那里调用DetachCurrentThread。 (通过pthread_setspecific方法,使用key值将JNIEnv存储在线程本地存储中;这样,它将作为参数传递到析构函数中。)


jclass,jmethodID和jfieldID
如果您想通过本地代码访问某个对象字段,可以执行以下操作:

    使用FindClass获取某个类的类对象引用
    使用GetFieldID获取某个字段的字段ID
    用适当的方式获取字段的值,例如GetIntField
    
同样,要调用一个方法,你首先需要获取一个类对象引用和一个方法ID。这些ID通常是指向内部运行时数据结构的指针。
查阅它们可能需要进行几次字符串对照,一旦实际调用它们来获取字段值或调用该方法将很快。

如果考虑性能,那么首次查看值后并将结果缓存在本地代码中会很有用。由于每个进程对该JavaVM的限制,因此将这些数据存储在静态结构中是合理的。

在类被卸载之前,类引用jclass,字段ID和方法ID都是有效的。如果与某个ClassLoader相关的所有类都能被垃圾回收,所有类才会被卸载,这在Android中很少见,但不是不可能的。
注意,无论怎样,jclass都是一个类引用,必须通过调用NewGlobalRef来保护(请参阅下一节)。

如果您想在加载类时缓存ID,并且在类被卸载并重新加载时自动重新缓存它们,那么正确的初始化ID的方法是将以下代码添加到对应的类中:

/*
     * We use a class initializer to allow the native code to cache some
     * field offsets. This native function looks up and caches interesting
     * class/field/method IDs. Throws on failure.
     */
    private static native void nativeInit();

    static {
        nativeInit();
    }

C/C++代码中实现一个nativeClassInit方法, 执行ID查找的功能。 在类初始化时执行一次该代码将执行一次。如果这个类被卸载然后重新加载,它将被再次执行。

Local and Global References

每个传递到本地方法的参数、绝大多数JNI方法返回的对象都是"局部引用",这意味着它仅在当前线程的当前本地方法的
执行期间有效。 即使在本地方法返回后对象本身仍然存在,它的引用也是无效的。

这适用于jobject的所有子类,包括jclass,jstring和jarray。(当启用扩展JNI检查时,运行时环境会警告大多数引用错误的用法。)

获得非局部引用的唯一方法是通过函数NewGlobalRef和NewWeakGlobalRef。

如果您想要一个长久有效的引用,则必须使用"全局"引用。 NewGlobalRef函数将局部引用作为参数并返回全局引用。
在调用DeleteGlobalRef之前,全局引用保证有效。

这个例子常用于缓存一个从FindClass返回的jclass:
   

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));   

所有的JNI方法都接受局部和全局引用作为参数。对同一对象的多个引用有可能具有不同的值。
例如,对同一对象,连续调用NewGlobalRef的返回值可能不同。要查看两个引用是否引用同一个对象,
必须使用IsSameObject函数。在本地代码中切勿使用==来比较两个引用。

这意味着在本地代码中,你不能假定对象引用是常量或唯一的。表示对象的32位的数值可能在两次方法调用中并不相同,
同时两个不同的对象在连续调用时可能具有相同的32位的数值。不要使用jobject的值作为键值。

程序员应该"不要过度创建"局部引用。实际上,这意味着如果你创建了大量的局部引用,也许在运行一组对象时,你应该使用DeleteLocalRef手动释放它们,而不是让JNI为你做。该实现仅需要为16个局部引用预留空间,因此如果您需要更多,则应该随时删除或使用EnsureLocalCapacity/PushLocalFrame来预留更多。

请注意,jfieldID s和jmethodID s是非透明类型,非对象引用,不应传递给NewGlobalRef。像GetStringUTFChars和
GetByteArrayElements这样的函数返回的原始数据指针也不是对象。 (它们可以在线程之间传递,在被释放之前都是有效的.)

单独提及一个特殊情况。如果使用AttachCurrentThread来连接本地线程,则运行的代码永远不会自动释放局部引用,直到线程断开连接。你创建的任意局部引用都需要手动删除。通常,循环创建局部引用的本地代码可能需要做手动删除工作。

UTF-8 and UTF-16 Strings
Java编程语言使用UTF-16。为了方便,JNI提供了与编辑后的UTF-8一起使用的方法。编辑后的编码对于C代码非常有用,
因为它将\u0000编码为0xc0 0x80,而不是0x00。这一点的好处是,您可以期望C风格的以零结尾的字符串,适用于标准的libc字符串函数。不利的一面是,你不能将任意的UTF-8数据传递给JNI,并期望它能正常工作。

如果有可能,使用UTF-16字符串操作通常会更快。 Android目前不需要GetStringChars中的副本,而GetStringUTFChars需要分配存储空间并转换为UTF-8。请注意,UTF-16字符串不是以零结尾的,同时\u0000是允许的,因此您需要保留字符串长度和jchar指针。

不要忘记释放你得到的字符串。字符串函数返回jchar *或jbyte *,它们不是局部引用,而是指向原始数据的C型指针。
在调用Release之前,它们保证有效,这意味着它们在本地方法返回时不会被释放。

传递给NewStringUTF的数据必须采用编辑后的UTF-8格式编码。一个常见的错误是从文件或网络流中读取字符数据,不进行过滤就将其传递给NewStringUTF。除非你知道数据是7位ASCII,否则您需要去除高位ASCII字符或将其转换为适当
的编辑后的UTF-8格式。如果你不这样做,UTF-16转换的结果可能并不是你期望的。扩展的JNI检查将扫描字符串并警告
相关的无效数据,但它们不会捕获所有内容。

Primitive Arrays
基本数组

JNI提供了访问数组对象的函数。虽然对象数组一次只能访问一个元素,但是可以直接读取和写入基本数组,就像它们在C中声明一样。

为了在不受VM实现的限制情况下使接口尽可能高效,Get <PrimitiveType> ArrayElements调用系列允许运行时环境返回指向实际元素的指针,或者分配内存并进行复制。无论哪种方式,返回的原始指针保证有效,直到相应的Release被调用(这意味着,如果数据未被拷贝,则数组对象将被固定并且不能被当做压缩堆栈的一部分进行重定向)。你必须释放你申请的每一个数组。另外,如果Get调用失败,则必须确保您的代码不会稍后尝试释放NULL指针。

您可以通过为isCopy参数传入非NULL指针来确定数据是否已被复制。这很少用。

Release调用接受以下三种之一作为模式参数。运行时执行的操作取决于它是返回了指向实际数据的指针还是它的副本:

    0
        Actual:数组对象是不固定的。
        Copy:数据被复制回来。带有副本的缓冲区被释放。
    JNI_COMMIT
        Actual:不做任何事情。
        Copy:数据被复制回来。带有副本的缓冲区不会被释放。
    JNI_ABORT
        Actual:数组对象是不固定的。早期写入不会被中止。
        Copy:带有副本的缓冲区被释放;对它的任何更改都会丢失。

检查isCopy标志的一个原因是要知道在对数组进行更改后是否需要使用JNI_COMMIT调用Release - 如果要在更改和执行使用数组内容的代码之间进行切换,则可以跳过无操作提交。检查此标志的另一个可能原因是有效处理JNI_ABORT。例如,您可能想要获取一个数组,修改某个位置的成员,传递某些成员元素到其他函数,然后放弃更改。
如果知道JNI为您制作新副本,则无需创建另一个"可编辑"的副本。如果JNI传递给你的是原始数据,那么你需要制作自己的副本。

一个常见的错误(比如在示例代码中)是假设* isCopy为false时,您可以跳过Release调用。 不是这种情况。 如果未分配复制缓冲区,则必须固定原始内存,并且垃圾回收器不能移动原始内存。

还要注意,JNI_COMMIT标志不会释放该数组,最终您需要使用另一个标志再次调用Release。


Region Calls

当你想要做的只是复制数据时,一个类似 Get<Type>ArrayElements和GetStringChars的调用,这可能会非常有用。
例如:

 jbyte* data = env->GetByteArrayElements(array, NULL);
 if (data != NULL) {
     memcpy(buffer, data, len);
     env->ReleaseByteArrayElements(array, data, JNI_ABORT);
 }


注:
GetByteArrayElements:将java中的byte数组array转化为jni中的jbyte数组,并jbyte使指针data指向jbyte数组的第一个元素

void *memcpy(void *destin, void *source, unsigned n); 从源内存地址的起始位置开始拷贝若干个字节到目标内存

ReleaseByteArrayElements:释放分配的jbyte数组,其中data指向GetByteArrayElements分配的空间

这将获取数组,将前len字节的元素复制出来,然后释放数组。 根据实现的不同,Get调用将固定或复制数组内容。代码复制数据(也许再一次),然后调用Release; 在这种情况下,JNI_ABORT确保不会产生第三个副本。


可以更简单地完成同样的事情:

env->GetByteArrayRegion(array, 0, len, buffer);//将jbyteArray数组拷贝到buffer中,buffer为待存放数据的jbyte数组

这有几个好处:

     只需一个JNI调用而不是两个,从而减少开销。
     不需要固定或额外的数据拷贝。
     减少程序员错误的风险 - 在发生失败后没有因为忘记调用Release带来的风险。

相似的,可以使用Set<Type>ArrayRegion 将数据复制到一个数组中,使用GetStringRegion或GetStringUTFRegion
从字符串中复制字符出来。
    
Exceptions
异常

当异常挂起时,你不能调用大多数JNI函数。 您的代码应该注意到异常(通过函数的返回值,ExceptionCheck或
ExceptionOccurred)并返回,或清除异常并处理它。

在异常挂起时唯一允许调用的JNI函数是:

    DeleteGlobalRef
    DeleteLocalRef
    DeleteWeakGlobalRef
    ExceptionCheck
    ExceptionClear
    ExceptionDescribe
    ExceptionOccurred
    MonitorExit
    PopLocalFrame
    PushLocalFrame
    Release<PrimitiveType>ArrayElements
    ReleasePrimitiveArrayCritical
    ReleaseStringChars
    ReleaseStringCritical
    ReleaseStringUTFChars
    

许多JNI调用可能会抛出异常,但通常会提供更简单的方法来检查失败。例如,如果NewString返回一个非NULL值,则不需要检查异常。但是,如果您调用某个方法(使用像CallObjectMethod函数),则必须始终检查一个异常,因为如果抛出异常,返回值无效。

请注意,由解释代码引发的异常不会释放本地堆栈帧,并且Android尚不支持C++异常。JNI Throw和ThrowNew指令只是在当前线程中设置一个异常指针。在从本地代码返回到托管代码后,异常将被记录并适当处理。

本地代码可以通过调用ExceptionCheck或ExceptionOccurred"捕捉"异常,并使用ExceptionClear清除它。通常,抛弃异常而不处理它们可能会导致问题。

没有用于操作Throwable对象的内置函数,所以如果你想(例如)获取异常字符串,你需要找到Throwable类,查找getMessage的方法ID"()Ljava/lang/String;",调用它,如果结果为非NULL,则使用GetStringUTFChars来获取可以传给printf(3)或等价方法的参数。

Extended Checking
JNI的错误检查很少。错误通常会导致崩溃。 Android还提供了一种称为CheckJNI的模式,该模式下JavaVM和JNIEnv
函数表指针被转换成指向调用标准实现之前执行扩展系列检查的函数表。

额外的检查包括:

    数组:尝试分配负数大小的数组。
    坏指针:将错误的jarray/jclass/jobject/jstring传递给JNI调用,或者将NULL指针传递给带有非空参数的JNI调用。
    类名称:传递非"java/lang/String" 格式的类名传递给JNI调用。
    关键调用:在"关键"get获取与其相应的release释放之间进行JNI调用。
    Direct ByteBuffers:将错误的参数传递给NewDirectByteBuffer。
    异常:在有异常待处理时进行JNI调用。
    JNIEnv *s:使用来自错误线程的JNIEnv *。
    jfieldIDs:使用一个NULL jfieldID或使用jfieldID将字段设置为错误类型的值(例如,尝试将StringBuilder分配给String
    字段),或者使用静态字段jfieldID来设置实例字段,反之亦然,或者使用来自一个类的jfieldID和另一个类的实例。
    jmethodIDs:在进行JNI调用时使用错误类型的jmethodID *:不正确的返回类型,静态/非静态不匹配,'this'
    (用于非静态调用)或错误类型(用于静态调用)的错误类型。
    引用:在错误的引用类型上使用DeleteGlobalRef/DeleteLocalRef。
    Release 模式:将错误的Release模式传递给Release调用(除了0,JNI_ABORT或JNI_COMMIT之外的参数)。
    类型安全性:从本地方法返回一个不兼容的类型(比如从一个声明返回String的方法中返回StringBuilder)。
    UTF-8:将无效的可编辑的UTF-8字节序列传递给JNI调用。

(方法和字段的访问限制仍未检查:访问限制不适用于本地代码。)


有几种启用CheckJNI的方法。

如果您使用模拟器,默认情况下CheckJNI处于打开状态。

如果您拥有一部root后的设备,则可以使用以下命令序列来重新启动运行时环境时启用CheckJNI:
    

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

    在这两种情况下,当运行时环境启动时,您会在logcat输出中看到类似下面的内容:
    D AndroidRuntime: CheckJNI is ON
    
    如果您有常规设备,则可以使用以下命令:
    adb shell setprop debug.checkjni 1
    
    这不会影响已经运行的应用程序,但是从该点开始,启动的任何应用程序都将启用CheckJNI。
    (将属性更改为任何其他值,或者只需重新启动就会再次禁用CheckJNI。)在这种情况下,下次启动应用
    程序时,您会在logcat输出中看到类似下面的内容:
    D Late-enabling CheckJNI
    
    
    您还可以在应用程序的清单中设置android:debuggable属性,以便仅为您的应用程序启用CheckJNI。
    请注意,Android构建工具会自动为特定的构建类型执行此操作。
    
Native Libraries
   您可以使用标准的System.loadLibrary从共享库加载本地代码。获取您的本地代码的首选方法是:

  •     从静态类初始化程序中调用System.loadLibrary。(请参阅前面的示例,在那里一个用于调用nativeClassInit)该参数是"未修饰"的库名称,因此如果加载"libfubar.so",您应该传递"fubar"。
  •     提供本地方法:jint JNI_OnLoad(JavaVM *vm,void *reserved)
  •     在JNI_OnLoad中,注册所有的本地方法。您应该声明方法为"静态",以便名称不占用设备符号表中的空间。


如果用C++编写,JNI_OnLoad函数应该看起来像这样:
   

    jint JNI_OnLoad(JavaVM* vm, void* reserved)
    {
        JNIEnv* env;
        if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
            return -1;
        }

        // Get jclass with env->FindClass.
        // Register methods with env->RegisterNatives.

        return JNI_VERSION_1_6;
    }


    您也可以调用带有共享库的完整路径名称的System.load。对于Android应用程序,您可能会发现从上下文对象获取应用
    程序专用数据存储区的完整路径很有用。

    这是推荐的方法,但不是唯一的方法。显式注册不是必需的,您也不需要提供JNI_OnLoad函数。您可以改为使用以特定
    方式命名的本地方法的"发现"(请参阅​​JNI规范中的详细信息),尽管这不太理想,因为如果方法签名错误,直到第一次方
    法实际上被使用前,您将不会知道它。

    关于JNI_OnLoad的另一个注意事项是:任何FindClass调用都会在用于加载共享库的类加载器的上下文中发生。通常,FindClass在解释堆栈的顶部使用与该方法相关联的加载器,或者如果没有(因为线程刚刚连接)它使用"System"类加载器。

这使得JNI_OnLoad成为查找和缓存类对象引用的便利之地。

64-bit Considerations
    Android目前在32位平台上运行。理论上它可以用于64位系统,但目前不能。在大多数情况下,这不是您在与本地代码
    交互时需要担心的问题,但是如果您计划存储指向本地对象的整数字段的指针时需注意,要支持使用64位指针
    的体系结构,您需要将原生指针存储在一个long字段中而不是一个int中。
    
Unsupported Features/Backwards Compatibility

    所有JNI 1.6功能都受支持,但有以下例外:

    DefineClass未实现。 Android不使用Java字节码或类文件,因此传入二进制类数据不起作用。

为了向后兼容较旧的Android版本,您可能需要了解:

    本地函数的动态查找
    在Android 2.0(Eclair)之前,在搜索方法名称时,'$'字符不能被正确转换为"_00024"。
    解决这个问题需要使用显式注册或将本地方法移出内部类。
    
    分离线程(Detaching threads)
    在Android 2.0(Eclair)之前,不能使用pthread_key_create析构函数来避免“退出前线程必须分离”的检查。(运行时环境
    也使用pthread key析构函数,所以竞争会发生,看哪个被首先调用。)

    弱全局引用(Weak global references)
    在Android 2.2(Froyo)之前,弱全局引用并未实现。旧版本会大力拒绝尝试使用它们。您可以使用
    Android平台版本常量来测试支持与否。

    在Android 4.0(Ice Cream Sandwich)之前,弱全局引用只能传递给NewLocalRef,NewGlobalRef和
    DeleteWeakGlobalRef。(该规范强烈鼓励程序员在对它们进行任何操作之前,创建对弱全局变量的严格
    引用,所以这不应该完全限制。)
    从Android 4.0(Ice Cream Sandwich)开始,弱全局引用可以像任何其他JNI引用一样使用。
    
    局部引用(Local references)
    Android 4.0以前,局部引用实际上就是指针。Android 4.0版本增加必要的性能来更好的支持垃圾收集器,但这意味着大量的JNI bug在旧版本中无法检测到。有关更多详细信息,请参阅JNI Local Reference Changes in ICS
    
    使用GetObjectRefType确定引用类型(Determining reference type with GetObjectRefType)
    在Android 4.0(Ice Cream Sandwich)之前,由于直接使用指针(见上文),不可能正确实现
    GetObjectRefType。作为替代,我们使用了一种按照顺序查看弱全局变量表,参数,局部变量表和全局变量表的方法。
    它第一次找到你的指针,它会报告你的引用是它正在检查的类型。这意味着,例如,如果您在全局jclass上调用了          GetObjectRefType,该jclass恰好与作为静态本地方法的隐式参数传递的jclass相同,那么您将得到JNILocalRefType而不是JNIGlobalRefType。
    
    FAQ: Why do I get UnsatisfiedLinkError?
    
    在处理本地代码时,看到这样的失败很常见:
   

   java.lang.UnsatisfiedLinkError: Library foo not found

   在某些情况下,这意味着库没有找到。 在其他情况下,该库存在但不能由dlopen(3)打开,
    失败的详细信息可以在异常信息中找到。

    您可能遇到"找不到库"异常的常见原因:

  •      该库不存在或对应用程序来说无法访问。 使用adb shell ls -l <path> 来检查它是否存在和访问权限。
  •      该库不是用NDK构建的。 这可能导致函数或库的依赖在设备上不存在。

     另一类UnsatisfiedLinkError错误看起来像:

 java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

    在logcat上你将看到:
    
    W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V
    
    这意味着运行时试图找到匹配的方法但不成功。一些常见的原因是:

    库没有被加载。检查logcat输出以获取关于库加载的消息。
    
    由于名称或签名不匹配,未找到该方法。这通常是由于:
        对于惰性方法查找,未能用extern"C"和适当的可见性(JNIEXPORT)声明C++函数。
        请注意,在Android4.0版本之前,JNIEXPORT宏是不正确的,因此在旧的jni.h中使用新的GCC将不起作用。
        您可以使用arm-eabi-nm来查看库中出现的符号;如果它们看起来受到损坏
        (类似于_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass而不是Java_Foo_myfunc),或者如果符号类型是小写't'
        而不是大写'T',那么您需要调整声明。
        
        对于显式注册,输入方法签名时出现小错误。确保您传递给注册调用的内容与日志文件中的签名匹配。请记住
        'B'是byte,'Z'是boolean。签名中的类名称以'L'开头,以';'结尾,使用'/'分隔包/类名称,并使用'$'分
        隔内部类名称(例如, Ljava/util/Map$Entry;)。

    使用javah自动生成JNI头文件可能有助于避免一些问题。
    
    
    FAQ: Why didn't FindClass find my class?
    
    (大部分建议同样适用使用GetMethodID或GetStaticMethodID查找方法失败,或使用GetFieldID或GetStaticFieldID查找字段失败。)

    确保类名字符串具有正确的格式。 JNI类名称以包名称开头,并以斜线分隔,如java/lang/String。如果你正在查找一个
    数组类,你需要从适当数量的方括号[开始,并且还必须用'L'和';'来包装这个类,所以一个String的一维数组应该是[Ljava/lang/String; 。
    如果您正在查找内部类,请使用'$'而不是'.'。通常,在.class文件上使用javap是查找类的内部名称的好方法。

    如果您使用的是ProGuard,请确保ProGuard不会剥离您的类。如果您的类/方法/字段仅在JNI中使用,则会发生这种情况。

    如果类名称看起来正确,那么您可能会遇到类加载器问题。 FindClass想要在与你的代码相关的类加载器中开始类搜索。它检  查调用堆栈,它看起来像这样:

       Foo.myfunc(Native Method)
        Foo.main(Foo.java:10)

    最上面的方法是Foo.myfunc。 FindClass查找与Foo类关联的ClassLoader对象并使用它。

    这通常会做你想要的操作。如果您自己创建线程(可能是通过调用pthread_create并将其与AttachCurrentThread附加在一起),可能会遇到麻烦。此时应用程序中没有堆栈帧。如果从该线程调用FindClass,则JavaVM将从"system"类加载器开始,而不是与应用程序关联的类加载器,因此尝试查找特定于应用程序的类将失败。

    有几种方法可以解决这个问题:

  •     在JNI_OnLoad中执行一次FindClass查找,然后缓存类引用以备后用。任何作为执行JNI_OnLoad的一部分的FindClass调用都将使用与调用System.loadLibrary的函数相关联的类加载器(这是一个特殊规则,用于使库初始化更加方便)。如果您的应用代码正在加载库, FindClass将使用正确的类加载器。
  •     将类的实例传递给需要它的函数,通过声明你的本地方法接受一个Class参数,然后传入Foo.class。
  •     缓存对ClassLoader对象的引用,并直接发出loadClass调用。这需要一些精力。

    
    FAQ: How do I share raw data with native code?
    
    您可能发现自己处于需要从托管代码和本地代码访问大量原始数据缓冲区的情况。常见的例子包括操作位图或声音样本。有两种基本方法。

    您可以将数据存储在一个byte[]中。这允许从托管代码进行非常快速的访问。然而,对于本地代码,无论如何,您不能保证不复制数据就能够访问数据。在一些实现中,GetByteArrayElements和GetPrimitiveArrayCritical将返回实际指向托管代码堆栈中原始数据的指针,但在其他实现中,它将在本地堆上分配缓冲区并复制数据。

    另一种方法是将数据存储在直接字节缓冲区(direct byte buffer)中。这些可以使用java.nio.ByteBuffer.allocateDirect或
    JNI NewDirectByteBuffer函数创建。与常规字节缓冲区不同,存储不会分配到托管代码的堆栈上,并且始终可以从本地代码直接访问(使用GetDirectBufferAddress获取地址)。取决于如何实现直接字节缓冲区的访问,从托管代码访问数据可能会非常缓慢。

    选择哪一个取决于两个因素:

    大部分数据访问是通过Java或C/C++编写的代码实现的吗?
    如果数据最终传递给系统API,它必须使用哪种形式? (例如,如果数据最终传递给需要byte []的函数,那么在直接ByteBuffer中执行处理可能是不明智的。)

    如果没有明确的赢家,请使用直接字节缓冲区(direct byte buffer)。对它们的支持直接构建在JNI中,未来版本中的性能应该会得到改进。
    

  关于jni更多资料参见Java Native Interface Specification

https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/jniTOC.html

版权声明:本文为博主原创文章,未经博主允许不得转载;来自https://blog.csdn.net/milanac007/article/details/79792804

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值