前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JNI的探索

JNI的探索

作者头像
包子388321
发布2020-06-16 18:47:23
9460
发布2020-06-16 18:47:23
举报
文章被收录于专栏:包子的书架

JNI的概念

  • 定义 JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植
  • 原理
 Jni原理图
Jni原理图

 java平台
java平台
  • 开发工具 1、vs2015 2、eclipse(或者Android studio) 3、java环境

JNI的调用过程

  • 步骤: 1.编写带有native声明的方法的java类 2.编译生成class文件 3.利用javah生成(.h)的头文件 命令:javah 类名, 注:不需要class后缀 4.将(.h)头文件复制到vs下,创建(.cpp)或者(.c)文件实现(.h)头文件声明的方法 5.实现完成后,编译成dll库 6.将dll复制到java项目的根目录,调用System.loadLibrary("dll库名"); //注:不要dll后缀 7.在代码里面调用native方法,访问native(.cpp 或者 .c)的代码
  • 栗子: 1、java上的native定义
代码语言:javascript
复制
//定义
public native static String getStringFromCPP();
//调用
public static void main(String[] args) {
    System.out.println(getStringFromCPP());
}

2、利用javac或者编译器直接编译,生成class文件 3、利用jdk下的javah 生成(.h)的头文件

生成(.h)文件
生成(.h)文件

4、将生成的头文件放置到vs新建的项目,如下; 还需要将JDK目录下的include 目录下的jni.h 和 jni_md.h文件copy到项目 因为生成的JniMain.h文件需要依赖到这两个文件,同时将JniMain.h中的 #include <jni.h> 改成 #include "jni.h"

JNI的vs项目结构
JNI的vs项目结构

5、创建C++或者C文件实现JniMain的方法, 这边创建JniDemo.cpp, 引入头文件 具体代码:

代码语言:javascript
复制
#include "stdafx.h"
#include "JniMain.h"
#include <string.h>

/*
* Class:     JniMain
* Method:    getStringFromCPP
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
(JNIEnv *env, jclass jclaz) {
return env->NewStringUTF("java static method call C++ back string");
}

6、打包成dll 这边vs项目创建的是win32的项目,所以需要配置成dll 在项目右键 ->属性

配置类型改为Dll
配置类型改为Dll

由于个人的环境是64位的,所以配置管理,需要修改为x64位

配置管理
配置管理
配置管理器调整平台为x64
配置管理器调整平台为x64

生成dll

生成dll
生成dll

7、将dll 复制到java项目工程的根目录,并加载dll库, 运营程序

代码语言:javascript
复制
public class JniMain {

    //静态方法
    public native static String getStringFromCPP();

    static{
        System.loadLibrary("Jni");
    }

    public static void main(String[] args) {
        System.out.println(getStringFromCPP());
    }
}

结果:

结果
结果

调用的分析

JNI的数据类型
  • JNI基本数据类型:

java

C/C++

boolean

jboolean

byte

jbyte

char

jchar

short

jshort

int

jint

long

jlong

float

jfloat

double

jdouble

  • 引用类型:

java

C/C++

String

jstring

Object

jobject

  • 基本数据类型数组: //type[] jTypeArray; byte[] jByteArray;
  • 引用类型数组 Object jobjectArray;
JNI对应的java属性与方法签名

在jni调用中,返回值和参数,以及静态字段和实例字段,有对应着相应的签名,如下表格: 这些签名的时候在接下的实例讲解中会用到; 简而言之,在jni中涉及到类型的使用(包括基本类和引用类型)

Java属性与方法签名列表
Java属性与方法签名列表

方法签名例子: 方法1:

代码语言:javascript
复制
public string addTail(String tail, int index)

其对应的签名如下:

代码语言:javascript
复制
(Ljava/util/String;I)Ljava/util/String;

方法2:

代码语言:javascript
复制
public int addValue(int index, String value,int[] arr)

其对应的签名如下:

代码语言:javascript
复制
(ILjava/util/String;[I)I
  • javap命令查看class文件中对应jni的签名 命令:javap -s -p class文件的路径
 javap命令
javap命令

native修饰的静态方法
  • java代码:
代码语言:javascript
复制
public native static String getStringFromCPP();
  • C++ 代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    getStringFromCPP
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
(JNIEnv *env, jclass jclaz) {
    return env->NewStringUTF("java static method call C++ back string");
}
  • 说明: java中定义native方法是静态的话,生成的接口方法的参数就是(JNIEnv *env, jclass jclaz) JNIEnv: 是jni接口调用的api指针 jclass: 表示的就是native修饰的java静态方法所在的类
native修饰的非静态方法
  • java代码:
代码语言:javascript
复制
public native String getStringFromCPP2();
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    getStringFromCPP2
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP2
(JNIEnv *env, jobject job) {
    return env->NewStringUTF("java non-static method call C++ back string ");
}
  • 说明: java中定义native方法是非静态的话,生成的接口方法的参数是(JNIEnv *env, jobject job) JNIEnv: 是jni接口调用的api指针 jobject: 表示的就是native修饰的java非静态方法所在类的对象
访问java类中的成员变量
  • java代码:
代码语言:javascript
复制
public String key = "key";
public native void accessField(); //该native方法用于调用c++的接口访问java变量
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    accessField
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_accessField
(JNIEnv *env, jobject job) {
    //先获取对应的java类
    jclass jclaz = env->GetObjectClass(job);

    //第二个参数对应的是java的变量名,第三个是类型的签名
    jfieldID fid = env->GetFieldID(jclaz, "key", "Ljava/lang/String;");

    char text[100] = "Jni change string field value ";

    if (fid == NULL) { // 如果字段为 NULL ,直接退出,查找失败
        return;
    }

    // 获取字段对应的值
    jstring jstr = (jstring)env->GetObjectField(job, fid); 
    const char * str = env->GetStringUTFChars(jstr, NULL);

    strcat(text, str);

    jstring key = env->NewStringUTF(text);
    //修改key的值
    env->SetObjectField(job, fid, key);

}
  • 说明:类似java的反射,步骤如下: 1、获取 Java 对象的类 2、获取对应字段的 id 3、获取具体的字段值
访问java类中的静态变量
  • java代码:
代码语言:javascript
复制
public static int count = 9;
public native void accessStaticField();  //该native方法用于调用c++的接口访问java静态变量
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    accessStaticField
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_accessStaticField
(JNIEnv *env, jobject job) {
    //获取类名
    jclass jclaz = env->GetObjectClass(job);

    //获取静态字段  第一个参数是类,第二个参数对应的是java的变量名,第三个是类型的签名
    jfieldID fid = env->GetStaticFieldID(jclaz, "count", "I");

    if (fid == NULL) {
        return;
    }

    // 获取字段对应的值
    jint count = env->GetStaticIntField(jclaz, fid);

    count = 20;
    // 修改字段的值
    env->SetStaticIntField(jclaz, fid, count);
}
  • 说明:静态字段的访问类似实例字段的访问,步骤相同
jni中调用java某个对象的方法
  • java代码:
代码语言:javascript
复制
public class Animal {

    protected String name;
    public static int num = 0;
    public Animal(String name) {
        this.name = name;
    }
    //jni访问的非静态方法
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    public int getNum() {
        return num;
    }

    public static String getUID(String id) {
        return "10001"+id;
    }
}
//另一个类的native方法
public native void callInstanceMethod(Animal animal);
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    callInstanceMethod
* Signature: (LAnimal;)V
*/
JNIEXPORT void JNICALL Java_JniMain_callInstanceMethod
(JNIEnv *env, jobject instance, jobject animal) {

    // 获得具体的类
    jclass cls = env->GetObjectClass(animal);
    // 获得具体的方法 id
    jmethodID mid = env->GetMethodID(cls, "setName", "(Ljava/lang/String;)V");

    if (mid == NULL) {
        return;
    }
    //设置要穿的参数
    jstring name = env->NewStringUTF("baozi");
    //调用java的方法
    env->CallVoidMethod(animal, mid, name);
}
  • 说明: 与访问字段不同的是,GetFieldID 方法换成了 GetMethodID 方法,另外由 CallVoidMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。方法签名 = (参数类型额签名) + 返回值类型的签名 GetMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。 对于不需要返回值的函数,调用 CallVoidMethod 即可,对于返回值为引用类型的,调用 CallObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallBooleanMethod、CallShortMethod、CallDoubleMethod 等等
jni中调用java类的某个静态方法
  • java代码:
代码语言:javascript
复制
public class Animal {

    protected String name;
    public static int num = 0;
    public Animal(String name) {
        this.name = name;
    }
    //jni访问的非静态方法
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    public int getNum() {
        return num;
    }
    //jni访问的静态方法
    public static String getUID(String id) {
        return "10001"+id;
    }
}
//另一个类的native方法
public native String callStaticMethod(Animal animal);
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    callStaticMethod
* Signature: (LAnimal;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_callStaticMethod
(JNIEnv *env, jobject instance, jobject animal) {

    //获取具体的类
    jclass jclz = env->FindClass("Animal");//参数也累的路径名

    // 获取具体的静态方法的 id
    jmethodID mid = env->GetStaticMethodID(jclz, "getUID", "(Ljava/lang/String;)Ljava/lang/String;");
    if (mid == NULL) {
        return env->NewStringUTF("method no found!");
    }
    jstring id = env->NewStringUTF("xxxxxx");
    jstring result = (jstring)env->CallStaticObjectMethod(jclz, mid, id);
    return result;
}
  • 说明: 与访问非静态不同的是,GetMethodID 方法换成了 GetStaticMethodID 方法,另外由 CallStaticObjectMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。 GetStaticMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。对于不需要返回值的函数,调用 CallStaticVoidMethod 即可,对于返回值为引用类型的,调用 CallStaticObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallStaticBooleanMethod、CallStaticShortMethod、CallStaticDoubleMethod 等等
调用java类的构造方法
  • java代码:
代码语言:javascript
复制
//访问构造方法
public native Date acceessConstructor();
  • C++ 代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    acceessConstructor
* Signature: (LAnimal;)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL Java_JniMain_acceessConstructor
(JNIEnv *env, jobject job) {
    //通过类的路径来从JVM里面找到对应的类
    jclass jclz = env->FindClass("java/util/Date");
    //jmethodId  构造方法
    jmethodID jmid = env->GetMethodID(jclz, "<init>","()V");

    if (jmid == NULL) {
        return NULL;
    }

    // 调用newObject 实例化Date 对象,返回值是一个jobject
    jobject date_obj = env->NewObject(jclz, jmid);

    // 得到对应的对象方法,前提是,我们访问了相关对象的构造函数创建了这个对象
    jmethodID time_mid = env->GetMethodID(jclz, "getTime","()J");
    jlong time = env->CallLongMethod(date_obj, time_mid);

    printf("time: %lld \n", time);
    return date_obj;
}
  • 说明 调用java类的构造方法,等于在C++里面创建一个java对象,然后进行调用;同样也是采用GetMethodID的方法进行获取构造函数的id,然后由NewObject 进行对象的创建
JNI数组的使用
  • java代码:
代码语言:javascript
复制
//整型数据在C++中进行排序
public native void giveArray(int[] inArray);
public native int[][] initInt2DArray(int size);
public native String[] initStringArray(int size);
//调用
int[] array = {3,9,2,50,6,13};
jniMain.giveArray(array);
for(int i=0; i< array.length; i++) {
    System.out.println(array[i]);
}

String[] strArr = jniMain.initStringArray(5);
for (int i = 0; i < strArr.length; i++) {
    System.out.println("strArr["+i+"] = "+strArr[i]);
}

int[][] intArr = jniMain.initInt2DArray(4);
for(int i =0; i < 4; i++) {
    for(int j = 0; j < 3; j ++) {
        System.out.println("arr["+i+"]["+j+"] = "+intArr[i][j]);
    }
}
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    giveArray
* Signature: ([I)V
*/
JNIEXPORT void JNICALL Java_JniMain_giveArray
(JNIEnv *env, jobject job, jintArray array) {
    //jintArray -> jint *
    jint *elemts = env->GetIntArrayElements(array, NULL);
    if (elemts == NULL)
    {
        return;
    }
    //数组长度
    int len = env->GetArrayLength(array);
    qsort(elemts, len, sizeof(jint), compare);
    //释放可能的内存
    //将JNI  修改的数据重新写回原来的内存
    env->ReleaseIntArrayElements(array, elemts, JNI_COMMIT);
}

/*
* Class:     JniMain
* Method:    initInt2DArray
* Signature: (I)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_JniMain_initStringArray
(JNIEnv *env, jobject job, jint size) {
    //创建jobjectArray对象
    jobjectArray result;
    jclass jclz;
    int i;
    jclz = env->FindClass("java/lang/String");
    if (jclz == NULL) {
        return NULL;
    }
    result = env->NewObjectArray(size,jclz, job);
    if (result == NULL) {
    return NULL;
    }
    //赋值
    for ( i = 0; i < size; i++)
    {
        char * c_str = (char *)malloc(256);
        memset(c_str, 0, 256);
        //将 int 转换成为 char
        sprintf(c_str, "hello num: %d\n",i);
        // C -> jstring
        jstring str = env->NewStringUTF(c_str);
        if (str == NULL) {
            return NULL;
        }

        // 将jstring 赋值给数组
        env->SetObjectArrayElement(result, i, str);
        free(c_str);
        c_str = NULL;
    }

    return result;
}

/*
* Class:     JniMain
* Method:    initInt2DArray
* Signature: (I)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_JniMain_initInt2DArray
(JNIEnv *env, jobject job, jint size) {
    //返回对象,是个二维数组
    jobjectArray ret;
    int i = 0;
    int j = 0;
    jclass intArrayClz = env->FindClass("[I");
    if (intArrayClz == NULL) {
       return NULL;
    }

    ret = env->NewObjectArray(size * 3, intArrayClz, NULL);

    jint tmp[3];//固定数组
    for ( i = 0; i < size; i++)
    {
        jintArray intArr = env->NewIntArray(3);
        for ( j = 0; j < 3; j++)
        {
            tmp[j] = i + j;
        }

        env->SetIntArrayRegion(intArr, 0, 3, tmp);
        //将一维数组的值复制到
        env->SetObjectArrayElement(ret, i, intArr);
        env->DeleteLocalRef(intArr);
    }
    return ret;
}
  • 说明: jobjectArray 表示二维数组 env->SetObjectArrayElement(ret, i, intArr); 二维数组的赋值 env->NewObjectArray(size * 3, intArrayClz, NULL); 二维数组的创建
处理中文字符串的乱码问题

由于java的字符串编码,和C或者C++的字符串编码不一样,所以在java传中文到C/C++会出现乱码的现象

字符编码切换
字符编码切换

解决方法,就是在C/C++直接调用java的String来处理

  • java代码:
代码语言:javascript
复制
//定义
public native String chineseChars2(String str);

//调用
System.out.println(jniMain.chineseChars2("宝宝22"));
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    chineseChars2
* Signature: (LAnimal;)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL Java_JniMain_chineseChars2
(JNIEnv *env, jobject job, jstring in) {

     char * c_str = "马蓉与宝宝";
    //创建String的类
    jclass jclz = env->FindClass("java/lang/String");
    //获取构造函数的mid   这边使用的是java的String(byte[], string)的构造函数
    jmethodID mid = env->GetMethodID(jclz, "<init>", "([BLjava/lang/String;)V");

    //创建参数 jstring -> jbyteArray
    jbyteArray bytes = env->NewByteArray(strlen(c_str));

    // char * 赋值到byte数组中
    env->SetByteArrayRegion(bytes, 0, strlen(c_str), (jbyte*)c_str);
    // 设置编码
    jstring charsetName = env->NewStringUTF("GB2312");

    return env->NewObject(jclz, mid, bytes, charsetName);
}
JNI中 局部引用
  • java代码:
代码语言:javascript
复制
public native void localRef();
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    localRef   局部引用
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_localRef
(JNIEnv *env, jobject job) {
    int i;
    for ( i = 0; i < 5; i++)
    {
        jclass clz = env->FindClass("java/util/Date");
        jmethodID mid = env->GetMethodID(clz, "<init>", "()V");
        //创建对象
        jobject date_obj = env->NewObject(clz, mid);
        //使用这个引用
        jmethodID time_mid = env->GetMethodID(clz, "getTime", "()J");
        jlong time = env->CallLongMethod(date_obj, time_mid);

        printf("local reference time: %lld \n", time);

        //释放引用
        env->DeleteLocalRef(clz);
        env->DeleteLocalRef(date_obj);
    }
}
  • 说明: 局部引用通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放
JNI中 全局引用
  • java代码:
代码语言:javascript
复制
public native void createGlobalRef();
public native String getglobalRef();
public native void delGlobalRef();
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    createGlobalRef  创建全局引用
* Signature: ()V
*/
jstring global_str;
JNIEXPORT void JNICALL Java_JniMain_createGlobalRef
(JNIEnv *env, jobject job) {
    jstring str = env->NewStringUTF("JNI is intersting");
    global_str = (jstring)env->NewGlobalRef(str);
}
//全局引用
//跨线程,跨方法使用
// NewGlobalRef 是创建全局引用的唯一方法

/*
* Class:     JniMain
* Method:    getglobalRef
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getglobalRef
(JNIEnv *env, jobject job) {
    return global_str;
}

/*
* Class:     JniMain
* Method:    delGlobalRef  移除全局引用
* Signature: ()V;
*/
JNIEXPORT void JNICALL Java_JniMain_delGlobalRef
(JNIEnv *env, jobject job) {
    env->DeleteGlobalRef(global_str);
}
  • 说明 全局引用,变量是定义在方法外,调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放env->DeleteGlobalRef(g_cls_string);
JNI中 弱全局引用
  • java代码:
代码语言:javascript
复制
public native String createWeakRef();
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    createWeakRef
* Signature: ()Ljava/lang/String;
*/
jstring g_weak_cls;
JNIEXPORT jstring JNICALL Java_JniMain_createWeakRef
(JNIEnv *env, jobject job) {
    jclass cls_string = env->FindClass("java/lang/String");
    g_weak_cls = (jstring)env->NewWeakGlobalRef(cls_string);
    g_weak_cls = env->NewStringUTF("Jni weak reference");
    printf("weak ref = %s \n ",g_weak_cls);
    return g_weak_cls;
}
  • 说明: 弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。env->DeleteWeakGlobalRef(g_cls_string)
JNI中 异常处理
  • java代码:
代码语言:javascript
复制
public native void exception();
  • C++代码:
代码语言:javascript
复制
/*
* Class:     JniMain
* Method:    exception   异常处理
* Signature: ()V;
*/
JNIEXPORT void JNICALL Java_JniMain_exception
(JNIEnv *env, jobject job) {
    jclass cls = env->GetObjectClass(job);
    jfieldID fid = env->GetFieldID(cls, "key11", "Ljava/lang/String;");

    //检查是否发送异常
    jthrowable ex = env->ExceptionOccurred();
    // 判断异常是否发送
    if (ex != NULL) {
        jclass newExc;
        //清空JNI 产生的异常
        env->ExceptionClear();
        //NullPointerException
        newExc = env->FindClass("java/lang/IllegalArgumentException");
    if (newExc == NULL)
    {
        printf("exception\n");
        return;
    }
    env->ThrowNew(newExc, "Throw exception from JNI: GetFieldID faild ");
    }
}
  • 说明: native调用java中的方法,java中的方法抛出异常,我们在native中检测异常,检测到后抛出native中的异常,并清理异常。 函数介绍: 1> ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE 2> ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL 3> ExceptionDescribe:打印异常的堆栈信息 4> ExceptionClear:清除异常堆栈信息 5> ThrowNew:在当前线程触发一个异常,并自定义输出异常信息 6> Throw:丢弃一个现有的异常对象,在当前线程触发一个新的异常 7> FatalError:致命异常,用于输出一个异常信息,并终止当前VM实例(即退出程序)

jni的静态注册和动态注册

参考:https://blog.csdn.net/qq_20404903/article/details/80662316

结语

以上就是个人对jni的认知和总结,如有错误的地方,欢迎大家指出,该篇文件比较适合于对JNI的入门的同学 项目代码:https://github.com/jasonkevin88/JniDemo

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • JNI的概念
  • JNI的调用过程
  • 调用的分析
    • jni的静态注册和动态注册
      • 结语
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档