JNI时容易出错的地方

本章总结了JNI实际应用中容易出错的一些情况供JNI程序员参考。
10.1 错误检查
编写本地方法时最常见的错误就是忘记检查是否发生了异常。我承认,JNI里面的异常检查确实比较麻烦,但是,这很重要。
10.2 向JNI函数传递非法参数
JNI不会检查参数是否正确,如果你自己不保证参数的正确有效,那么出现什么样的错误是未知的。通常,不检查参数的有效性在C/C++库中是比较常见的。
10.3 把jclass和jobject弄混
一开始使用JNI时,很容易把对象引用(jobject类型的值)和类引用(jclass类型的值)弄混。对象引用对应的是数组或者java.lang.Object及其子类的对象实例,而类引用对应的是java.lang.Class的实例。
像GetFieldID这样需要传入jclass作为参数的方法做的是一个类操作,因为它是从一个类中获取字段的描述。而GetIntField这样需要传入jobject作为参数的方法做的是一个对象操作,因为它从一个对象实例中获取字段的值。
10.4jboolean会面临数据截取的问题
Jboolean是一个8-bit unsigned的C类型,可以存储0255的值。其中,0对应常量JNI_FALSE,而1255对应常量JNI_TRUE。但是,32或者16位的值,如果最低的8位是0的话,就会引起问题。
假设你定义了一个函数print,需要传入一个jboolean类型的condition作为参数:
void print(jboolean condition)
{
/* C compilers generate code that truncates condition
to its lower 8 bits. /
if (condition) {
printf("true\n");
} else {
printf("false\n");
}
}
对上面这段代码来说,下面这样用就会出现问题:
int n = 256; /
the value 0x100, whose lower 8 bits are all 0 /
print(n);
我们传入了一个非0的值256(0X100),因为这个值的低8位(即,0)被截出来使用,上面的代码会打印“false”。
根据经验,这里有一个常用的解决方案:
n = 256;
print (n ? JNI_TRUE : JNI_FALSE);
10.5 编程的时候,什么用JAVA,什么时候用C?
这里有一些经验性的注意事项:
1、 尽量让JAVA和C之间的接口简单化,C和JAVA间的调用过于复杂的话,会使得BUG调试、代码维护和JVM对代码进行优化都会变得很难。比如虚拟机很容易对一些JAVA方法进行内联,但对本地方法却无能为力。
2、 尽量少写本地代码。因为本地代码即不安全又是不可移植的,而且本地代码中的错误检查很麻烦。
3、 让本地代码尽量独立。也就是说,实际使用的时候,尽量让所有的本地方法都在同一个包甚至同一个类中。
JNI把JVM的许多功能开发给了本地代码:类加载、对象创建、字段访问、方法调用、线程同步等。虽然用JAVA来做这些事情的时候很容易,但有时候,用本地代码来做很诱人。下面的代码会告诉你,为什么用本地代码进行JAVA编程是愚蠢的。假设我们需要创建一个线程并启动它,JAVA代码这样写:
new JobThread().start();
而用本地代码却需要这样:
/
Assume these variables are precomputed and cached:

  • Class_JobThread:  the class "JobThread"
    
  • MID_Thread_init:  method ID of constructor
    
  • MID_Thread_start: method ID of Thread.start()
    

/
aThreadObject =
(
env)->NewObject(env, Class_JobThread, MID_Thread_init);
if (aThreadObject == NULL) {
... /* out of memory /
}
(
env)->CallVoidMethod(env, aThreadObject, MID_Thread_start);
if ((env)->ExceptionOccurred(env)) {
... /
thread did not start /
}
比较起来,本地代码写会使用编程变得复杂,代码量大,错误处理多。通常,如果不得不用本地代码来做这些事的话,在JAVA中提供一个辅助函数,并在本地代码中对这个辅助函数进行回调。
10.6 混淆ID和引用
本地代码中使用引用来访问JAVA对象,使用ID来访问方法和字段。
引用指向的是可以由本地代码来管理的JVM中的资源。比如DeleteLocalRef这个本地函数,允许本地代码删除一个局部引用。而字段和方法的ID由JVM来管理,只有它所属的类被unload时,才会失效。本地代码不能显式在删掉一个字段或者方法的ID。
本地代码可以创建多个引用并让它们指向相同的对象。比如,一个全局引用和一个局部引用可能指向相同的对象。而字段ID和方法ID是唯一的。比如类A定义了一个方法f,而类B从类A中继承了方法f,那么下面的调用结果是相同的:
jmethodID MID_A_f = (
env)->GetMethodID(env, A, "f", "()V");
jmethodID MID_B_f = (env)->GetMethodID(env, B, "f", "()V");
10.7 缓存字段ID和方法ID
这里有一个缓存ID的例子:
class C {
private int i;
native void f();
}
下面是本地方法的实现,没有使用缓存ID。
// No field IDs cached.
JNIEXPORT void JNICALL
Java_C_f(JNIEnv env, jobject this) {
jclass cls = (
env)->GetObjectClass(env, this);
... /
error checking /
jfieldID fid = (
env)->GetFieldID(env, cls, "i", "I");
... /* error checking /
ival = (
env)->GetIntField(env, this, fid);
... /* ival now has the value of this.i */
}
上面的这些代码一般可以运行正确,但是下面的情况下,就出错了:
// Trouble in the absence of ID caching
class D extends C {
private int i;
D() {
f(); // inherited from C
}
}
类D继承了类C,并且也有一个私有的字段i。
当在D的构造方法中调用f时,本地方法接收到的参数中,cls指向提类D的对象,fid指向的是D.i这个字段。在这个本地方法的末尾,ival里面是D.i的值,而不是C.i的值。这与你想象的是不一样的。
上面这种问题的解决方案是:
// Version that caches IDs in static initializers
class C {
private int i;
native void f();
private static native void initIDs();
static {
initIDs(); // Call an initializing native method
}
}
本地方法这样实现:
static jfieldID FID_C_i;

JNIEXPORT void JNICALL
Java_C_initIDs(JNIEnv env, jclass cls) {
/
Get IDs to all fields/methods of C that
native methods will need. /
FID_C_i = (
env)->GetFieldID(env, cls, "i", "I");
}

JNIEXPORT void JNICALL
Java_C_f(JNIEnv env, jobject this) {
ival = (
env)->GetIntField(env, this, FID_C_i);
... /* ival is always C.i, not D.i */
}
字段ID在类C的静态初始时被计算并缓存下来,这样就可以确保缓存的是C.i的ID,因此,不管本地方法中接收到的jobject是哪个类的实例,访问的永远是C.i的值。
另外,同样的情况也可能会出现在方法ID上面。
10.8 Unicode字符串结尾
从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。一些操作系统,如Windows NT中,Unicode字符串必须以两个’\0’结尾,这样的话,就不能直接把GetStringChars得到的字符串传递给Windows NT系统的API,而必须复制一份并在字符串的结尾加入两个“\0”
10.9 访问权限失效
在本地代码中,访问方法和变量时不受JAVA语言规定的限制。比如,可以修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任意位置的内存。这些都会造成意想不到的结果。比如,本地代码中不应该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。否则,会破坏JAVA规范。
10.10 忽视国际化
JVM中的字符串是Unicode字符序列,而本地字符串采用的是本地化的编码。实际编码的时候,我们经常需要使用像JNU_NewStringNative和JNU_GetStringNativeChars这样的工具函数来把Unicode编码的jstring转化成本地字符串,要对消息和文件名尤其关注,它们经常是需要国际化的,可能包含各种字符。
如果一个本地方法得到了一个文件名,必须把它转化成本地字符串之后才能传递给C库函数使用:
JNIEXPORT jint JNICALL
Java_MyFile_open(JNIEnv *env, jobject self, jstring name,
jint mode)
{
jint result;
char *cname = JNU_GetStringNativeChars(env, name);
if (cname == NULL) {
return 0;
}
result = open(cname, mode);
free(cname);
return result;
}
上例中,我们使用JNU_GetStringNativeChars把Unicode字符串转化成本地字符串。

10.11 确保释放VM资源

JNI编程时常见的错误之一就是忘记释放VM资源,尤其是在执行路径分支时,比如,有异常发生的时候:
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv env, jclass cls, jstring jstr)
{
const jchar cstr =
(
env)->GetStringChars(env, jstr, NULL);
if (cstr == NULL) {
return;
}
...
if (...) { /
exception occurred /
/
misses a ReleaseStringChars call /
return;
}
...
/
normal return /
(
env)->ReleaseStringChars(env, jstr, cstr);
}
忘记调用ReleaseStringChars可能导致jstring永远被VM给pin着不被回收。一个GetStringChars必然要对应着一个ReleaseStringChars,下面的代码就没有正确地释放VM资源:
/* The isCopy argument is misused here! */
JNIEXPORT void JNICALL
Java_pkg_Cls_f(JNIEnv env, jclass cls, jstring jstr)
{
jboolean isCopy;
const jchar cstr = (env)->GetStringChars(env, jstr,
&isCopy);
if (cstr == NULL) {
return;
}
... /
use cstr /
/
This is wrong. Always need to call ReleaseStringChars. /
if (isCopy) {
(
env)->ReleaseStringChars(env, jstr, cstr);
}
}
即使在isCopy的值是JNI_FALSE时,也应该调用ReleaseStringChars在unpin掉jstring。
10.12 过多的创建局部引用
大量的局部引用创建会浪费不必要的内存。一个局部引用会导致它本身和它所指向的对象都得不到回收。尤其要注意那些长时间运行的方法、创建局部引用的循环和工具函数,充分得利用Pus/PopLocalFrame来高效地管理局部引用。
10.13 使用已经失效的局部引用
局部引用只在一个本地方法的调用期间有效,方法执行完成后会被自动释放。本地代码不应该把存储局部引用存储到全局变量中在其它地方使用。
10.14 跨进程使用JNIEnv
JNIEnv这个指针只能在当前线程中使用,不要在其它线程中使用。
10.15 错误的线程模型(Thread Models)
搞不明白,不翻了。。。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,099评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,473评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,229评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,570评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,427评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,335评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,737评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,392评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,693评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,730评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,512评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,349评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,750评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,017评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,290评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,706评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,904评论 2 335

推荐阅读更多精彩内容

  • _ 声明: 对原文格式以及内容做了细微的修改和美化, 主要为了方便阅读和理解 _ 一. 基础 Java Nativ...
    元亨利贞o阅读 5,861评论 0 34
  • 要使用NDK首先要了解NDK到底是什么? (英语:native development kit,简称NDK)是一种...
    cuiandroid阅读 2,118评论 0 2
  • 现在,你知道了如何通过JNI来访问JVM中的基本类型数据和字符串、数组这样的引用类型数据,下一步就是学习怎么样和J...
    738bc070cd74阅读 856评论 0 1
  • 本章是JNI设计思想的一个概述,在讲的过程中,如果有必要的话,还会对底层实现技术的原理做说明。本章也可以看作是JN...
    738bc070cd74阅读 439评论 0 0
  • 薪酬不等于薪资,只要约定给员工固定工资,就等于公司欠员工的钱,无论员工干好干坏,只要天天上班,就得支付工资,直接...
    83be8522010b阅读 207评论 0 0