JNI 入门

ref:

Android Studio开发JNI示例

Android NDK 开发(二)JNI 传递参数和返回值

Android NDK 开发(三)JNI 调用Java属性和方法

Android NDK 开发(四)JNI 中局部引用、全局引用和弱全局引用

Android深入理解JNI(二)类型转换、方法签名和JNIEnv

1 JNI和NDK介绍

JNI(Java Native Interface),是方便Java调用C、C++等Native代码所封装的一层接口,相当于一座桥梁。通过JNI可以操作一些Java无法完成的与系统相关的特性,尤其在图像和视频处理中大量用到。

NDK(Native Development Kit)是Google提供的一套工具,其中一个特性是提供了交叉编译,即C或者C++不是跨平台的,但通过NDK配置生成的动态库却可以兼容各个平台。比如C在Windows平台编译后生成.exe文件,那么源码通过NDK编译后可以生成在安卓手机上运行的二进制文件.so

1.1 AS环境配置

  1. 下载NDK
JNI 入门_第1张图片
image
  1. 配置环境变量

    sudo gedit  /etc/profile
    增加如下内容
    export ANDROID_NDK_ROOT=/home/rentianxin/android-sdk-linux/ndk-bundle
    export PATH=$PATH:$ANDROID_NDK_ROOT
    
  2. 修改build.gradle 增加配置

externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
    }

1.2 在AS中使用ndk-build开发JNI示例

Android Studio2.2之前对于JNI开发的支持不是很好,开发一般使用Eclipse+插件编写本地动态库。后面Google官方全面增强了对JNI的支持,包括内置NDK。

1.2.1 在AS中新建一个项目

1.2.2 声明一个native方法

package com.richy.richydemo.jni;

public class JNITest {
    public native static String getStrFromJNI();
}

1.2.3 通过javah命令生成头文件

rentianxin@rentianxin-Desk ~/A/RichyDemo> cd mobile/src/main/java/
rentianxin@rentianxin-Desk ~/A/R/m/s/m/java> javah -jni com.richy.richydemo.jni.JNITest 

生成头文件 com_richy_richydemo_jni_JNITest.h

实际项目最终可以不包含此头文件,不熟悉C的语法的开发人员,借助于该头文件可以知道JNI的相关语法:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include  /*首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体*/
/* Header for class com_richy_richydemo_jni_JNITest */

#ifndef _Included_com_richy_richydemo_jni_JNITest
#define _Included_com_richy_richydemo_jni_JNITest
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_richy_richydemo_jni_JNITest
 * Method:    getStrFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI
  (JNIEnv *, jclass);/*jclass是jni.h中定义好的,类型是jobject,实际上是一个不确定类型的指针,这里用来接收Java中的this*/

#ifdef __cplusplus
}
#endi

首先引入jni.h,里面包含了很多宏定义及调用本地方法的结构体。重点是方法名的格式。这里的JNIEXPORT和JNICALL都是jni.h中所定义的宏。JNIEnv *表示一个指向JNI环境的指针,可通过它来访问JNI提供的接口方法。jclass也是jni.h中定义好的,类型是jobject,实际上是一个不确定类型的指针,这里用来接收Java中的this。实际编写中一般只要遵循Java_包名_类名_方法名就好了。

1.2.4 编写mk文件实现JNI方法

像上面的头文件只是定义了方法,并没有实现,就像一个接口一样。这里就用C写一个简单的无参的JNI方法。
先创建一个jni目录,我直接在src的父目录下创建的,也可以在其他目录创建,因为最终只需要编译好的动态库。在jni目录下创建Android.mk和demo.c文件。

JNI 入门_第2张图片
image.png

Android.mk是一个makefile配置文件,安卓大量采用makefile进行自动化编译。LOCAL_MODULE定义的名称就是编译好的so库名称,比如这里是jni-demo,最终生成的动态库名称就叫libjni-demo.so。 LOCAL_SRC_FILES表示参与编译的源文件名称,这里就是demo.c

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE := jni-demo
LOCAL_SRC_FILES := demo.c

include $(BUILD_SHARED_LIBRARY)

(可选)编写 Application.mk :

# 指定生成哪些cpu架构的库
APP_ABI := armeabi-v7a
# 此变量包含目标 Android 平台的名称
APP_PLATFORM := android-22

这里的demo.c实现了一个很简单的方法,返回String类型。方法名是从前面生成的com_richy_richydemo_jni_JNITest.h直接拷贝。

#include

jstring Java_com_mercury_jnidemo_JNITest_getStrFromJNI(JNIEnv *env,jobject thiz){
    return (*env)->NewStringUTF(env,"I am Str from jni libs!");
}

也可以依赖之前生成的.h文件

#include
JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI
        (JNIEnv *env, jclass thiz) {
    return (*env)->NewStringUTF(env, "I am Str from jni libs!");
}

这时候NDK编译生成的动态库会有四个CPU平台:arm64-v8a、armeabi-v7a、x86、x86_64。如果创建Application.mk就可以指定要生成的CPU平台,语法也很简单:

APP_ABI := all

这样就会生成各个CPU平台下的动态库。

1.2.5 使用ndk-build编程生成.so库

切回到jni目录的父目录下,在Terminal中运行ndk-build指令,就可以在和jni目录同级生成一个libs文件夹,里面存放相对应的平台的.so库。同时生成的还有一个中间临时的obj文件夹,和jni文件夹可以一起删除。

rentianxin@rentianxin-Desk ~/A/R/m/s/main> ndk-build                                   
Android NDK: APP_PLATFORM not set. Defaulting to minimum supported version android-16. 
Android NDK: WARNING: APP_PLATFORM android-16 is higher than android:minSdkVersion 1 in ./AndroidManifest.xml. NDK binaries will *not* be compatible with devices older than android-16. See https://android.googlesource.com/platform/ndk/+/master/docs/user/common_problems.md for more information.    
[arm64-v8a] Compile        : jni-demo <= demo.c
[arm64-v8a] SharedLibrary  : libjni-demo.so
[arm64-v8a] Install        : libjni-demo.so => libs/arm64-v8a/libjni-demo.so
[armeabi-v7a] Compile thumb  : jni-demo <= demo.c
[armeabi-v7a] SharedLibrary  : libjni-demo.so
[armeabi-v7a] Install        : libjni-demo.so => libs/armeabi-v7a/libjni-demo.so
...

JNI 入门_第3张图片
生成文件

需要注意,使用NDK一定要先在build.gradle下要配置ndk-build的相关路径,这样在编写本地代码时才会有相关的提示功能,并且可以关联到相关的头文件

externalNativeBuild {
        ndkBuild {
            path 'src/main/jni/Android.mk'
        }
}

1.2.6 加载.so库并调用方法

在类初始化的时候要加载该.so库,一般会写在静态代码块里。名称就是前面的LOCAL_MODULE。

public class JNITest {

    static {
        System.loadLibrary("jni-demo");
    }

    public native static String getStrFromJNI();
}

需要注意的是如果是有参的JNI方法,那么直接在参数列表里补充在jni.h预先typedef好的数据类型就可以了。

public class JniTestActivityActivity extends BaseActivity {
    ...
    @OnClick(R.id.btn_load)
    public void onClick() {
        final String strFromJNI = JNITest.getStrFromJNI();
        logd(strFromJNI);
        mtvText.setText(strFromJNI);
    }
}

1.3 在AS中使用使用CMake开发JNI

CMake是一个跨平台的安装(编译)工具,通过编写CMakeLists.txt,可以生成对应的makefile或project文件,再调用底层的编译。AS 2.2之后工具中增加了对CMake的支持,官方也推荐用CMake+CMakeLists.txt的方式,代替ndk-build+Android.mk+Application.mk的方式去构建JNI项目.

1.3.1 创建使用CMake构建的项目

开始前AS要先在SDK Manager中安装SDK Tools->CMake

JNI 入门_第4张图片
image

只要勾选 Include C++ Support。其中会提示配置C++支持的功能.
JNI 入门_第5张图片
image.png

1.3.2 工程的目录结构

JNI 入门_第6张图片
image

创建好的工程主Module下直接就有.externalNativeBuild,多出一个 CMakeLists.txt,相当于以前的配置文件。并且在src/main目录下多了一个cpp文件夹,里面存放的是C++文件,相当于以前的jni文件夹。这个是工程创建后AS生成的示例JNI方法,返回了一个字符串。后面开发JNI就可以按照这个目录结构。

相应的,build.gradle下也增加了一些配置。

android {
    defaultConfig {
       ...
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    ...
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

defaultConfig中的externalNativeBuild各项属性和前面创建项目时的选项配置有关,外部的externalNativeBuild则定义了CMakeLists.txt的存放路径。

如果只是在自己的项目中使用,CMake的方式在打包APK的时候会自动将cpp文件编译成so文件拷贝进去。如果要提供给外部使用时,Make Project,之后在libs目录下就可以看到生成的对应配置的相关CPU平台的.so文件。

1.3.3 CMakeLists.txt

CMakeLists.txt可以自定义命令、查找文件、头文件包含、设置变量,具体可见 官方文档。项目默认生成的CMakeLists.txt核心内容如下:

# 编译本地库时我们需要的最小的cmake版本
cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.
# 相当于Android.mk
add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
# 添加一些我们在编译我们的本地库的时候需要依赖的一些库,这里是用来打log的库
find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
# 关联自己生成的库和一些第三方库或者系统库,先添加再关联
target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} 

直接编译项目只会在中间文件中产生so文件,如果想要明确看到生成的so,可以在CMakeLists.txt中指定so库的输出路径,这样编译后就会在libs目录下生成相应so文件,但一定要在add_library之前设置,否则不会生效:

#指定路径
#生成的so库在和CMakeLists.txt同级目录下的libs文件夹下
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY 
    ${PROJECT_SOURCE_DIR}/libs/${ANDROID_ABI})

如果想要配置so库的目标CPU平台,可以在build.gradle中设置

android {
    ...
    defaultConfig {
        ...
        ndk{
            abiFilters "x86","armeabi","armeabi-v7a"
        }
    }
    ...
  
}

需要注意的是,如果是多次使用add_library,则会生成多个so库。如果想将多个本地文件编译到一个so库中,只要最后一个参数添加多个C/C++文件的相对路径就可以

1.3.4 示例-用C语言实现字符串加密

Java中实现字符串加密的一种比较简单的方法是异或,将字符串转换为字符数组,遍历对其中的每个字符用密钥(可以是字符)进行一次异或运算,生成新的字符串。如果用JNI和C实现,大致步骤如下(jstring是要加密的字符串):

  1. 获取jstring的长度

  2. 动态开辟一个跟data长度一样的char*

  3. 将jstring类型转换为char数组(用char*接收)

  4. 遍历char数组,进行异或运算

  5. 将char*转换为jstring类型返回

  6. 释放动态开辟的堆内存空间

完整代码如下:

//CMakeDemo/app/src/main/cpp/native-lib.c
#include
#include 

jboolean checkUtfBytes(const char *bytes, const char **errorKind);

jstring Java_com_richy_cmakedemo_MainActivity_encryptStr
        (JNIEnv *env, jobject object, jstring data) {
    if (data == NULL) {//字符串为空
        return (*env)->NewStringUTF(env, "");
    }
    jsize len = (*env)->GetStringLength(env, data);//得到字符串长度
    char *buffer = (char *) malloc(len * sizeof(char));//分配内存控件 char *
    (*env)->GetStringUTFRegion(env, data, 0, len, buffer);//将data放入buffer
    int i = 0;
    for (; i < len; i++) {
        buffer[i] = (char) (buffer[i] ^ 2);//和2异或
    }

    const char *errorKind = NULL;
    checkUtfBytes(buffer, &errorKind);//排除非utf-8字符,errorKind会改变
    free(buffer);//释放内存
    if (errorKind == NULL) {
        return (*env)->NewStringUTF(env, buffer);
    } else {
        return (*env)->NewStringUTF(env, "");
    }
}

//把char*和errorKind传入,如果errorKind不为NULL说明含有非utf-8字符,做相应处理
//char **是二级char指针,表示字符串数组(第一级为字符串,第二级为数组
jboolean checkUtfBytes(const char *bytes, const char **errorKind) {
    while (*bytes != '\0') {
        jboolean utf8 = *(bytes++);
        // Switch on the high four bits.
        switch (utf8 >> 4) {
            case 0x00:
            case 0x01:
            case 0x02:
            case 0x03:
            case 0x04:
            case 0x05:
            case 0x06:
            case 0x07:
                // Bit pattern 0xxx. No need for any extra bytes.
                break;
            case 0x08:
            case 0x09:
            case 0x0a:
            case 0x0b:
            case 0x0f:
                /*
                 * Bit pattern 10xx or 1111, which are illegal start bytes.
                 * Note: 1111 is valid for normal UTF-8, but not the
                 * modified UTF-8 used here.
                 */
                *errorKind = "start";
                return utf8;
            case 0x0e:
                // Bit pattern 1110, so there are two additional bytes.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                // Fall through to take care of the final byte.
            case 0x0c:
            case 0x0d:
                // Bit pattern 110x, so there is one additional byte.
                utf8 = *(bytes++);
                if ((utf8 & 0xc0) != 0x80) {
                    *errorKind = "continuation";
                    return utf8;
                }
                break;
        }
    }
    return 0;
}

1.4 JNIEnv 是什么

JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的,JNIEnv的主要作用有两点:
1.调用Java的方法。
2.操作Java(获取Java中的变量和对象等等)。

先来看JNIEnv的定义,如下所示。
libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型 
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型  
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。
在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。

通过JavaVM的AttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。还要记得在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。

2 JNI 传递参数和返回值

在native层实现 getStrFromJNI 静态方法;

JNIEXPORT jstring JNICALL Java_com_richy_richydemo_jni_JNITest_getStrFromJNI
  (JNIEnv *, jclass);

由此看出,每个native函数,都至少有两个参数(JNIEnv*,jclass或者jobject)

  • 当native方法为静态方法时:
    jclass 代表native方法所属类的class对象(JniTest.class)
  • 当native方法为非静态方法时:
    jobject 代表native方法所属的对象

2.1 Java基本数据类型传递

在Java世界中可以使用boolean,byte,char,short,int,long,float,double,但是native世界可没有这些类型,对应的jni中提供了相应的替代,如jboolean,jbyte,jchar,jshort,jint,jlong,jfloat,jdouble 等,这几种类型几乎都可以当成对应的C++类型来用。

Java基本数据类型与JNI数据类型的映射关系如下:

2.1.1 基本数据类型的转换

Java Native Signature
byte jbyte B
char jchar C
double jdouble D
float jfloat F
int jint I
short jshort S
long jlong J
boolean jboolean Z
void void V

从上表可以可看出,基本数据类型转换,除了void,其他的数据类型只需要在前面加上“j”就可以了。第三列的Signature 代表签名格式,后文会介绍它。接着来看引用数据类型的转换。

2.1.2 引用数据类型的转换

Java Native Signature
所有对象 jobject L+classname +;
Class jclass Ljava/lang/Class;
String jstring Ljava/lang/String;
Throwable jthrowable Ljava/lang/Throwable;
Object[] jobjectArray [L+classname +;
byte[] jbyteArray [B
char[] jcharArray [C
double[] jdoubleArray [D
float[] jfloatArray [F
int[] jintArray [I
short[] jshortArray [S
long[] jlongArray [J
boolean[] jbooleanArray [Z

从上表可一看出,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。

另外,引用数据类型还具有继承关系,如下所示:

JNI 入门_第7张图片
image

2.2 参数传递示例

2.2.1 String参数的传递

JNIEXPORT jstring JNICALL
Java_com_richy_cmakedemo_JniTest_getLine(JNIEnv *env, jobject instance, jstring prompt_) {
    //将java字符串转化为c语言可以识别的字符串
    const char *prompt = env->GetStringUTFChars(prompt_, 0);
    if (prompt == NULL) {
        return NULL;
    }
    std::cout << prompt << std::endl;

    //释放资源
    env->ReleaseStringUTFChars(prompt_, prompt);

    char *temStr = "return string";
    //编码成java字符串
    jstring rtStr = env->NewStringUTF(temStr);
    return rtStr;
}

2.2.2 数组参数的传递

int compare(int *a, int *b) {
    return (*a) - (*b);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_richy_cmakedemo_JniTest_giveArray(JNIEnv *env, jobject instance, jintArray array_) {
//    jintArray -> jint指针 -> c int 数组
    jint *array = env->GetIntArrayElements(array_, NULL);
    //printf("%#x,%#x\n", &array, &array_);

    //数组的长度
    int len = env->GetArrayLength(array_);
    //排序
    qsort(array, len, sizeof(jint), compare);

    //同步
    //mode
    //0, Java数组进行更新,并且释放C/C++数组
    //JNI_ABORT, Java数组不进行更新,但是释放C/C++数组
    //JNI_COMMIT,Java数组进行更新,不释放C/C++数组(函数执行完,数组还是会释放)
    env->ReleaseIntArrayElements(array_, array, JNI_COMMIT);
}

这个代码中的GetIntArrayElementsReleaseIntArrayElements函数就是JNI提供用于处理int数组的函数。

如果用arr[i]的方式去访问jintArray类型,不用问肯定会出错。
JNI还提供了另一对函数GetIntArrayRegionReleaseIntArrayRegion访问int数组,不在这里做介绍,至于其他的类型数组,方法类似。

2.2.3 返回数组

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_richy_cmakedemo_JniTest_getArray(JNIEnv *env, jobject instance, jint len) {
    //创建一个指定大小的数组
    jintArray jint_arr = env->NewIntArray(len);
    jint *elems = env->GetIntArrayElements(jint_arr, NULL);
    int i = 0;
    for (; i < len; i++) {
        elems[i] = i;
    }

    //同步
    env->ReleaseIntArrayElements(jint_arr, elems, 0);

    return jint_arr;
}

3 JNI 调用Java属性和方法

3.1 访问类、对象和方法

初始化了Java虚拟机后,native就可以调用Java的方法,要调用一个Java对象的方法必须经过几个步骤:

3.1.1 获取指定对象的类定义(jclass)

有两种方式来获取对象的类定义:

第一种是在已知类名的情况下使用FindClass来查找对应的类。但是要注意类名并不同于平时写的Java代码,例如要得到类jni.test.Demo的定义必须调用如下代码:

jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠

第二种是通过对象直接得到其所对应的类定义:

jclass cls = (*env)-> GetObjectClass(env, obj); //其中obj是要引用的对象,类型是jobject
//创建Date对象
jclass cls = env->FindClass("java/util/Date");
jmethodID constructor_mid = env->GetMethodID(cls, "", "()V");
//新建对象/变量
jobject obj = env->NewObject(cls, constructor_mid);

3.1.2 读取要调用方法的定义

我们先来看看JNI中获取方法定义的函数:

jmethodID (JNICALL *GetMethodID)(JNIEnv *env, jclass clazz, const char *name,  
const char *sig); 

jmethodID (JNICALL *GetStaticMethodID)(JNIEnv *env, jclass class, const char  
*name, const char *sig);

这两个函数的区别明显都能猜到,GetStaticMethodID 是用来获取静态方法的定义,而GetMethodID 则是获取非静态的方法定义。

这两个函数都需要提供四个参数:

  • 第一个参数env 就是初始化虚拟机得到的jni环境;
  • 第二个参数class 是对象的类定义,也就是第一步得到的obj;
  • 第三个参数是方法名称;
  • 第四个参数是方法签名。

我们知道Java是有重载方法的,可以定义方法名相同,但参数不同的方法,正因为如此,在JNI中仅仅通过方法名是无法找到 Java中的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI的方法签名的格式为:

(参数签名格式...)返回值签名格式

如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,Java提供了javap命令来自动生成方法签名。

rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> javac JniTest.java           
rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> ls                           
JniTest.class  JniTest.java  MainActivity.java
rentianxin@rentianxin-Desk ~/A/R/C/a/s/m/j/c/r/cmakedemo> javap -s -p JniTest.class     
Compiled from "JniTest.java"
public class com.richy.cmakedemo.JniTest {
  public com.richy.cmakedemo.JniTest();
    descriptor: ()V

  public static native java.lang.String getStringFromC();
    descriptor: ()Ljava/lang/String;

  public native java.lang.String getString2FromC(int);
    descriptor: (I)Ljava/lang/String;

  private native java.lang.String getLine(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;

  public native void giveArray(int[]);
    descriptor: ([I)V

  public native int[] getArray(int);
    descriptor: (I)[I

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V

  static {};
    descriptor: ()V
}

3.1.3 调用方法

获取到方法的定义jmethodID后,就可以调用方法了。为了调用对象的某个方法,可以使用函数

CallMethod 或者 CallStaticMethod(访问类的静态方法)

根据不同的返回类型而定。这些方法都是使用可变参数的定义,如果访问某个方法需要参数时,只需要把所有参数按照顺序填写到方法中就可以。在讲到构造函数的访问时,将演示如何访问带参数的构造函数。


3.3 访问类属性

3.3.1 获取指定对象的类(jclass)

这一步,与访问类方法完全一样。

jclass cls = (*env)->FindClass(env, "jni/test/Demo"); //把点号换成斜杠

3.3.2 读取类属性的定义(jfieldID)

在JNI中是这样定义获取类属性的方法的:

jfieldID (JNICALL *GetFieldID)
(JNIEnv *env, jclass clazz, const char *name, const char *sig); 


jfieldID (JNICALL *GetStaticFieldID) 
(JNIEnv *env, jclass clazz, const char *name, const char *sig); 

这两个函数中第一个参数为JNI环境;clazz为类的定义;name为属性名称;第四个参数同样是为了表达属性的类型。前面我们使用javap工具获取类的详细定义的时候有这样两行:

public java.lang.String key;
/* Ljava/lang/String; */

其中第二行注释的内容就是第四个参数要填的信息,这跟访问类方法时是相同的。


3.3.3 读取对象的属性和设置属性值

获取到属性的定义fieldID后,就可以访问属性值了。有几个方法用来读取和设置类的属性,它们是:

GetField、 SetField、GetStaticField、 SetStaticField

JNIEXPORT jstring JNICALL
Java_com_richy_cmakedemo_JniTest_accessField(JNIEnv *env, jobject instance) {
    //jobj是t对象,JniTest.class
    jclass cls = env->GetObjectClass(instance);
    //jfieldID
    //属性名称,属性签名
    jfieldID fid = env->GetFieldID(cls, "key", "Ljava/lang/String;");

    //richy >> super richy
    //获取key属性的值
    //GetField
    jstring jstr = static_cast(env->GetObjectField(instance, fid));
    printf("jstr:%#x\n",&jstr);

    //jstring -> c字符串
    //isCopy 是否复制(true代表赋值,false不复制)
    char *c_str = const_cast(env->GetStringUTFChars(jstr, JNI_FALSE));
    //拼接得到新的字符串
    char text[20] = "super ";
    strcat(text,c_str);

    //c字符串 ->jstring
    jstring new_jstr = env->NewStringUTF(text);

    //修改key
    //SetField
    env->SetObjectField(instance, fid, new_jstr);

    printf("new_jstr:%#x\n", &new_jstr);

    return new_jstr;
}

4 内存管理

对于Java程序员来说,内存管理是完全透明的,Java虚拟机会处理。然而从Java虚拟机创建的对象传到C/C++代码时会产生引用,根据Java的垃圾回收机制,只要有引用存在就不会触发该引用所指向Java对象的垃圾回收。

这些引用在 JNI 中分为3种:全局引用(Global Reference)、局部引用 (Local Reference)、弱全局引用 (Week Global Reference- since JDK1.2)。

4.1 三种引用的区别

4.1.1 全局引用

全局引用可以跨方法、跨线程使用,直到被开发者显式释放。类似局部引用,一个全局引用在被释放前保证引用对象不被GC回收。和局部引用不同的是,没有那么多函数能够创建全局引用。能创建全局引用的函数只有 NewGlobalRef。以下例子说明了如何使用一个全局引用。

extern "C"
JNIEXPORT void JNICALL
Java_com_richy_cmakedemo_JniTest_createGlobalRef(JNIEnv *env, jobject instance) {
    jstring obj = env->NewStringUTF("jni development is powerful!");
    global_str = static_cast(env->NewGlobalRef(obj));
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_richy_cmakedemo_JniTest_getGlobalRef(JNIEnv *env, jobject instance) {
    return global_str;
}

extern "C"
JNIEXPORT void JNICALL
Java_com_richy_cmakedemo_JniTest_deleteGlobalRef(JNIEnv *env, jobject instance) {
    env->DeleteGlobalRef(global_str);
}

/*内存引用研究---------------end--------------*/

4.1.2 局部引用

一个局部引用仅在创建它的native函数及该函数调用的函数中有效。在一个native函数执行期间创建的所有局部引用将在该函数返回时被释放

示例创建了大量的局部引用,占用了太多的内存,而且这些局部引用跟后面的操作没有关联性,就可以提前释放。

JNIEXPORT void JNICALL
Java_com_richy_cmakedemo_JniTest_localRef(JNIEnv *env, jobject instance) {
    int i = 0;
    for (; i < 5; i++){
        //创建Date对象
        jclass cls = env->FindClass("java/util/Date");
        jmethodID constructor_mid = env->GetMethodID(cls, "", "()V");
        //新建对象
        jobject obj = env->NewObject(cls, constructor_mid);

        //...
        //不在使用jobject对象了
        //通知垃圾回收器回收这些对象
        //释放局部引用
        env->DeleteLocalRef(obj);
        //...
    }
}

4.1.3 弱全局引用

为了节省内存,在内存不足时可以是释放所引用的对象,可以引用一个不常用的对象,如果为NULL,临时创建,弱全局引用使用NewGlobalWeakRef创建,使用DeleteGlobalWeakRef释放。下面简称弱引用。

与全局引用类似,弱引用可以跨方法、线程使用。但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象,所以在使用时需要多加小心,它所引用的对象可能是不存在的或者已经被回收

1.创建弱全局引用

用NewWeakGlobalRef函数对弱全局引用进行初始化,例如:

extern "C"
JNIEXPORT void JNICALL
Java_com_richy_cmakedemo_JniTest_weakGlobalRef(JNIEnv *env, jobject instance) {
    weakGlobalcls = static_cast(env->NewWeakGlobalRef(instance));

    //...
    if (JNI_FALSE == env->IsSameObject(weakGlobalcls, NULL)) {
        //TODO 对象未被回收,可以使用
    } else {
        //TODO 对象被垃圾回收器回收,不能使用,根据业务需求判断是否要重新新建
    }
    //....
}

2.引用的比较

给定两个引用(不管是全局、局部还是弱全局引用),我们只需要调用IsSameObject来判断它们两个是否指向相同的对象。例如:(env)->IsSameObject(env, obj1, obj2)
如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。

有一个特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null对象。如果obj是一个局部或全局引用,使用(*env)->IsSameObject(env, obj, NULL) 或者 obj == NULL 来判断obj是否指向一个null对象即可。但需要注意的是,IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用的。比如:

if(JNI_FALSE == (*env)->IsSameObject(env,weakGlobalcls,NULL)){
//TODO 对象未被回收,可以使用
}else{
//TODO 对象被垃圾回收器回收,不能使用,根据业务需求判断是否要重新新建
}

ref:

Android Studio开发JNI示例

Android NDK 开发(二)JNI 传递参数和返回值

Android NDK 开发(三)JNI 调用Java属性和方法

Android NDK 开发(四)JNI 中局部引用、全局引用和弱全局引用

Android深入理解JNI(二)类型转换、方法签名和JNIEnv

你可能感兴趣的:(JNI 入门)