Android JNI开发建议

JNI提示

JNI是Java Native Interface。 它定义了Android从托管代码(用Java或Kotlin编程语言编写)编译的字节码的方式,以与本机代码(用C / C ++编写)进行交互。 JNI是供应商中立的,支持从动态共享库加载代码,虽然繁琐但有时相当有效。

注意:由于Android以与Java编程语言类似的方式将Kotlin编译为ART友好字节码,因此您可以根据JNI体系结构及其相关成本将此页面上的指南应用于Kotlin和Java编程语言。

如果您还不熟悉它,请阅读Java Native Interface Specification以了解JNI的工作原理和可用的功能。 界面的某些方面在第一次阅读时并不是很明显,因此您可能会发现接下来的几个部分很方便。

一般提示

尽量减少JNI层的占用空间。 这里有几个方面需要考虑。 您的JNI解决方案应该尝试遵循这些准则(下面按重要性顺序列出,从最重要的开始):

  • 最小化跨JNI层的资源编组。 跨越JNI层进行编组具有非常重要的成本。 尝试设计一个界面,以最大限度地减少编组所需的数据量以及必须对数据进行编组的频率。
  • 避免在托管编程语言编写的代码与可能的C ++编写的代码之间进行异步通信 。 这将使您的JNI界面更易于维护。 您通常可以通过使用与UI相同的语言保持异步更新来简化异步UI更新。 例如,不是通过JNI从Java代码中的UI线程调用C ++函数,而是最好在Java编程语言中的两个线程之间进行回调,其中一个线程进行阻塞C ++调用,然后通知UI线程当阻塞呼叫完成时。
  • 最大限度地减少JNI需要触摸或触摸的线程数。 如果确实需要在Java和C ++语言中使用线程池,请尝试在池所有者之间而不是在各个工作线程之间保持JNI通信。
  • 将您的接口代码保存在少量易于识别的C ++和Java源位置,以方便将来的重构。 考虑适当使用JNI自动生成库。

JavaVM和JNIEnv

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

JNIEnv提供了大多数JNI功能。 您的本机函数都接收JNIEnv作为第一个参数。

JNIEnv用于线程本地存储。 因此, 您无法在线程之间共享JNIEnv 。 如果一段代码没有其他方法来获取它的JNIEnv,你应该共享JavaVM,并使用GetEnv来发现线程的JNIEnv。 (假设它有一个;请参阅下面的AttachCurrentThread 。)

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

主题

所有线程都是Linux线程,由内核调度。 它们通常从托管代码(使用Thread.start )启动,但也可以在其他地方创建,然后附加到JavaVM。 例如,使用pthread_create启动的线程可以使用JNI AttachCurrentThreadAttachCurrentThreadAsDaemon函数附加。 在连接一个线程之前,它没有JNIEnv, 也无法进行JNI调用 。

附加本机创建的线程会导致构造java.lang.Thread对象并将其添加到“main” ThreadGroup ,使调试器可以看到它。 在已经连接的线程上调用AttachCurrentThread是一个无操作。

Android不会挂起执行本机代码的线程。 如果正在进行垃圾收集,或者调试器已发出挂起请求,则Android将在下次进行JNI调用时暂停该线程。

通过JNI连接的线程必须在退出之前调用DetachCurrentThread 。 如果直接编码是不方便的,在Android 2.0(Eclair)及更高版本中你可以使用pthread_key_create来定义在线程退出之前调用的析构函数,并从那里调用DetachCurrentThread 。 (将该键与pthread_setspecific一起使用以将JNIEnv存储在线程局部存储中;这样它将作为参数传递给析构函数。)

jclass,jmethodID和jfieldID

如果要从本机代码访问对象的字段,请执行以下操作:

  • 使用FindClass获取类的类对象引用
  • 使用GetFieldID获取字段的字段ID
  • 使用适当的内容(例如GetIntField获取字段的内容

同样,要调用方法,首先要获取类对象引用,然后获取方法ID。 ID通常只是指向内部运行时数据结构的指针。 查找它们可能需要进行多次字符串比较,但是一旦有了它们,实际调用获取字段或调用方法非常快。

如果性能很重要,那么查看值一次并将结果缓存在本机代码中非常有用。 由于每个进程限制一个JavaVM,因此将此数据存储在静态本地结构中是合理的。

在卸载类之前,类引用,字段ID和方法ID保证有效。 只有在与ClassLoader关联的所有类都可以进行垃圾回收时才会卸载类,这种情况很少见,但在Android中并非不可能。 但请注意, jclass是类引用, 必须通过调用NewGlobalRef 进行保护 (请参阅下一节)。

如果您想在加载类时缓存ID,并在卸载和重新加载类时自动重新缓存它们,初始化ID的正确方法是将一段代码添加到相应的代码中。类:

科特林

伴侣对象{
     / *
      *我们使用静态类初始化程序来允许本机代码缓存一些
      *场偏移。 这个本机函数查找并缓存有趣
      * class / field / method ID。 失败了。
      * /
    私人外部乐趣nativeInit()

    在里面 {
         nativeInit()
     }
 }

Java的

     / *
      *我们使用类初始化程序来允许本机代码缓存一些
      *场偏移。 这个本机函数查找并缓存有趣
      * class / field / method ID。 失败了。
      * /
     private static native void nativeInit();

    静态的 {
         nativeInit();
     }

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

本地和全球参考

每个参数都传递给本机方法,几乎​​JNI函数返回的每个对象都是“本地引用”。 这意味着它在当前线程中当前本机方法的持续时间内有效。 即使在本机方法返回后对象本身继续存在,引用也无效。

这适用于jobject所有子类,包括jclass , jstringjarray 。 (当启用扩展JNI检查时,运行时将警告您大多数引用误用。)

获取非本地引用的唯一方法是通过函数NewGlobalRefNewWeakGlobalRef 。

如果要保留较长时间段的引用,则必须使用“全局”引用。 NewGlobalRef函数将本地引用作为参数并返回全局引用。 在调用DeleteGlobalRef之前,保证全局引用有效。

这种模式通常在缓存从FindClass返回的FindClass ,例如:

  jclass localClass = env-> FindClass(“MyClass”);
 jclass globalClass = reinterpret_cast (env-> NewGlobalRef(localClass)); 

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

这样做的一个结果是您不能假定对象引用在本机代码中是常量或唯一的 。 表示对象的32位值可能与方法到下一个方法的一次调用不同,并且两个不同对象可能在连续调用上具有相同的32位值。 不要将jobject值用作键。

程序员必须“不要过度分配”本地引用。 实际上,这意味着如果您正在创建大量本地引用,也许在运行对象数组时,您应该使用DeleteLocalRef手动释放它们,而不是让JNI为您执行此操作。 实现仅需要为16个本地引用保留插槽,因此如果您需要更多,则应该EnsureLocalCapacity删除或使用EnsureLocalCapacity / PushLocalFrame来保留更多。

请注意, jfieldIDjmethodID是不透明类型,而不是对象引用,不应传递给NewGlobalRef 。 由GetStringUTFCharsGetByteArrayElementsGetStringUTFChars返回的原始数据指针GetByteArrayElements是对象。 (它们可以在线程之间传递,并且在匹配的Release调用之前有效。)

一个不寻常的案例值得单独提及。 如果使用AttachCurrentThread附加本机线程,则运行的代码将永远不会自动释放本地引用,直到线程分离。 您创建的任何本地引用都必须手动删除。 通常,在循环中创建本地引用的任何本机代码可能需要进行一些手动删除。

小心使用全局引用。 全局引用可能是不可避免的,但它们很难调试,并且可能导致难以诊断的内存(错误)行为。在其他条件相同的情况下,具有较少全局引用的解决方案可能更好。

UTF-8和UTF-16字符串

Java编程语言使用UTF-16。 为方便起见,JNI也提供了使用Modified UTF-8的方法。 修改后的编码对C代码很有用,因为它将\ u0000编码为0xc0 0x80而不是0x00。 关于这一点的好处是你可以依靠C风格的零终止字符串,适合与标准的libc字符串函数一起使用。 缺点是您无法将任意UTF-8数据传递给JNI并期望它能够正常工作。

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

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

传递给NewStringUTF的数据必须采用Modified UTF-8格式 。 一个常见的错误是从文件或网络流中读取字符数据并将其交给NewStringUTF而不对其进行过滤。 除非您知道数据是有效的MUTF-8(或7位ASCII,这是兼容的子集),否则您需要删除无效字符或将它们转换为正确的修改的UTF-8格式。 如果不这样做,UTF-16转换可能会产生意外结果。 CheckJNI - 默认情况下为模拟器打开 - 扫描字符串并在VM收到无效输入时中止VM。

原始数组

JNI提供了访问数组对象内容的函数。 虽然一次只能访问一个条目的对象数组,但可以直接读取和写入基元数组,就好像它们是用C语句声明的一样。

为了使接口尽可能高效而不约束VM实现, GetArrayElements系列调用允许运行时返回指向实际元素的指针,或者分配一些内存并进行复制。 无论哪种方式,返回的原始指针保证有效,直到发出相应的Release调用(这意味着,如果数据未被复制,则数组对象将被固定,并且不能作为压缩的一部分重新定位堆)。您必须ReleaseGet每个阵列。 此外,如果Get调用失败,则必须确保您的代码稍后不会尝试Release NULL指针。

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

Release调用采用一个mode参数,该参数可以包含三个值之一。 运行时执行的操作取决于它是否返回指向实际数据的指针或其副本:

  • 0
    • 实际:数组对象未固定。
    • 复制:复制数据。 释放带有副本的缓冲区。
  • JNI_COMMIT
    • 实际:什么都不做。
    • 复制:复制数据。 没有释放带有副本的缓冲区。
  • JNI_ABORT
    • 实际:数组对象未固定。 早期的写入不会中止。
    • 复制:释放带有副本的缓冲区; 对它的任何改变都会丢失。

检查isCopy标志的一个原因是知道在更改数组后是否需要使用JNI_COMMIT调用Release - 如果您在进行更改和执行使用数组内容的代码之间交替,您可以跳过无操作提交。 检查标志的另一个可能原因是有效处理JNI_ABORT。 例如,您可能希望获取一个数组,将其修改到位,将片段传递给其他函数,然后丢弃更改。 如果您知道JNI正在为您制作新副本,则无需创建另一个“可编辑”副本。 如果JNI将原件传给你,那么你需要制作自己的副本。

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

另请注意, JNI_COMMIT标志不会释放数组,您最终需要使用不同的标志再次调用Release 。

地区电话

除了GetArrayElementsGetStringChars这样的调用之外,当你想要做的就是复制数据时,可能会非常有用。 考虑以下:

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

这会抓取数组,将第一个len字节元素复制出来,然后释放数组。 根据实现, Get调用将固定或复制数组内容。代码复制数据(可能是第二次),然后调用Release ; 在这种情况下, JNI_ABORT确保没有第三个副本的机会。

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

  env-> GetByteArrayRegion(array,0,len,buffer); 

这有几个好处:

  • 需要一个JNI调用而不是2,从而减少开销。
  • 不需要固定或额外的数据副本。
  • 降低程序员错误的风险 - 没有在发生故障后忘记调用Release风险。

同样,您可以使用SetArrayRegion调用将数据复制到数组中,使用GetStringRegionGetStringUTFRegion将字符复制到String 。

例外

异常处于挂起状态时,不得调用大多数JNI函数。 您的代码需要注意异常(通过函数的返回值, ExceptionCheckExceptionOccurred )并返回,或清除异常并处理它。

在异常挂起时,您可以调用的唯一JNI函数是:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • ReleaseArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

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

请注意,解释代码抛出的异常不会展开本机堆栈帧,Android也不支持C ++异常。 JNI ThrowThrowNew指令只是在当前线程中设置了一个异常指针。 从本机代码返回托管后,将注意并正确处理该异常。

本机代码可以通过调用ExceptionCheckExceptionOccurred来“捕获” ExceptionOccurred ,并使用ExceptionClear清除它。 像往常一样,丢弃异常而不处理它们可能会导致问题。

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

扩展检查

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:在进行Call*Method JNI调用时使用错误的jmethodID:返回类型不正确,静态/非静态不匹配,'this'(非静态调用)或错误类(静态调用)类型错误。
  • 参考文献:在错误的引用类型上使用DeleteGlobalRef / DeleteLocalRef 。
  • 发布模式:将错误发布模式传递给发布调用( 0 , JNI_ABORTJNI_COMMIT以外的其他JNI_COMMIT)。
  • 类型安全:从本机方法返回不兼容的类型(从声明为返回String的方法返回StringBuilder,比方说)。
  • UTF-8:将无效的Modified UTF-8字节序列传递给JNI调用。

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

有几种方法可以启用CheckJNI。

如果您正在使用模拟器,则默认情况下CheckJNI处于启用状态。

如果您有root设备,则可以使用以下命令序列在启用CheckJNI的情况下重新启动运行时:

  adb shell停止
 adb shell setprop dalvik.vm.checkjni是的
 adb shell启动 

在这两种情况中,当运行时启动时,您将在logcat输出中看到类似的内容:

  D AndroidRuntime:CheckJNI已开启 

如果您有常规设备,则可以使用以下命令:

  adb shell setprop debug.checkjni 1 

这不会影响已经运行的应用程序,但从那时起启动的任何应用程序都将启用CheckJNI。 (将属性更改为任何其他值或只是重新启动将再次禁用CheckJNI。)在这种情况下,您将在下次应用程序启动时在logcat输出中看到类似的内容:

  D延迟启用CheckJNI 

您还可以在应用程序的清单中设置android:debuggable属性,以便为您的应用启用CheckJNI。 请注意,Android构建工具将自动为某些构建类型执行此操作。

本地图书馆

您可以使用标准System.loadLibrary从共享库加载本机代码。 使用本机方法的首选方法是:

  • 从静态类初始化程序调用System.loadLibrary 。 参数是“未修饰”的库名称,因此要加载“libfubar.so”,您将传入“fubar”。
  • 提供JNI_OnLoad函数: JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
  • JNI_OnLoad ,使用RegisterNatives注册所有本机方法。
  • 使用-fvisibility=hidden构建,以便只从您的库中导出JNI_OnLoad 。 这会产生更快,更小的代码,并避免与加载到应用程序中的其他库发生潜在冲突(但如果应用程序在本机代码中崩溃,则会创建不太有用的堆栈跟踪)。

静态初始化程序应如下所示:

 静态的 {
        的System.loadLibrary( “FUBAR”);
     } 

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

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

     //使用env-> FindClass获取jclass。
     //使用env-> RegisterNatives注册方法。

    返回JNI_VERSION_1_6;
 } 

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

使用JNI_OnLoad是推荐的方法,但不是唯一的方法。 使用RegisterNatives显式注册本机方法不是必需的,也不必提供任何JNI_OnLoad函数。 您可以使用以特定方式命名的本机方法的“发现”(有关详细信息,请参阅JNI规范 ),但这意味着如果方法签名错误,则在第一次方法之前您将不会知道它实际上是被调用的。

如果您只有一个具有本机方法的类,则对System.loadLibrary的调用在该类中是有意义的。 否则你应该从Application进行调用,这样你就知道它总是被加载,并且总是提前加载。

关于JNI_OnLoad另一个注意JNI_OnLoad :从那里进行的任何FindClass调用都将发生在用于加载共享库的类加载器的上下文中。 通常, FindClass使用与解释堆栈顶部的方法关联的加载器,或者如果没有(因为线程刚刚附加),它使用“系统”类加载器。 这使得JNI_OnLoad成为查找和缓存类对象引用的便利位置。

64位注意事项

要支持使用64位指针的体系结构,在Java域中存储指向本机结构的指针时,请使用long字段而不是int 。

不支持的功能/向后兼容性

支持所有JNI 1.6功能,但以下情况除外:

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

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

  • 动态查找本机函数

    在Android 2.0(Eclair)之前,在搜索方法名称时,'$'字符未正确转换为“_00024”。 解决此问题需要使用显式注册或将本机方法移出内部类。

  • 分离线程

    在Android 2.0(Eclair)之前,不可能使用pthread_key_create析构函数来避免“退出前必须分离线程”检查。 (运行时也使用了一个pthread键析构函数,所以它首先要看哪个被调用。)

  • 弱全球参考

    在Android 2.2(Froyo)之前,没有实现弱全局引用。 较旧的版本会强烈拒绝使用它们的尝试。 您可以使用Android平台版本常量来测试支持。

    在Android 4.0(Ice Cream Sandwich)之前,弱全局引用只能传递给NewLocalRef , NewGlobalRefDeleteWeakGlobalRef 。 (该规范强烈鼓励程序员在对它们做任何事情之前创建对弱全局变量的硬引用,所以这不应该是任何限制。)

    从Android 4.0(Ice Cream Sandwich)开始,弱全局引用可以像任何其他JNI引用一样使用。

  • 本地参考

    直到Android 4.0(冰淇淋三明治),本地引用实际上是直接指针。 Ice Cream Sandwich添加了支持更好的垃圾收集器所需的间接,但这意味着在旧版本中无法检测到大量JNI错误。 有关详细信息,请参阅ICS中的JNI本地参考更改 。

    在Android 8.0之前的Android版本中,本地引用的数量限制为特定于版本的限制。 从Android 8.0开始,Android支持无限制的本地引用。

  • 使用GetObjectRefType确定引用类型

    直到Android 4.0(冰淇淋三明治),由于使用直接指针(见上文),才能正确实现GetObjectRefType 。 相反,我们使用了一种启发式方法,按顺序查看弱全局表,参数,本地表和全局表。 第一次找到你的直接指针时,它会报告你的引用是它正在检查的类型。 这意味着,例如,如果您在全局jclass上调用了GetObjectRefType ,该jclass恰好与作为静态本机方法的隐式参数传递的jclass相同,那么您将获得JNILocalRefType而不是JNIGlobalRefType 。

常见问题:为什么我会收到UnsatisfiedLinkError ?

在处理本机代码时,看到这样的故障并不罕见:

  java.lang.UnsatisfiedLinkError:找不到库foo 

在某些情况下,它意味着它所说的 - 找不到图书馆。 在其他情况下,库存在但无法通过dlopen(3)打开,并且可以在异常的详细消息中找到失败的详细信息。

您可能遇到“未找到库”例外的常见原因:

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

另一类UnsatisfiedLinkError失败如下:

  java.lang.UnsatisfiedLinkError:myfunc
        在Foo.myfunc(原生方法)
        在Foo.main(Foo.java:10) 

在logcat中,您将看到:

  W / dalvikvm(880):没有找到原生LFoo的实现; .myfunc()V 

这意味着运行时尝试查找匹配方法但不成功。 一些常见的原因是:

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

使用javah自动生成JNI头可能有助于避免一些问题。

常见问题:为什么FindClass找不到我的课程?

(大多数建议同样适用于使用GetMethodIDGetStaticMethodID查找方法的失败,或者使用GetFieldIDGetStaticFieldID字段。)

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

如果您正在使用ProGuard,请确保ProGuard没有删除您的课程 。 如果您的类/方法/字段仅用于JNI,则会发生这种情况。

如果类名看起来正确,则可能会遇到类加载器问题。 FindClass希望在与您的代码关联的类加载器中启动类搜索。 它检查调用堆栈,它看起来像:

  Foo.myfunc(原生方法)
     Foo.main(Foo.java:10) 

最顶层的方法是Foo.myfunc 。 FindClass找到与Foo类关联的ClassLoader对象并使用它。

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

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

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

常见问题:如何与本机代码共享原始数据?

您可能会发现自己需要从托管代码和本机代码访问大型原始数据缓冲区。 常见示例包括操纵位图或声音样本。 有两种基本方法。

您可以将数据存储在byte[] 。 这允许从托管代码进行非常快速的访问。 但是,在本机方面,您无法保证能够访问数据而无需复制数据。 在某些实现中, GetByteArrayElementsGetPrimitiveArrayCritical将返回托管堆中原始数据的实际指针,但在其他实现中,它将在本机堆上分配缓冲区并复制数据。

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

选择使用哪个取决于两个因素:

  1. 大多数数据访问是否会发生在用Java或C / C ++编写的代码中?
  2. 如果数据最终被传递给系统API,那么它必须采用什么形式? (例如,如果数据最终传递给采用byte []的函数,则在直接ByteBuffer进行处理可能是不明智的。)

如果没有明确的赢家,请使用直接字节缓冲区。 对它们的支持直接构建在JNI中,并且在将来的版本中性能应该得到改善。

你可能感兴趣的:(#,Java,Android,JNI,Java)