Android jni的关键函数和模型


native效率

效率的讨论

jni用法

不要用下面的方式使用jni:

  • Not caching method IDs, field IDs, and classes
  • Triggering array copies
  • Reaching back instead of passing parameters
  • Choosing the wrong boundary between native and Java code
  • Using many local references without informing the JVM ###android native 编程模型 官方文档

JNI函数文档

native代码通过JNI函数访问java虚拟机的功能,JNI函数是通过一个接口指针(interface pointer)来访问的。 接口指针是一个指向指针的指针(c语言),指向结构体的指针(c++)。c++在JNI函数在C的基础上进行了封装,内部通过inline函数调用了c的函数。

jni.h

struct _JNIEnv;
struct _JavaVM;
typedef const struct JNINativeInterface* C_JNIEnv;

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

struct JNINativeInterface {
    void*       reserved0;
    void*       reserved1;
    void*       reserved2;
    void*       reserved3;

    jint        (*GetVersion)(JNIEnv *);
    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*,
                        jsize);
    jclass      (*FindClass)(JNIEnv*, const char*);
    ....
};
struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

#if defined(__cplusplus)

    jint GetVersion()
    { return functions->GetVersion(this); }

    jclass DefineClass(const char *name, jobject loader, const jbyte* buf,
        jsize bufLen)
    { return functions->DefineClass(this, name, loader, buf, bufLen); }

    jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    ...
}

JNIEnv仅仅针对当前线程有效,不可以把JNIEnv作为参数传递到另一线程使用。native方法在被jvm调用时JNIEnv会作为参数传入native函数,jvm会保证在同一个线程调用native方法传入的是同一JNIEnv。 如果是不同的java线程调用同一native方法对应的JNIEnv参数时不同的。因为jvm是多线程的,所以native代码在编译时也应该使能多线程的配置,比如gcc的-DREENTRANT _D_POSIX_C_SOURCE

native方法的参数

JNIEnv是每个native方法的第一个参数,第二个参数根据对应的java方法是不是静态方法分为两种情况,对于非静态方法第二个参数是java对象的this,对应静态方法对应的第二个参数是java class。 参数的传值和传指针,基本数据类型integer、character、等是传值,也就是从java对象拷贝一份到native空间,对于任意的java对象都是使用传引用的方式。 jvm会在对应的java对象的引用上加一,所以被native引用的对象不会被垃圾回收。如果native对象不在使用时要及时释放对象,这样对应的java对象才能被回收。

JNI把对象引用分为两种,全局引用局部引用类似对应的全局变量和局部变量。局部引用会在超出可见域后自动销毁,全局引用则需要显示的自己销毁。所有传入native方法的参数均为局部引用, 所有的JNI函数的返回值也为局部引用。所有的局部引用仅仅在当前线程有效,不能跨线程使用。

访问java对象

...

访问java基本元素数组

当大量访问java对象是效率很低,所以直接在native代码里遍历java数组是不合适的,比如int数组,string等,一种方法是pinning通过把java对象的地址直接映射到native的代码空间,该方法有如下局限

  • jvm来及清理必须支持pinng
  • jvm的实现数组必须是连续内存的实现方式

为了克服上面的局限,jvm提供了如下方案:

  1. 提供了一套方法用来把数组的部分成员从java地址空间赋值到native的地址空间,这种方法适用于只访问少量数组成员的情况。
  2. 提供了pinng方式的一套方法屏蔽具体实现方式,具体实现jvm可能会有内存拷贝,也可能没有,取决于虚拟机的实现机制。
    • 如果jvm的垃圾回收支持pinng并且数组在内存中是连续存储的,则没有内存拷贝。
    • 否则,数组被拷贝到一处固定的地址空间(比如c的heap)并进行必须的格式转换,返回指向该拷贝地址的指针。

同时提供了释放的函数,在使用完数组后释放不用的资源(引用或内存)。

访问成员变量和方法

    jclass jcls = env->GetObjectClass(thiz); //jclass clazz = (*env)->FindClass(env, "com/inceptix/android/t3d/MainActivity");
    jmethodID jmid = env->GetMethodID(jcls,"calledByNative","(Ljava/lang/String;)Ljava/lang/String;");
    jstring  rlt = (jstring) env->CallObjectMethod(thiz,jmid,env->NewStringUTF("native call java parameter"));

需要注意方法ID(jmethodID)和成员ID(jfieldID)并不能阻止VM卸载相应的class,如果相应的额class被VM卸载,则jmethodID jfieldID失效。对应的ID的合法性需要使用者来保证。

缓存jmethodID jfieldID策略

    /*
     * 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();
    }

通过在对应的nativeInit中更新对应的jmethodID jfieldID来保证对应的缓存是更新的有效的。

异常(exception)的处理

natvie代码可以抛出任意异常,也可以处理java端的异常。JNI函数一般同时使用返回值和exception的方式,调用者可以根据返回值(比如NULL)判断是否需要获取异常。 可以使用ExceptionOccurred()JNI函数来获取。

在下面两种情况下,无论返回值如何,使用者需要检查异常。

  • JNI函数调用一个java的方法返回java方法的返回值,使用者需要使用ExceptionOccurred()来判断执行的java方法是否有异常。
  • 一些JNI数组的函数,不是返回错误码,而是抛出ArrayIndexOutOfBoundsException or ArrayStoreException异常。

native函数处理异常的方式有如下两种:

  • native方法直接返回,exception会直接返回到调用native的java代码中。
  • native方法可以通过ExceptionClear()清除exception,然后执行native的exception处理函数。

当有exception发生时,native的代码必须在调用其他JNI函数前先清除异常。

jni函数的两种注册方式

静态注册

private/public native void nativeFree();
javah –classpath bin/classes com.example.hellojni.HelloJni

生成类似下面的h头文件

jstring
Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env,
jobject thiz )
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}

动态注册(推荐)


jstring sayHello(JNIEnv* env, jobject thiz,jstring in){
    jboolean isCopy;
    const char* str = env->GetStringUTFChars(in, &isCopy);
    __android_log_print(ANDROID_LOG_WARN ,"fgt", "str is:%s iscopy:%d",str,isCopy);
    env->ReleaseStringUTFChars(in, str);

    jclass jcls = env->GetObjectClass(thiz);
    jmethodID jmid = env->GetMethodID(jcls,"calledByNative","(Ljava/lang/String;)Ljava/lang/String;");
    jstring  rlt = (jstring) env->CallObjectMethod(thiz,jmid,env->NewStringUTF("native call java parameter"));

    const char* str1 = env->GetStringUTFChars(rlt, &isCopy);
    __android_log_print(ANDROID_LOG_WARN ,"fgt", "jni call java return value is:%s iscopy:%d",str1,isCopy);
    env->ReleaseStringUTFChars(rlt, str1);

    return env->NewStringUTF("this is native return value");
}

static JNINativeMethod gJNI_Methods[] = {
        {
        "nativeHello",
        "(Ljava/lang/String;)Ljava/lang/String;",
        (void *)sayHello
        }
};

void regiserMethods(JNIEnv *env){
    if(env != NULL) {
        env->RegisterNatives(env->FindClass("com/tencent/timfeng/testnull/MainActivity"),gJNI_Methods, NELEM(gJNI_Methods));
        gEnv = env;
    }
}

jint JNI_OnLoad(JavaVM *jvm, void *reserved){
    JNIEnv *env = NULL;
    if(jvm->GetEnv((void **)&env, JNI_VERSION_1_4) != JNI_OK) {
        return 0;
    }

    regiserMethods(env);

    return JNI_VERSION_1_4;
}

字符串转换

参考

首先,需要明确几个关于编码的基本概念:

  • java内部是使用的16bit的unicode编码(utf-16)来表示字符串的,无论英文还是中文都是2字节;
  • jni内部是使用utf-8编码来表示字符串的,utf-8是变长编码的unicode,一般ascii字符是1字节,中文是3字节;
  • c/c++使用的是原始数据,ascii就是一个字节,中文一般是GB2312编码,用2个字节表示一个汉字。
       String 
      (UTF-16)
          |
[java]    |
--------------------  JNI 调用
[cpp]     |
          v
       jstring 
       (UTF-16)
          |   
 +--------+---------+
 |GetStringChars    |GetStringUTFChars
 |                  |
 v                  v
wchar_t*           char*
(UTF_16)           (UTF-8)
       String 
      (UTF-16)
          ^
          |
[java]    |
--------------------  JNI 返回
[cpp]     |
       jstring 
       (UTF-16)
          ^
          |   
 +--------+---------+
 ^                  ^
 |                  |
 |NewString         |NewStringUTF
wchar_t*          char*
(UTF_16)          (UTF-8)

jni 处理字符串乱码问题

连接


Copyright © FengGuangtu 2017