JNI/NDK学习笔记(二)

*** 说明:本文不代表博主观点,均是由以下资料整理的读书笔记。 ***

【参考资料】

1、向您的Android Studio项目添加C/C++代码
2、Google开发者文档 -- 添加C++代码到现有Android Studio项目中
3、JNI Tips 英文原版
4、JNI Tips 中文
5、极客学院 JNI/NDK 开发指南
6、极客学院 深入理解 JNI
7、使用CMake构建JNI环境
8、使用C和C++的区别
9、Google官方 NDK 文档
10、极客学院 NDK开发课程
11、ndk-build 构建 JNI 环境
12、开发自己的NDK程序
13、JNI/NDK开发教程
14、JNI层修改参数值
15、JNI引用和垃圾回收
16、《Android高级进阶》-- 顾浩鑫
17、《Android C++ 高级编程 -- 使用 NDK》 -- Onur Cinar


六、JNI 访问数组

JNI 中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是 JNI 的基本数据类型,可以直接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问 Java 传递给 JNI 层的数组,必须选择合适的 JNI 函数来访问和设置 Java 层的数组对象。

6.1 访问基本类型数组

Java 代码:

// 在本地代码中求数组中所有元素的和  
private native int sumArray(int[] arr);  

Native 代码:

extern "C"
JNIEXPORT jint JNICALL
Java_com_scu_miomin_learncmake_NativeLib_sumArray(
        JNIEnv *env,
        jclass cls,
        jintArray j_array) {

    jint i, sum = 0;
    jint *c_array;
    jint arr_len;

    //1. 获取数组长度
    arr_len = env->GetArrayLength(j_array);
    //2. 根据数组长度和数组元素的数据类型申请存放java数组元素的缓冲区(堆内存)
    c_array = (jint *) malloc(sizeof(jint) * arr_len);
    //3. 初始化缓冲区
    memset(c_array, 0, sizeof(jint) * arr_len);
    //4. 拷贝Java数组中的所有元素到缓冲区中
    env->GetIntArrayRegion(j_array, 0, arr_len, c_array);
    //5. 累加数组元素的和
    for (i = 0; i < arr_len; i++) {
        sum += c_array[i];
    }
    //6. 释放存储数组元素的缓冲区
    free(c_array);
    return sum;
}

在前面的例子当中,通过调用 GetIntArrayRegion,将 int 数组中的所有元素拷贝到 C 临时缓冲区中,然后在本地代码中访问缓冲区中的元素来实现求和的计算。另外 JNI 还提供一系列直接获取数组元素指针的函数 Get/ReleaseArrayElements,比如:GetIntArrayElements、ReleaseArrayElements、GetFloatArrayElements、ReleaseFloatArrayElements 等。下面我们用这种方式重新实现计算数组元素的和:

extern "C"
JNIEXPORT jint JNICALL
Java_com_scu_miomin_learncmake_NativeLib_sumArray2(
        JNIEnv *env,
        jclass cls,
        jintArray j_array) {

    jint i, sum = 0;
    jint *c_array;
    jint arr_len;

    // 可能数组中的元素在内存中是不连续的,JVM可能会复制所有原始数据到缓冲区,然后返回这个缓冲区的指针
    c_array = env->GetIntArrayElements(j_array, NULL);
    // 判断:JVM复制原始数据到缓冲区失败
    if (c_array == NULL) {
        return 0;
    }

    arr_len = env->GetArrayLength(j_array);
    for (i = 0; i < arr_len; i++) {
        sum += c_array[i];
    }

    // 释放有可能存在的缓冲区
    env->ReleaseIntArrayElements(j_array, c_array, 0);
    return sum;
}

GetIntArrayElements 第三个参数表示返回的数组指针是原始数组,还是拷贝原始数据到临时缓冲区的指针,如果是 JNI_TRUE:表示临时缓冲区数组指针,JNI_FALSE:表示临时原始数组指针。在获取到的指针必须做校验,因为当原始数据在内存当中不是连续存放的情况下,JVM 会复制所有原始数据到一个临时缓冲区,并返回这个临时缓冲区的指针。有可能在申请开辟临时缓冲区内存空间时,会内存不足导致申请失败,这时会返回 NULL。

为了让接口更有效率而不受VM实现的制约,GetArrayElements系列调用允许运行时返回一个指向实际元素的指针,或者是分配些内存然后拷贝一份。不论哪种方式,返回的原始指针在相应的Release调用之前都保证有效(这意味着,如果数据没被拷贝,实际的数组对象将会受到牵制,不能重新成为整理堆空间的一部分)。你必须释放(Release)每个你通过Get得到的数组。同时,如果Get调用失败,你必须确保你的代码在之后不会去尝试调用Release来释放一个空指针(NULL pointer)。

你可以用一个非空指针作为isCopy参数的值来决定数据是否会被拷贝。这相当有用。

Release类的函数接收一个mode参数,这个参数的值可选的有下面三种。而运行时具体执行的操作取决于它返回的指针是指向真实数据还是拷贝出来的那份:

  • 0
  • 真实的:实际数组对象不受到牵制
  • 拷贝的:数据将会复制回去,备份空间将会被释放。
  • JNI_COMMIT
  • 真实的:不做任何操作
  • 拷贝的:数据将会复制回去,备份空间将不会被释放。
  • JNI_ABORT
  • 真实的:实际数组对象不受到牵制.之前的写入不会被取消。
  • 拷贝的:备份空间将会被释放;里面所有的变更都会丢失。

在 Java 中创建的对象全都由 GC(垃圾回收器)自动回收,不需要像 C/C++ 一样需要程序员自己管理内存。GC 会实时扫描所有创建的对象是否还有引用,如果没有引用则会立即清理掉。当我们创建一个像 int 数组对象,本地代码想去访问时,发现这个对象正被 GC 线程占用了,这时本地代码会一直处于阻塞状态,直到等待 GC 释放这个对象的锁之后才能继续访问。为了避免这种现象的发生,JNI 提供了 Get/ReleasePrimitiveArrayCritical 这对函数,本地代码在访问数组对象时会暂停 GC 线程。不过使用这对函数也有个限制,在 Get/ReleasePrimitiveArrayCritical 这两个函数期间不能调用任何会让线程阻塞或等待 JVM 中其它线程的本地函数或JNI函数,和处理字符串的 Get/ReleaseStringCritical 函数限制一样。这对函数和 GetIntArrayElements 函数一样,返回的是数组元素的指针。

6.2 建议

1、对于小量的、固定大小的数组,应该选择 Get/SetArrayRegion 函数来操作数组元素是效率最高的。因为这对函数要求提前分配一个 C 临时缓冲区来存储数组元素,可以直接在 Stack(栈)上或用 malloc 在堆上来动态申请,当然在栈上申请是最快的。有童鞋可能会认为,访问数组元素还需要将原始数据全部拷贝一份到临时缓冲区才能访问而觉得效率低?我想告诉你的是,像这种复制少量数组元素的代价是很小的,几乎可以忽略。这对函数的另外一个优点就是,允许你传入一个开始索引和长度来实现对子数组元素的访问和操作(SetArrayRegion函数可以修改数组),不过传入的索引和长度不要越界,函数会进行检查,如果越界了会抛出 ArrayIndexOutOfBoundsException 异常。
2、如果不想预先分配 C 缓冲区,并且原始数组长度也不确定,而本地代码又不想在获取数组元素指针时被阻塞的话,使用 Get/ReleasePrimitiveArrayCritical 函数对,就像 Get/ReleaseStringCritical 函数对一样,使用这对函数要非常小心,以免死锁。
3、Get/ReleaseArrayElements 系列函数永远是安全的,JVM 会选择性的返回一个指针,这个指针可能指向原始数据,也可能指向原始数据的复制。
4、当你想做的只是拷出或者拷进数据时,可以选择调用像GetArrayElements和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函数而是不是两个,减少了开销。
  • 不需要指针或者额外的拷贝数据。
  • 减少了开发人员犯错的风险-在某些失败之后忘记调用Release不存在风险。

6.3 访问对象数组

JNI 提供了两个函数来访问对象数组,GetObjectArrayElement 返回数组中指定位置的元素,SetObjectArrayElement 修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据中的所有对象元素或者一次复制多个对象元素到缓冲区。因为字符串和数组都是引用类型,只能通过 Get/SetObjectArrayElement 这样的 JNI 函数来访问字符串数组或者数组中的数组元素。


七、C/C++ 访问 Java 实例方法和静态方法

** java 代码:**

public class ClassMethod {

    private static void callStaticMethod(String str, int i) {
        Log.i("Miomin", "ClassMethod::callStaticMethod called!-->str=" + str + ", " + " i=" + i);
    }

    private void callInstanceMethod(String str) {
        Log.i("Miomin", "ClassMethod::callStaticMethod called!-->str=" + str);
    }
}

** JNI 代码:**

extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_callJavaStaticMethod(
        JNIEnv *env,
        jclass cls) {

    jclass clazz = NULL;
    jstring str_arg = NULL;
    jmethodID mid_static_method;

    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    clazz = env->FindClass("com/scu/miomin/learncmake/ClassMethod");
    if (clazz == NULL) {
        LOGV("Class not found : ClassMethod.class");
        return;
    }

    // 2、从clazz类中查找callStaticMethod方法
    mid_static_method = env->GetStaticMethodID(clazz, "callStaticMethod",
                                               "(Ljava/lang/String;I)V");
    if (mid_static_method == NULL) {
        LOGV("Method callStaticMethod not found.");
        return;
    }

    // 3、调用clazz类的callStaticMethod静态方法
    str_arg = env->NewStringUTF("我是静态方法");
    env->CallStaticVoidMethod(clazz, mid_static_method, str_arg, 100);

    // 删除局部引用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(str_arg);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_callJavaInstaceMethod(
        JNIEnv *env,
        jclass cls) {

    jclass clazz = NULL;
    jobject jobj = NULL;
    jmethodID mid_construct = NULL;
    jmethodID mid_instance = NULL;
    jstring str_arg = NULL;

    // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
    clazz = env->FindClass("com/scu/miomin/learncmake/ClassMethod");
    if (clazz == NULL) {
        LOGV("Class not found : ClassMethod.class");
        return;
    }

    // 2、获取类的默认构造方法ID
    mid_construct = env->GetMethodID(clazz, "<init>", "()V");
    if (mid_construct == NULL) {
        LOGV("Default constructor of ClassMethod.class not found.");
        return;
    }

    // 3、查找实例方法的ID
    mid_instance = env->GetMethodID(clazz, "callInstanceMethod", "(Ljava/lang/String;)V");
    if (mid_instance == NULL) {
        LOGV("Method callInstanceMethod not found.");
        return;
    }

    // 4、创建该类的实例
    jobj = env->NewObject(clazz, mid_construct);
    if (jobj == NULL) {
        LOGV("Method callInstanceMethod not found.");
        return;
    }

    // 5、调用对象的实例方法
    str_arg = env->NewStringUTF("我是实例方法");
    env->CallVoidMethod(jobj, mid_instance, str_arg, 200);

    // 删除局部引用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(jobj);
    env->DeleteLocalRef(str_arg);
}

提示:其实GetMethodID的第三个参数 "(Ljava/lang/String;I)V" 指的是函数签名,签名规则详见此文:http://www.cnblogs.com/CCBB/p/3978847.html

注意:虽然函数结束后,JVM 会自动释放所有局部引用变量所占的内存空间。但还是手动释放一下比较安全,因为在 JVM 中维护着一个引用表,用于存储局部和全局引用变量,经测试在 Android NDK 环境下,这个表的最大存储空间是512 个引用,如果超过这个数就会造成引用表溢出,JVM 崩溃。(局部引用和全局引用在后面的文章中会详细介绍)


八、C/C++ 访问 Java 实例变量和静态变量

** Java代码:**

public class ClassField {

    private static int num;
    private String str;

    public int getNum() {
        return num;
    }

    public void setNum(int num) {
        ClassField.num = num;
    }

    public String getStr() {
        return str;
    }

    public void setStr(String str) {
        this.str = str;
    }
}

** Native代码:**

extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_accessInstanceField(
        JNIEnv *env,
        jclass cls,
        jobject obj) {
    jclass clazz;
    jfieldID fid;
    jstring j_str;
    jstring j_newStr;
    const char *c_str = NULL;

    // 1.获取ClassField类的Class引用
    clazz = env->GetObjectClass(obj);
    if (clazz == NULL) {
        return;
    }

    // 2. 获取ClassField类实例变量str的属性ID
    fid = env->GetFieldID(clazz, "str", "Ljava/lang/String;");

    // 3. 获取实例变量str的值
    j_str = (jstring) env->GetObjectField(obj, fid);

    // 4. 将unicode编码的java字符串转换成C风格字符串
    c_str = env->GetStringUTFChars(j_str, NULL);
    if (c_str == NULL) {
        return;
    }
    env->ReleaseStringUTFChars(j_str, c_str);

    // 5. 修改实例变量str的值
    j_newStr = env->NewStringUTF("This is C String");
    if (j_newStr == NULL) {
        return;
    }

    env->SetObjectField(obj, fid, j_newStr);

    // 6.删除局部引用
    env->DeleteLocalRef(clazz);
    env->DeleteLocalRef(j_str);
    env->DeleteLocalRef(j_newStr);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_scu_miomin_learncmake_NativeLib_accessStaticField(
        JNIEnv *env,
        jclass cls) {
    jclass clazz;
    jfieldID fid;
    jint num;

    //1.获取ClassField类的Class引用
    clazz = env->FindClass("com/scu/miomin/learncmake/ClassField");
    if (clazz == NULL) {    // 错误处理
        return;
    }

    //2.获取ClassField类静态变量num的属性ID
    fid = env->GetStaticFieldID(clazz, "num", "I");
    if (fid == NULL) {
        return;
    }

    // 3.获取静态变量num的值
    num = env->GetStaticIntField(clazz, fid);

    // 4.修改静态变量num的值
    env->SetStaticIntField(clazz, fid, 80);

    // 删除属部引用
    env->DeleteLocalRef(clazz);
}

因为实例变量str是 String 类型,属于引用类型。在 JNI 中获取引用类型字段的值,调用 GetObjectField 函数获取。同样的,获取其它类型字段值的函数还有 GetIntField,GetFloatField,GetDoubleField,GetBooleanField 等。

由于 JNI 函数是直接操作J VM 中的数据结构,不受 Java 访问修饰符的限制。即,在本地代码中可以调用 JNI 函数可以访问 Java 对象中的非 public 属性和方法。

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

推荐阅读更多精彩内容