专业做网站公司 前景,家教中介网站开发,wordpress 扫码登录,seo网页推广原文地址#xff1a;http://android.xsoftlab.net/training/articles/perf-jni.html
JNI的全称为Java Native Interface#xff0c;中文意思是Java本地接口。它定义了Java代码与C/C代码之间的交互方式。它是两者的桥梁#xff0c;支持从动态共享库中加载代码。虽然有些复杂…原文地址http://android.xsoftlab.net/training/articles/perf-jni.html
JNI的全称为Java Native Interface中文意思是Java本地接口。它定义了Java代码与C/C代码之间的交互方式。它是两者的桥梁支持从动态共享库中加载代码。虽然有些复杂但是它的执行效率还是蛮高的。
如果你对JNI还不太熟悉那么可以通过Java Native Interface Specification来了解一下JNI的大致工作流程以及JNI的特性。
JavaVM与JNIEnv
JNI定义了两个关键的数据结构”JavaVM”与”JNIEnv”。这两个函数本质上都为指向函数指针的指针表。JavaVM提供了”接口调用”功能该功能允许创建、销毁JavaVM。理论上每个进程可以拥有多个虚拟机但是在Android中只允许出现一个。
JNIEnv提供了大部分的JNI功能。任何本地方法都以JNIEnv为第一回调参数。
JNIEnv用于线程局部存储。正出于这个原因所以不能在线程间共享JNIEnv。如果不能够通过其它方式获取其对应的JNIEnv对象那么应该先共享JavaVM然后通过GetEnv函数获取该线程对应的JNIEnv(假设该线程拥有一个JNIEnv具体请往下看)。
C与C对JNIEnv和JavaVM的声明方式并不相同。头文件”jni.h”针对C或者C提供了不同的类型定义。正因为这个原因在头文件中包含JNIEnv参数并不是个明智的主意。
线程
Android中所有的线程都是Linux线程都由内核执行。通常由受控代码启动(比如Thread.start)但是也可以由别的地方创建然后再附加到JavaVM上启动。举个例子线程可以由pthread_create函数创建然后通过AttachCurrentThread或AttachCurrentThreadAsDaemon将其附加到JavaVM上执行。
Android并不会挂起正在执行本地代码的线程。如果垃圾收集正在进行或者调试器发起了挂起请求那么Android会在下次JNI调用时暂停线程。
通过JNI所附加的线程在退出前必须调用DetachCurrentThread函数。
jclass, jmethodID, 及jfieldID
如果需要在本地代码中访问对象的属性那么需要执行以下操作
通过FindClass获取类对象的引用通过GetFieldID获得属性的ID通过对应的方法获取对象的内容比如GetIntField
相应的如果要调用一个方法首先获取类对象的引用其次获取该方法的ID。ID通常只是指向了一个内部的运行时数据结构。查找这些方法通常需要进行若干次字符串比对但是一旦找到那么后期的获取属性或者方法调用都会非常的迅速。
如果性能对你很重要那么在找到这些属性或者方法之后应该将其缓存起来。因为Android中只允许每个进程有一个JavaVM的存在所以将这些数据缓存在一个静态本地结构中是合理的。
类的引用、属性的ID、方法的ID在这个类被卸载之前都可以保证它们有效。一个类只有在这种情况下才会被卸载该类所关联的ClassLoader也能被回收。虽然这几率很低但是在Android中不是没有可能的。
如果想在类加载的时候将这些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的查找与缓存。该方法会在类初始化的时候执行一次。就算是类被卸载后又重新加载那么这个方法还是会被执行一次。
局部引用全局引用
每个被回调到本地方法的参数以及几乎所有的通过JNI方法返回的对象都是局部变量。这意味着当前线程中该方法内的所有局部变量都是合法的。在本地方法返回之后虽然对象仍然存活但是引用却是无效的。
这适用于jobject所有的子类jclass, jstring, 以及jarray。
获取非局部变量的唯一方式就是通过NewGlobalRef及NewWeakGlobalRef函数获得。
如果需要长时间持有一段引用那么必须使用全局引用。NewGlobalRef函数会将一个局部引用转换为一个全局引用。在调用DeleteGlobalRef方法之前该全局引用一直有效。
这种模式通常用于缓存一个由FindClass返回的一个jclass对象
jclass localClass env-FindClass(MyClass);
jclass globalClass reinterpret_castjclass(env-NewGlobalRef(localClass));
所有的JNI方法都可以以这两种引用为参数。不过引用相同的值可能有不同的结果。举个例子以同一个引用为参数连续调用两次NewGlobalRef可能会得到不同的值。如果要查看两个引用是否指向了同一个对象必须使用IsSameObject函数。绝不要在本地代码中使用””比较两个引用。
绝不要认为在本地代码中的对象引用是个常量或者是唯一的。一个32位的值所代表的对象的方法调用可能与下次调用就有所不同这可能是因为两个不同的对象拥有相同的32位值。不要将jobject的值当做键使用。
程序员经常被要求不要过度的申请局部变量。这意味着如果你创建了大量的局部变量那么应当通过DeleteLocalRef函数手动的释放它们而不是让JNI为你做这些事情。
要注意jfieldIDs、jmethodID并不是对象引用所以不能够将它们传给NewGlobalRef函数使用。GetStringUTFChars函数与GetByteArrayElements函数所返回的原始数据指针也同样不是对象。
一个不寻常的情况需要单独说明一下如果通过AttachCurrentThread函数attach到了一个本地线程上那么在该线程被detache之前代码中所有的局部变量都不会被自动释放。任何创建的局部变量都需要手动删除。
UTF-8与UTF-16字符串
Java语言使用的是UTF-16字符串。为了方便起见JNI所提供的方法工作在Modified UTF-8字符串下。修正后的编码对于C语言代码很有用因为它将\u0000编码为了0xc0 0x80。
不要忘记释放你所获得的字符串。字符串函数会返回jchar* 或 jbyte*它们是指向原始数据的指针而不是本地引用。它们在被释放之前一直有效这意味着在本地方法返回后它们并没有被释放。
传给NewStringUTF函数的数据必须是Modified UTF-8格式。一个常见的错误就是从文件流或者网络流中读取字符串数据然后没有过滤就直接交给了NewStringUTF函数进行处理。除非你知道这些数据是7位的ASCII否则你需要剔除高位的ASCII字符串或者将它们转换为正确的Modified UTF-8格式。如果你不这么做那么转换的结果可能不是你想看到的。额外的JNI检查会扫描字符串并会警告你这是无效的数据但是它们不会捕获任何事情。
原始数组
JNI提供了用于访问对象数组的功能。然而同一时间只能对一个元素进行访问可以直接对数组今夕读写操作就好像直接在C中声明的一样。
为了使JNI接口尽可能的高效也不受虚拟机实现的限制调用GetArrayElements的相关函数可以返回一个指向实际值的指针或者可以申请一些内存以完成复制。无论哪种方法所返回的指针都可以保证是有效的直到相应的释放方法被触发。必须释放你所取得的每个数组。如果Get方法调取失败也需要保证不要去释放一个空的指针对象。
你可以通过isCopy参数来检测一个数组是否是由指针所拷贝过来的这一点很有用。
Release方法需要一个mode参数这个参数有三种值。运行时执行的操作取决于它返回指向实际数据的指针或者指针的副本
0 实际指针非final修饰的数组对象指针副本拷贝后的数组数据拷贝的缓冲区会被释放JNI_COMMIT 实际指针不做任何事情指针副本拷贝后的数组数据拷贝的缓冲区不会被释放JNI_ABORT 实际指针非final修饰的数组对象。早些写入不会被中止。指针副本所拷贝的缓冲区被释放缓冲区内的任何变更都会丢失。
检查isCopy标志的其中一个原因是需要知道在对数组作出变更之后是否需要调用JNI_COMMIT的相关释放方法如果要更改一个正在作出变更以及读取数组内容的操作那么可以根据该标志跳过这次操作。另一个可能的原因就是用于有效的处理JNI_ABORT。举个例子你可能想要得到一个数组然后对其修改之后将其传给一个函数。如果你知道JNI会为你做一个副本的话那么就不需要创建另外的可编辑副本了。如果JNI传回的是原始数据那么你自己需要创建一个副本。
一个常见的错误就是如果*isCopy是false那么可以不调用相关释放方法。但是事实并非如此如果没有申请拷贝缓冲区那么原始数据内存必定会被一直占用也不会被垃圾收集器回收。
还要注意的是JNI_COMMIT并不会释放数组你需要在另外的标志执行后再执行一次释放。
方法调用
JNI在方法使用上有两种方式一种如下所示 jbyte* data env-GetByteArrayElements(array, NULL);if (data ! NULL) {memcpy(buffer, data, len);env-ReleaseByteArrayElements(array, data, JNI_ABORT);}
上面这段代码首先得到了一个数组然后拷贝出len个字节的元素最后将这个数组释放。根据实现的不同Get调用会返回原始数据或者数据副本。在这个案例中JNI_ABORT可以确保不出现第三个副本。
另一种实现则要更简单一些 env-GetByteArrayRegion(array, 0, len, buffer);
对于此有若干建议 - 减少JNI调用可以节省开销。 - 不要原始数据或者额外的数据拷贝。 - 降低程序员出错的风险–他们会在某些操作失败后忘记调用相关的释放方法。
类似的你可以使用SetArrayRegion函数将数据拷贝到一个数组中GetStringRegion函数或GetStringUTFRegion可以从String拷贝任意长度的字符。
异常
当异常出现时请不要继续向下执行。代码应当注意到这些异常并返回或者处理这些异常。
当异常发生时只有以下JNI方法允许调用
DeleteGlobalRefDeleteGlobalRefDeleteLocalRefDeleteWeakGlobalRefExceptionCheckExceptionClearExceptionDescribeExceptionOccurredMonitorExitPopLocalFramePushLocalFrameReleaseArrayElementsReleasePrimitiveArrayCriticalReleaseStringCharsReleaseStringCriticalReleaseStringUTFChars
很多JNI函数都会抛出异常不过只提供了一种很简单的检查方法。比如如果NewString函数返回了一个非空的值那么就不需要检查异常。然而如果你调用一个方法比如CallObjectMethod那么就需要每次都检查一下异常因为如果异常被抛出后返回值是无效的。
主要注意的是由中断所抛出的异常不会释放本地栈帧Android目前也不支持C异常。JNI的Throw与ThrowNew结构也只是在当前的线程设置了一个异常指针。当异常发生时也只是返回到代码调用处异常也不会被正确的注意与处理。
本地代码可以通过ExceptionCheck函数或ExceptionOccurred函数捕获异常并可以通过ExceptionClear函数清理这些异常。通常情况下不处理这些异常会导致一些问题的出现。
JNI中并没有与Throwable相对应的映射函数所以如果你想获得异常字符串那么就需要先找到Throwable类然后查找相关的getMessage “()Ljava/lang/String;”方法ID然后调用这些方法如果返回的值是非空的话再调用GetStringUTFChars函数来获得你想得到的异常字符串最后将这些异常打印出来。
本地库
你可以通过标准的System.loadLibrary函数加载共享库中的本地代码。推荐获取本地代码的方法有
System.loadLibrary()该方法唯一的参数是一个简要的库名所以如果要加载”libfubar.so”你只需要传”fubar”即可。本地方法jint JNI_OnLoad(JavaVM* vm, void* reserved);在JNI_OnLoad方法内部注册所有的本地方法。如果将方法声明为”static”的话那么方法名将不会占用符号表的空间。
如果JNI_OnLoad函数是由C实现的话那么它看起来应该是这个样子
jint JNI_OnLoad(JavaVM* vm, void* reserved)
{JNIEnv* env;if (vm-GetEnv(reinterpret_castvoid**(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函数外加库的全限定名来加载本地库。
使用JNI_OnLoad另一个需要注意的是任何FindClass调用都会发生在类加载器的上下文环境中该类加载器用于加载共享库。通常情况下FindClass所用到的加载器位于解释栈的顶端如果还没有加载器那么它会使用系统的加载器。
64位的注意事项
Android目前运行于32位的平台上。虽然理论上可以为64位的平台构建系统但是目前它不是主要的目标。大多数情况下这不是你需要担心的事情但是如果要将指针存储于本地结构中的一个对象的Int属性上那么这就很值得关注了。为了支持64位指针结构你需要将本地指针存储于一个Long属性中。
不支持特性与向后兼容
支持所有的JNI1.6特性以及以下异常 - DefineClass 还没有实现。Android并没有使用Java的字节码以及类文件所以传入二进制的类数据是不会被执行的。
如果需要兼容Android老的版本那么应该检查以下部分
动态查询本地函数 在Android 2.0之前字符’$’在查找方法时不会被正确的转换为”_00024”。所以使用有关方法需要明确注册或者将内部类方法移出。分离线程 在Android 2.0之前无法使用pthread_key_create析构函数来避免”在退出之前必须分离线程”这项检查。弱的全局引用 在Android 2.2之前弱的全局引用还没有实现。之前的版本会拒绝使用它们。你可以使用Android平台版本来检测是否支持。在Android 4.0之前弱的全局引用只能被传入NewLocalRef, NewGlobalRef, 以及 DeleteWeakGlobalRef这几个函数。从Android 4.0开始弱的全局引用可以像其它JNI引用一样使用。本地引用 在Android 4.0之前本地引用实际上就是指针。在Android 4.0之后添加了必要的中间角色以便更好的支持垃圾回收器的工作不过这意味着有很多JNI的bug在老版本上无法察觉。查看JNI Local Reference Changes in ICS获取更多信息。通过GetObjectRefType检查引用类型 在Android 4.0之前由于直接指针的使用无法正确的实现GetObjectRefType。我们通过弱的全局表、参数、本地表以及全局表进行查找。首先它会找到你的直接指针并返回它所检查的引用类型。这意味着如果你在全局的jclass上作用GetObjectRefType而这个jclass以一个隐性参数传给了一个静态本地方法那么你将会获得JNILocalRefType而不是JNIGlobalRefType。