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定义了两个关键数据结构,“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 AttachCurrentThread
或AttachCurrentThreadAsDaemon
函数附加。 在连接一个线程之前,它没有JNIEnv, 也无法进行JNI调用 。
附加本机创建的线程会导致构造java.lang.Thread
对象并将其添加到“main” ThreadGroup
,使调试器可以看到它。 在已经连接的线程上调用AttachCurrentThread
是一个无操作。
Android不会挂起执行本机代码的线程。 如果正在进行垃圾收集,或者调试器已发出挂起请求,则Android将在下次进行JNI调用时暂停该线程。
通过JNI连接的线程必须在退出之前调用DetachCurrentThread
。 如果直接编码是不方便的,在Android 2.0(Eclair)及更高版本中你可以使用pthread_key_create
来定义在线程退出之前调用的析构函数,并从那里调用DetachCurrentThread
。 (将该键与pthread_setspecific
一起使用以将JNIEnv存储在线程局部存储中;这样它将作为参数传递给析构函数。)
如果要从本机代码访问对象的字段,请执行以下操作:
FindClass
获取类的类对象引用GetFieldID
获取字段的字段IDGetIntField
获取字段的内容同样,要调用方法,首先要获取类对象引用,然后获取方法ID。 ID通常只是指向内部运行时数据结构的指针。 查找它们可能需要进行多次字符串比较,但是一旦有了它们,实际调用获取字段或调用方法非常快。
如果性能很重要,那么查看值一次并将结果缓存在本机代码中非常有用。 由于每个进程限制一个JavaVM,因此将此数据存储在静态本地结构中是合理的。
在卸载类之前,类引用,字段ID和方法ID保证有效。 只有在与ClassLoader关联的所有类都可以进行垃圾回收时才会卸载类,这种情况很少见,但在Android中并非不可能。 但请注意, jclass
是类引用, 必须通过调用NewGlobalRef
进行保护 (请参阅下一节)。
如果您想在加载类时缓存ID,并在卸载和重新加载类时自动重新缓存它们,初始化ID的正确方法是将一段代码添加到相应的代码中。类:
伴侣对象{
/ *
*我们使用静态类初始化程序来允许本机代码缓存一些
*场偏移。 这个本机函数查找并缓存有趣
* class / field / method ID。 失败了。
* /
私人外部乐趣nativeInit()
在里面 {
nativeInit()
}
}
/ *
*我们使用类初始化程序来允许本机代码缓存一些
*场偏移。 这个本机函数查找并缓存有趣
* class / field / method ID。 失败了。
* /
private static native void nativeInit();
静态的 {
nativeInit();
}
在执行ID查找的C / C ++代码中创建nativeClassInit
方法。 在初始化类时,代码将执行一次。 如果该类被卸载然后重新加载,它将再次执行。
每个参数都传递给本机方法,几乎JNI函数返回的每个对象都是“本地引用”。 这意味着它在当前线程中当前本机方法的持续时间内有效。 即使在本机方法返回后对象本身继续存在,引用也无效。
这适用于jobject
所有子类,包括jclass
, jstring
和jarray
。 (当启用扩展JNI检查时,运行时将警告您大多数引用误用。)
获取非本地引用的唯一方法是通过函数NewGlobalRef
和NewWeakGlobalRef
。
如果要保留较长时间段的引用,则必须使用“全局”引用。 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
来保留更多。
请注意, jfieldID
和jmethodID
是不透明类型,而不是对象引用,不应传递给NewGlobalRef
。 由GetStringUTFChars
和GetByteArrayElements
等GetStringUTFChars
返回的原始数据指针GetByteArrayElements
是对象。 (它们可以在线程之间传递,并且在匹配的Release调用之前有效。)
一个不寻常的案例值得单独提及。 如果使用AttachCurrentThread
附加本机线程,则运行的代码将永远不会自动释放本地引用,直到线程分离。 您创建的任何本地引用都必须手动删除。 通常,在循环中创建本地引用的任何本机代码可能需要进行一些手动删除。
小心使用全局引用。 全局引用可能是不可避免的,但它们很难调试,并且可能导致难以诊断的内存(错误)行为。在其他条件相同的情况下,具有较少全局引用的解决方案可能更好。
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指针。
不要忘记Release
你Get
的字符串 。 字符串函数返回jchar*
或jbyte*
,它们是原始数据的C样式指针,而不是本地引用。 它们在调用Release
之前保证有效,这意味着在本机方法返回时它们不会被释放。
传递给NewStringUTF的数据必须采用Modified UTF-8格式 。 一个常见的错误是从文件或网络流中读取字符数据并将其交给NewStringUTF
而不对其进行过滤。 除非您知道数据是有效的MUTF-8(或7位ASCII,这是兼容的子集),否则您需要删除无效字符或将它们转换为正确的修改的UTF-8格式。 如果不这样做,UTF-16转换可能会产生意外结果。 CheckJNI - 默认情况下为模拟器打开 - 扫描字符串并在VM收到无效输入时中止VM。
JNI提供了访问数组对象内容的函数。 虽然一次只能访问一个条目的对象数组,但可以直接读取和写入基元数组,就好像它们是用C语句声明的一样。
为了使接口尽可能高效而不约束VM实现, Get
系列调用允许运行时返回指向实际元素的指针,或者分配一些内存并进行复制。 无论哪种方式,返回的原始指针保证有效,直到发出相应的Release
调用(这意味着,如果数据未被复制,则数组对象将被固定,并且不能作为压缩的一部分重新定位堆)。您必须Release
您Get
每个阵列。 此外,如果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
。
除了Get
和GetStringChars
这样的调用之外,当你想要做的就是复制数据时,可能会非常有用。 考虑以下:
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);
这有几个好处:
Release
风险。同样,您可以使用Set
调用将数据复制到数组中,使用GetStringRegion
或GetStringUTFRegion
将字符复制到String
。
异常处于挂起状态时,不得调用大多数JNI函数。 您的代码需要注意异常(通过函数的返回值, ExceptionCheck
或ExceptionOccurred
)并返回,或清除异常并处理它。
在异常挂起时,您可以调用的唯一JNI函数是:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
ReleaseArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
许多JNI调用都可以抛出异常,但通常会提供一种更简单的方法来检查失败。 例如,如果NewString
返回非NULL值,则无需检查异常。 但是,如果调用方法(使用类似CallObjectMethod
的函数),则必须始终检查异常,因为如果抛出异常,返回值将无效。
请注意,解释代码抛出的异常不会展开本机堆栈帧,Android也不支持C ++异常。 JNI Throw
和ThrowNew
指令只是在当前线程中设置了一个异常指针。 从本机代码返回托管后,将注意并正确处理该异常。
本机代码可以通过调用ExceptionCheck
或ExceptionOccurred
来“捕获” ExceptionOccurred
,并使用ExceptionClear
清除它。 像往常一样,丢弃异常而不处理它们可能会导致问题。
没有用于操作Throwable
对象本身的内置函数,所以如果你想(比如说)获取异常字符串,你需要找到Throwable
类,查找getMessage "()Ljava/lang/String;"
的方法ID getMessage "()Ljava/lang/String;"
,调用它,如果结果是非NULL,则使用GetStringUTFChars
获取可以传递给printf(3)
或等效的东西。
JNI进行的错误检查很少。 错误通常会导致崩溃。 Android还提供了一种名为CheckJNI的模式,其中JavaVM和JNIEnv函数表指针被切换到在调用标准实现之前执行扩展系列检查的函数表。
额外的检查包括:
NewDirectByteBuffer
。Call*Method
JNI调用时使用错误的jmethodID:返回类型不正确,静态/非静态不匹配,'this'(非静态调用)或错误类(静态调用)类型错误。DeleteGlobalRef
/ DeleteLocalRef
。0
, JNI_ABORT
或JNI_COMMIT
以外的其他JNI_COMMIT
)。(仍未检查方法和字段的可访问性:访问限制不适用于本机代码。)
有几种方法可以启用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位指针的体系结构,在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
, NewGlobalRef
和DeleteWeakGlobalRef
。 (该规范强烈鼓励程序员在对它们做任何事情之前创建对弱全局变量的硬引用,所以这不应该是任何限制。)
从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
检查其存在和权限。另一类UnsatisfiedLinkError
失败如下:
java.lang.UnsatisfiedLinkError:myfunc
在Foo.myfunc(原生方法)
在Foo.main(Foo.java:10)
在logcat中,您将看到:
W / dalvikvm(880):没有找到原生LFoo的实现; .myfunc()V
这意味着运行时尝试查找匹配方法但不成功。 一些常见的原因是:
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',那么你需要调整声明。byte
,'Z'是boolean
。 签名中的类名组件以'L'开头,以';'结尾,使用'/'分隔包/类名,并使用'$'分隔内部类名称( Ljava/util/Map$Entry;
,比如说Ljava/util/Map$Entry;
)。使用javah
自动生成JNI头可能有助于避免一些问题。
FindClass
找不到我的课程?(大多数建议同样适用于使用GetMethodID
或GetStaticMethodID
查找方法的失败,或者使用GetFieldID
或GetStaticFieldID
字段。)
确保类名字符串具有正确的格式。 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
将使用正确的类加载器。Foo.class
,将类的实例传递给需要它的函数。ClassLoader
对象的引用,并直接发出loadClass
调用。 这需要一些努力。您可能会发现自己需要从托管代码和本机代码访问大型原始数据缓冲区。 常见示例包括操纵位图或声音样本。 有两种基本方法。
您可以将数据存储在byte[]
。 这允许从托管代码进行非常快速的访问。 但是,在本机方面,您无法保证能够访问数据而无需复制数据。 在某些实现中, GetByteArrayElements
和GetPrimitiveArrayCritical
将返回托管堆中原始数据的实际指针,但在其他实现中,它将在本机堆上分配缓冲区并复制数据。
另一种方法是将数据存储在直接字节缓冲区中。 这些可以使用java.nio.ByteBuffer.allocateDirect
或JNI NewDirectByteBuffer
函数创建。 与常规字节缓冲区不同,存储不在托管堆上分配,并且始终可以直接从本机代码访问(使用GetDirectBufferAddress
获取地址)。 根据直接字节缓冲区访问的实现方式,从托管代码访问数据可能非常慢。
选择使用哪个取决于两个因素:
ByteBuffer
进行处理可能是不明智的。)如果没有明确的赢家,请使用直接字节缓冲区。 对它们的支持直接构建在JNI中,并且在将来的版本中性能应该得到改善。