JNI开发规范——从细节开始

JNI是Java本地接口。它定义了Android从托管代码(以Java或kotlin编程语言编写)到本地代码(C/C++编写)交互编译成字节码的一种方式。JNI是厂商中立的,支持从动态共享库加载,虽然有时繁琐,但是合理高效。

如果你还不够熟悉它,可以阅读 Java Native Interface Specification来更好理解JNI是如何工作的,以及它有哪些可用接口。刚开始阅读时,可能部分接口还没呈现,所以你需要仔细耐心去阅读去探索细节。

通用提示

尽量减少JNI的占用空间。可以从几方面进行考虑,你的JNI代码需要遵循以下准则(顺序按照重要性从高到低进行排列):

减少JNI层的资源调用。跨JNI层调用需要付出巨大代价。尝试设计一个合理接口减少跨JNI层调用数据的数量,以及降低跨层次调用资源的频率。

尽可能避免托管代码与C++代码之间的异步通信。这样能保证你的JNI接口容易维护。你可以尝试以与UI相同的语言来更新UI,代替异步的UI更新。例如,不要通过JNI从Java的UI线程调用C++函数,最好在Java编程语言的两个线程之间进行回调,其中一个线程进行阻塞C++调用,然后阻塞完成时通知UI线程更新。

减少JNI层需要访问或被访问的线程数。如果你需要在Java与C++之间使用线程池,尽可能保持JNI与线程池管理者通信,而不是其他工作线程。

将你的JNI接口代码保存于C++与Java源代码易于识别的位置,以便后续的重构扩展。可以考虑使用JNI自动生成库。

JavaVM和JNIEnv

JNI定义两个关键数据结构:JavaVM和JNIEnv。这两个指针必须指向方法表。(在C++版本中,它们是带有函数表指针的类,每个JNI函数的成员函数都是通过表引导的)JavaVM提供“调用接口”函数,允许你创建与销毁JavaVM。理论上,你可以在同一个进程持有多个JavaVM,但是Android中只有一个。

JNIEnv提供大多数的JNI函数。你的本地函数都需要接收JNIEnv作为第一个参数。

扫描二维码关注公众号,回复: 8586009 查看本文章

JNIEnv用作线程私有存储。因此,你不能在线程间共享JNIEnv变量。如果一个代码块无法获取到JNIEnv,你可以共享JavaVM,使用GetEnv来获取来获取对应线程的JNIEnv。(假如它有,请看下面介绍的AttachCurrentThread)

C语言定义的JavaVM和JNIEnv,与C++定义的不一样。“jni.h”头文件提供不同的typedefs,取决于它包含于C或C++。因此,在头文件声明JNIEnv变量,不建议同时包含C/C++两种语言。(换另外一种方式:如果你的头文件声明#ifdef __cplusplus,你可能需要做额外的工作,如果你的头文件引用到JNIEnv)

线程

所有线程都是Linux线程,由内核调度。它们通常在调用层代码启动(使用Thread.start),但它们可以在任意地方创建,并且关联到JavaVM。例如,一个线程使用pthread_create来启动,可以使用JNI的AttachCurrentThread 或 AttachCurrentThreadAsDaemon函数来获取。在线程关联之前,它没有JNIEnv,无法进行JNI调用

关联一个本地创建的线程会导致java.lang.Thread对象被构造,添加到线程组中,使得它可被调试。在已经关联的线程中调用AttachCurrentThread是一个no-op。

Android在执行本地代码时不会挂起线程。如果垃圾回收器在运行,或者调试器发起挂起的请求,在下次调用JNI时,Android会暂停线程。通过JNI进行线程关联,需要在线程退出前调用DetachCurrentThread 。假如代码直接this显得笨拙,你可以使用pthread_key_create 来定义一个析构函数,在线程退出前调用,在那里调用DetachCurrentThread 。(使用pthread_setspecific 键来存储线程私有的JNIEnv,它可以通过参数传递到析构函数里。)

jclass, jmethodID, and jfieldID

如果你想在本地代码访问对象的变量,你需要如下操作:

·使用FindClass来获取类对象的引用;

·使用GetFieldId来获取变量id;

·使用GetIntField来获取变量内容;

类似地,调用一个方法,首先你需要获得类对象的引用,然后是方法id。IDs通常指向内部运行的数据结构。查找它们可能需要经过字符串比较,但是你实际调用获取变量或者调用方法,效率是非常高的。

假如你很注重性能,你可以在获取到数值后,缓存到本地代码中。因为每个进程限制只能拥有一个JavaVM,以静态方式把数据存储于本地也是很合理的。

在类卸载前,类引用、变量ID和方法ID保证有效。只有在类加载器关联的所有类都被垃圾回收,类才会被卸载,这在Android中几乎不会发生这种情况。注意,尽管jclass是一个类引用,也要调用NewGlobalRef 来接受保护。

如果你希望在类加载后缓存IDs,在类被卸载或者重新加载时重新进行缓存,初始化IDs的正确方法是在对应类里添加一个代码块,如下操作:

    /*
     * 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方法,需要查找IDs。当类被初始化时,代码会被执行一遍。如果类被卸载然后重新加载,它会被再次执行。

本地与全局引用

传递到本地方法的每个参数,由JNI函数返回的每个对象大多数是“局部引用”。这意味着,只有当前线程的当前本地方法调用有效。即使对象本身在本地方法return后继续存活,但对象引用实际是无效的

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

假如你希望一个引用保持更长的生命周期,你必须使用一个“全局”引用。NewGlobalRef 函数需要传递局部引用作为参数,返回一个全局引用。全局引用保证一直有效,直到你调用DeleteGlobalRef来释放。

当从FindClass返回一个jclass时,通常是这样创建全局引用的:

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

所有JNI方法都可以接收局部引用和全局引用作为参数。可能同一个对象的引用有不同的值。例如,在相同对象连续调用NewGlobalRef 可能返回不同引用值。

需要注意的是,jfieldID和jmethodID是不透明的类型,而不是对象引用,无法通过NewGlobalRef来创建。像GetStringUTFChars 和GetByteArrayElements 这些方法返回的原始指针也不是对象。(它们可以在线程间传递,在释放前都保持有效)。

谨慎使用全局引用。也许全局引用无法避免,但是它们难以调试,并且可能引发难以定位的内存行为。尽量减少全局引用是最好的权衡选择。

UTF-8和UTF-16字符串

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指针指向原始数据而不是局部引用。在被释放前,它都会有效,意味着在本地方法返回前它们不会被释放。

原生数组

JNI提供方法访问数组对象的内容。数组对象在任一时刻只能访问一个元素。如果原生数组在C里面声明,它们可读也可写。通常会犯的错误是,如果*isCopy是false,你会忽略释放数组了。

另外需要注意的是,JNI_COMMIT 标志并不会释放数组,你需要调用Release 来进行释放。

异常

当发生异常时,大多数JNI函数都无法被调用。你的代码应该注意异常(通过函数返回值,ExceptionCheck, 或ExceptionOccurred),异常发生时返回,或者清除异常并处理。

异常发生时,只有以下JNI函数可被调用:

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

大多数JNI调用都可以抛出异常,但也提供更简便的方式来检查是否运行失败。例如,如果NewString 返回值非空,那么你需要检查异常了。但是,当你调用方法时(例如CallObjectMethod),需要检查异常,因为异常发生时将会返回无效值。

本地代码可以通过调用ExceptionCheckExceptionOccurred来捕获异常,调用ExceptionClear来清除异常。通常,丢弃异常并不会真正地解决问题。

发布了63 篇原创文章 · 获赞 179 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/u011686167/article/details/81784979