JNI一些错误需要注意:

本文深入探讨JNI编程中常见的资源管理问题,包括错误检查、数据截取、对象引用、本地代码与JAVA间的接口简化、字段ID与方法ID的缓存、Unicode字符串的处理、访问权限失效、国际化考虑、VM资源释放、局部引用管理等关键点。通过实例分析,揭示了在JNI编程中避免常见错误和提升代码质量的有效策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


  1.1、错误检查
  1.2 传递非法参数
  1.3 jclass 和  jobject
  对象引用:jobject
  类引用 : jclass
  1.4 jboolean面临数据截取问题
  1.5 恰当使用Java和C
  *尽量让JAVA和C之间的接口简单化,C和JAVA间的调用过于复杂的话,会使得BUG调试、代码维护和JVM对代码进行优化都会变得很难。
    比如虚拟机很容易对一些JAVA方法进行内联,但对本地方法却无能为力。
*尽量少写本地代码。因为本地代码即不安全又是不可移植的,而且本地代码中的错误检查很麻烦。
    *让本地代码尽量独立。也就是说,实际使用的时候,尽量让所有的本地方法都在同一个包甚至同一个类中。
  1.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");


  1.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上面。
  1.8  Unicode字符串结尾
从GetStringChars和GetStringCritical两个方法获得的Unicode字符串不是以NULL结尾的,需要调用GetStringLength来获取字符串的长度。
一些操作系统,如Windows NT中,Unicode字符串必须以两个’\0’结尾,这样的话,就不能直接把GetStringChars得到的字符串传递给Windows NT系统的API,而必须复制一份并在字符串的结尾加入两个“\0”。
  1.9 访问权限失效
在本地代码中,访问方法和变量时不受JAVA语言规定的限制。
比如,可以修改private和final修饰的字段。并且,JNI中可以访问和修改heap中任意位置的内存。这些都会造成意想不到的结果。
比如,本地代码中不应该修改java.lang.String和java.lang.Integer这样的不可变对象的内容。否则,会破坏JAVA规范。
  2.0 忽视国际化
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字符串转化成本地字符串。
  2.1 确保释放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。
  2.2 过多的创建局部引用
大量的局部引用创建会浪费不必要的内存。一个局部引用会导致它本身和它所指向的对象都得不到回收。
尤其要注意那些长时间运行的方法、创建局部引用的循环和工具函数,充分得利用Pus/PopLocalFrame来高效地管理局部引用。
  2.3 使用已经失效的局部引用
  局部引用只在一个本地方法的调用期间有效,方法执行完成后会被自动释放。本地代码不应该把存储局部引用存储到全局变量中在其它地方使用。
 2.4 跨进程使用JNIEnv
JNIEnv这个指针只能在当前线程中使用,不要在其它线程中使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值