网站首页 > 资源文章 正文
前言
本文是JNI编程注意事项的第二篇文章。在上篇中讲解了 JavaVM/JNIEnv, Threads, jclass/jfieldID/jmethodID 以及 Local/Global 引用。今天我们继续讲解余下的部分。
Native 库
我们可以使用System.loadLibrary将共享库导入进来。引入Native代码的最好方法如下:
静态类初始化时,调用System.loadLibrary。参数是未声明的库名子,如要加载“libfubar.so”,你应传入“fubar”
提供一个本地函数 jint JNI_OnLoad(JavaVM* vm, void* reserved)。
在JNI_OnLoad函数里,注册所有Native方法。你应该用"static"声明方法 ,这样名子在设备的符号表里不占空间。
如果用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函数而不是System.loadLibrary。对于Andrioid应用来说, 您可能会发现从上下文对象获取应用程序的私有数据存储区域的完整路径非常方便。
上面的方法是推荐方法,但不是唯一的方法。其实,可以不需要显式注册JNI方法,也不需要提供JNI_OnLoad函数。您可以使用以特定方式命名的Native方法。但这种方式很不好,因为如果方法签名是错的,直到第一次它被使用时你才知道它出错了。
另一个关于JNI_OnLoad需要注意的事项:任何FindClass操作,都应该在加载共享库的类加载器上下文中调用。通常,FindClass使用与解释栈顶端方法相关联的加载器,如果没有(因为线程刚刚绑定),它将使用“系统”类加载器。这使JNI_OnLoad成为查找和缓存类对象引用的最好地方。
UTF-8 和 UTF-16 符字串
Java编程语言使用UTF-16编码。为了方便,JNI提供了与UTF-8一起使用的方法。但这种UTF-8是修改过的UTF-8编码方式。这种方式对于C代码是有用的,因为它将\u0000编码为0xc0 0x80而不是0x00。好处是,您可以依靠拥有C风格的零终止字符串。坏处是,您不能将任意的UTF-8数据传递给JNI,并希望它能正常工作。
如果可能,通常使用UTF-16字符串操作更快。在Android当前版本中,使用GetStringChars函数不需要拷贝其内容(它的内容是UTF-8编码),但使用GetStringUTFChars则需要分配和转换为UTF-8。请注意,UTF-16字符串不是以零终止的,\u0000被认为是正常数据,所以你需要自己保存字符串长度以及jchar指针。
不要忘记释放你获得的字符串。字符串函数返回jchar *或jbyte *,它们是C样式的指向原始数据的指针,而不是本地引用。它们被保证有效,直到调用Release,这意味着当native方法返回时它们不会自动释放。
传递给NewStringUTF的数据必须使用修改过的UTF-8格式。常见的错误是从文件或网络流读取字符数据,并将其传递给NewStringUTF,而不对其进行过滤。除非你知道数据是7位ASCII,否则你需要去掉高ASCII字符或将它们转换成适当的UTF-8格式。
如果不这样做,UTF-16转换可能不会是您期望结果的。扩展的JNI检查将扫描字符串并警告您它是无效数据,但它们不会捕获所有内容。
原始数组
JNI提供了访问数组对象内容的功能,虽然对象数组必须一次访问一个条目,但是可以直接读取和写入原始数组,就像它们在C中被声明一样。
使接口尽可能高效,除非受到VM实现的限制,Get<PrimitiveType>ArrayElements系列调用允许运行时返回指向实际元素的指针,或分配一些内存并复制他们。无论哪种方式,返回的原始指针都将保证是有效的,直到发出相应的Release调用(这意味着,如果数据未被复制,数组中的对象是固定的,并且不能被重新定位)。你必须释放你获得的每个数组,此外,如果Get调用失败,您必须确保代码不会释放这个空指针。
您可以通过传递isCopy参数是否是NULL来确定数据是否被复制了。但这种方式基本没什么用。
Release函数的mode参数有三种值。运行时的行为依赖于返回的是实际数据的指针还是其副本:
0
实际:数组对象是非固定的。
复制:数据被复制回来。具有副本的缓冲区被释放。
JNI_COMMIT
实际:什么都不做。
复制:数据被复制回来。具有副本的缓冲区被释放。
JNI_ABORT
实际:数组对象是非固定的。早期写入的数据不会被中止。
复制:具有副本的缓冲区被释放;对它的任何更改都会丢失。
检查isCopy标志的原因之一,是在更改数组后知道是否需要使用JNI_COMMIT参数调用Release。如果在更改数组和执行代码之间进行交替,你可以什么都不做。检查标志的第二个原因,是有效地处理JNI_ABORT。例如,您可能需要得到一个数组,修改它,并将其传递给其他函数,然后丢弃更改。如果您知道JNI正在为您制作新的副本,则无需创建另一个“可编辑的”副本。如果JNI传给你的是原始的数据,那么你需要自己做拷贝。
常见的错误,是认为如果 *isCopy为false,则可以跳过Release调用。如果没有分配复制缓冲区,则原始内存必须被固定,并且不能被垃圾收集器移动。另请注意,JNI_COMMIT标志不会释放数组,您需要再次使用不同的标志调用Release。
Region Calls
拷贝数据时有一种替代方法,例如,使用Ge<Type>ArrayElements和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);
这种方式有几种优势:
需要一个JNI调用而不是2,减少开销。
不需要固定或额外的数据拷贝。
减少程序员错误的风险 - 没有任何失败后忘记调用释放的风险。
类似地,您可以使用Set<Type>ArrayRegion调用将数据复制到数组中,并使用GetStringRegion或GetStringUTFRegion从字符串中复制字符。
异常
当异常待处理时,不能调用大多数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 "()java/lang/String;",并且如果结果是非空的,则使用GetStringUTFChars获取可以传递给printf(3)或等同物的信息。
扩展检查
JNI几乎没有错误检查,错误通常会导致崩溃。Android提供了一种称为CheckJNI的模式,在调用标准实现之前,将JavaVM和JNIEnv函数表指针切换到执行扩展系列检查的函数表。
扩展检查包括:
数组:尝试分配负大小的数组。
错误的指针:将一个坏的jarray/jclass/jobject/jstring传递给JNI调用,或者传递一个NULL指针到一个不可空参数的JNI调用。
类名称:传递类似 “java/lang/String” 样式的类名传给JNI调用。
Critical调用:在“Critical”获取和释放之间进行JNI调用。
Direct ByteBuffers:将错误的参数传递给NewDirectByteBuffer。
Exceptions:在异常挂起时进行JNI调用。
JNIEnv* :在错误的线程中使用 JNIEnv* 。
fieldIDs :使用空的jfieldID,或使用jfieldID将字段设置为错误类型的值(尝试将StringBuilder分配给String字段),或给静态 jfieldID设置实例的字段或者相反,或者使用一个类的实例但却用的另一个类的字段。
jmethodIDs:在进行调用时,使用错误的jmethodID方法做JNI调用:不正确的返回类型,静态/非静态不匹配,错误类型为'this'(非静态调用)或错误类(用于静态调用)。
References:使用DeleteGlobalRef/DeleteLocalRef时,用了错误的引用。
释放模式:将错误的mode值传递给Release(除0,JNI_ABORT或JNI_COMMIT之外)。
类型安全:从本机方法返回不兼容的类型(例如:从声明返回String的方法返回StringBuilder)。
UTF-8:将无效的修改后的UTF-8字节序列传递给JNI调用。
(方法和字段的辅助功能仍未被检查:访问限制不适用于Native代码。)
有几种启用CheckJNI的方法:
如是你使用的是模拟器,CheckJNI默认是打开的。
如果拥有root权限的设备,你可以使用下面的一系列命令重启 Runtime 并开启 CheckJNI:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
在这些情况下,当 Runtime 启动时,在 logcat 输出中可以看到如下信息:
D AndroidRuntime: CheckJNI is ON
如果你是一台普通设备,你可以使用下面的命令
adb shell setprop debug.checkjni 1
这不会影响已经运行的应用程序,但从该点启动的任何应用程序将启用CheckJNI。(将属性更改为任何其他值或重新启动将会再次禁用CheckJNI。)在这种情况下,你能在下次应用程序启动时在logcat输出中看到下面的信息:
D Late-enabling CheckJNI
您还可以在应用程序的manifest中设置android:debuggable属性,以便为您的应用程序启用CheckJNI。请注意,Android构建工具会自动为某些构建类型执行此操作。
常见问题
FAQ: 为什么会出现 UnsatisfiedLinkError?
在处理Native代码时,看到这样的失败并不罕见:
java.lang.UnsatisfiedLinkError: Library foo not found
在某些情况下这意味着,库没有发现。其它情况是说库存在,但不能由 dlopen 打开。失败的具体信息在异常的信息中可以找到。
您可能遇到“库未找到”异常的常见原因:
库不存在或应用程序无法访问。使用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”声明C ++函数和适当的可见性(JNIEXPORT)。 请注意,在Ice Cream Sandwich之前,JNIEXPORT宏不正确,因此使用新的GCC与旧的jni.h将无法正常工作。您可以使用arm-eabi-nm查看在库中出现的符号;如果它们看起来很像(_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass而不是Java_Foo_myfunc),或者如果符号类型是小写't'而不是大写字母'T',则需要调整声明。
对于显式注册,输入方法签名时会出现较小的错误。确保您传递到registration 调用的内容与日志文件中的签名相匹配。记住'B'是字节,'Z'是布尔值。签名中的类名称组件以'L'开始,以';'结尾,使用'/'分隔包/类名称,并使用'$'分隔内部类名称(Ljava / util / Map $ Entry; say )。
使用javah自动生成JNI头可能有助于避免一些问题。
FAQ: 为什么FindClass找不到我的类?
这个建议大多数情况下同样适用于使用GetMethodID或GetStaticMethodID无法找到方法,或无法找到GetFieldID或GetStaticFieldID字段)。
确保类名字符串格式正确。JNI类名以包名开头,并以斜杠分隔,如java/lang/String。如果您正在查找数组类,则需要从适当数量的方括号开始,并且还必须用'L'和';'包装类,所以String的一维数组将是[Ljava/lang/String;。如果你正在查找一个内部类,请使用'