概述
接上一篇android中的jni
上一篇我们看android源码中的jni用法,现在继续。
1. 介绍
1.1 what
Java代码 里调用 C、C++等语言的代码 或 C、C++代码调用 Java 代码
- JNI是 Java 调用 Native 语言的一种特性
- JNI 是属于 Java 的,与 Android 无直接关系
1.2 why
- 背景:实际使用中,Java 需要与 本地代码 进行交互
- 问题:因为 Java 具备跨平台的特点,所以Java 与 本地代码交互的能力非常弱
- 解决方案: 采用 JNI特性 增强 Java 与 本地代码交互的能力
1.3 how
- java中声明native方法
- 编译java源码文件得到.class文件
- javah命令导出jni头文件,.h文件
- 使用java需要交互的本地代码实现java中声明的Native方法
- 如有需要,c++中也可实现java的navive方法
1.4 ndk
Native Development Kit,是 Android的一个工具开发包,属于android,与java无关。
- 作用:快速开发c c++动态库,自动将so打包成apk
1.5 两种注册方法
- 静态注册
public class JniDemo1{
static {
System.loadLibrary("samplelib_jni");
}
private native void nativeMethod();
}
//返回值 + Java前缀+全路径类名+方法名+参数1JNIEnv+参数2jobject+其他参数
-
动态注册
System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个
JNI_OnLoad
函数兵调用该函数,这个函数的作用是告诉Dalvik虚拟机此C库使用的是哪一个JNI版本,VM会默认该库使用最老的JNI 1.1版本。最新版本的JNI做了很多扩充,也优化了一些内容,必须在JNI_OnLoad()函数声明JNI的版本。同时也可以在该函数中做一些初始化的动作。该函数前面也有三个关键字分别是JNIEXPORT
,JNICALL
,jint
。其中JNIEXPORT
和JNICALL
是两个宏定义,用于指定该函数时JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层。
public class JniDemo1{
static {
System.loadLibrary("samplelib_jni");
}
}
#include <jni.h>
#include "Log4Android.h"
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#ifdef __cplusplus
extern "C" {
#endif
static const char *className = "com/gebilaolitou/jnidemo/JNIDemo2";
static void sayHello(JNIEnv *env, jobject, jlong handle) {
LOGI("JNI", "native: say hello ###");
}
//方法签名 格式 (参数1类型标示;参数2类型标示;参数3类型标示...)返回值类型标示
static JNINativeMethod gJni_Methods_table[] = {
{"sayHello", "(J)V", (void*)sayHello},
};
static int jniRegisterNativeMethods(JNIEnv* env, const char* className,
const JNINativeMethod* gMethods, int numMethods)
{
jclass clazz;
LOGI("JNI","Registering %s nativesn", className);
clazz = (env)->FindClass( className);
if (clazz == NULL) {
LOGE("JNI","Native registration unable to find class '%s'n", className);
return -1;
}
int result = 0;
if ((env)->RegisterNatives(clazz, gJni_Methods_table, numMethods) < 0) {
LOGE("JNI","RegisterNatives failed for '%s'n", className);
result = -1;
}
(env)->DeleteLocalRef(clazz);
return result;
}
//这个函数里面动态的注册native方法
jint JNI_OnLoad(JavaVM* vm, void* reserved){
LOGI("JNI", "enter jni_onload");
JNIEnv* env = NULL;
jint result = -1;
//获取JNIEnv结构体指针
if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return result;
}
jniRegisterNativeMethods(env, className, gJni_Methods_table, sizeof(gJni_Methods_table) / sizeof(JNINativeMethod));
return JNI_VERSION_1_4;
}
#ifdef __cplusplus
}
#endif
1.6 动态注册流程
//1)声明 Java 层 Native 方法 : 在 Java 类中声明 native 方法
/**
* 动态注册 JNI 方法 , Java 方法
*/
public native void dynamicRegisterJavaMethod();
public native int dynamicRegisterJavaMethod2(int i);
// 2)准备数据 JNINativeMethod methods[] 数组
/*
该数组中 , 每个元素都是一个 JNI 的 Native 方法
JNINativeMethod 是结构体
typedef struct {
const char* name; //Java 中定义的 Native 方法名 , 注意这是一个 C 字符串
const char* signature; //函数签名 , 可以使用 javap 生成
void* fnPtr; //C/C++ 中的 Native 函数签名
} JNINativeMethod;
*/
static const JNINativeMethod methods[] = {
{"dynamicRegisterJavaMethod", "()V", (void *)dynamicRegisterCMethod},
{"dynamicRegisterJavaMethod2", "(I)I", (void *)dynamicRegisterCMethod2}
};
//3)编写JNI_Onload方法
int JNI_OnLoad(JavaVM *vm , void *r){
return JNI_VERSION_1_6;
}
//4)获取JENEnv指针,调用JavaVM 结构体的 GetEnv 方法 , 获取 JNIEnv 指针
//1 . 获取 JNIEnv JNI 环境 , 需要从 JavaVM 获取
JNIEnv *env = nullptr;
//2 . 调用 JavaVM / _JavaVM 结构体的 jint GetEnv(void** env, jint version) 方法
// 返回值分析 : 动态注册会返回一个结果
// 如果 registerResult < 0 , 则动态注册失败
// 如果 registerResult == 0 , 则动态注册失败
int registerResult = vm->GetEnv( (void **) &env, JNI_VERSION_1_6 );
//5) 获取java类,调用JNIEnv结构体的FindClass方法获取jclass对象
/*
动态注册的 Java 类名称
注意 : 包名类名之间使用 "/" 分割
*/
static const char* className = "kim/hsl/onload/MainActivity";
//获取要动态注册的 Java 类的 Class 对象
jclass jclazz = env->FindClass(className);
//6)进行动态注册,调用JNIEnv的RegisterNatives方法,进行正式注册
/*
5 .正式注册
注册方法解析 :
jint RegisterNatives(
jclass clazz, //要注册的 Java 类
const JNINativeMethod* methods, //JNI注册方法数组
jint nMethods //要注册的 JNI 方法个数
)
sizeof(methods) / sizeof(JNINativeMethod) : 计算 JNINativeMethod methods[] 数组大小
*/
env->RegisterNatives(jclazz, methods, sizeof(methods) / sizeof(JNINativeMethod));
2 案例
2.1基本数据类型传递
java 基本数据类型boolean byte char short int long float double
C/C++编写native代码时不能直接使用java,
jni提供了 jbooan jbyte jchar jshot jint jlong jfloat jdouble
2.2 引用数据类型
jni包含大量的引用类型 jObject jstring jclass jbooleanArray jbyteArray jcharArray jshortArray
2.2.1 数组
// native->java
extern "C"
JNIEXPORT jbooleanArray JNICALL
Java_com_ihubin_ndkjni_NativeUtil_getNativeArray(JNIEnv *env, jclass clazz) {
jboolean* jb = new jboolean[5];
jb[0] = JNI_TRUE;
jb[1] = JNI_FALSE;
jb[2] = JNI_TRUE;
jb[3] = JNI_FALSE;
jb[4] = JNI_TRUE;
jbooleanArray jba = env->NewBooleanArray(5);
env->SetBooleanArrayRegion(jba, 0, 5, jb);
return jba;
}
//java->native
Java_com_ihubin_ndkjni_NativeUtil_formatArray(JNIEnv *env, jclass clazz, jintArray int_array) {
jint array[5];
env->GetIntArrayRegion(int_array, 0, 5, array);
jsize size = env->GetArrayLength(int_array);
char resutStr[100] = {0};
char str[10] = {0};
strcat(resutStr, "[");
for(int i = 0; i < size; i++) {
sprintf(str, "%d", array[i]);
strcat(resutStr, str);
if(i != size - 1) {
strcat(resutStr, ", ");
}
}
strcat(resutStr, "]");
return env->NewStringUTF(resutStr);
}
2.2.2 对象
extern "C"
JNIEXPORT jobject JNICALL
Java_com_ihubin_ndkjni_NativeUtil_javaClassTest(JNIEnv *env, jclass clazz) {
jclass userClass = env->FindClass("com/ihubin/ndkjni/User");
//参数,返回值,jni签名为了支持函数重载 格式: (参数1类型标示;参数2类型标示;参数3类型标示...)返回值类型标示
jfieldID normalField = env->GetFieldID(userClass, "normalField", "I");
jfieldID staticField = env->GetStaticFieldID(userClass, "staticField", "I");
jmethodID normalMethod = env->GetMethodID(userClass, "getNormalUserInfo", "()Ljava/lang/String;");
jmethodID staticMethod = env->GetStaticMethodID(userClass, "getStaticUserInfo", "()Ljava/lang/String;");
jmethodID voidInitMethod = env->GetMethodID(userClass, "<init>", "()V");
jmethodID paramInitMethod = env->GetMethodID(userClass, "<init>", "(Ljava/lang/String;I)V");
jmethodID getFormatInfoMethod = env->GetMethodID(userClass, "getFormatInfo", "()Ljava/lang/String;");
jobject userOne = env->NewObject(userClass, voidInitMethod);
jstring name = env->NewStringUTF("HUBIN");
jint age = 8;
jobject userTwo = env->NewObject(userClass, paramInitMethod, name, age);
//通过一个类创建对象,默认构造函数
jobject userThree = env->AllocObject(userClass);
jint normalFieldValue = env->GetIntField(userOne, normalField);
LOGD("normalField: %d", normalFieldValue);
jint staticFieldValue = env->GetStaticIntField(userClass, staticField);
LOGD("staticField: %d", staticFieldValue);
//调用对象中的方法
jobject normalMethodResultObj = env->CallObjectMethod(userOne, normalMethod);
jstring normalMethodResult = static_cast<jstring>(normalMethodResultObj);
const char *normalMethodResultNativeString = env->GetStringUTFChars(normalMethodResult, 0);
LOGD("normalMethodResult: %s", normalMethodResultNativeString);
jobject staticMethodResultObj = env->CallStaticObjectMethod(userClass, staticMethod);
jstring staticMethodResult = static_cast<jstring>(staticMethodResultObj);
const char *staticMethodResultNativeString = env->GetStringUTFChars(staticMethodResult, 0);
LOGD("staticMethodResult: %s", staticMethodResultNativeString);
jobject getFormatInfoMethodResultObj = env->CallObjectMethod(userTwo, getFormatInfoMethod);
jstring getFormatInfoMethodResult = static_cast<jstring>(getFormatInfoMethodResultObj);
const char *getFormatInfoMethodResultNativeString = env->GetStringUTFChars(getFormatInfoMethodResult, 0);
LOGD("getFormatInfoMethodResult: %s", getFormatInfoMethodResultNativeString);
jobject userThreeMethodResultObj = env->CallObjectMethod(userThree, normalMethod);
jstring userThreeMethodResult = static_cast<jstring>(userThreeMethodResultObj);
const char *userThreeMethodResultNativeString = env->GetStringUTFChars(userThreeMethodResult, 0);
LOGD("userThreeMethodResult: %s", userThreeMethodResultNativeString);
jstring ygName = env->NewStringUTF("老妖怪");
jint ygAge = 999;
jobject userYG = env->NewObject(userClass, paramInitMethod, ygName, ygAge);
return userYG;
}
3 jni方法解析
## 3.1 JNIEXPORT JNICALL 宏定义
- windows
#ifndef _JAVASOFT_JNI_MD_H_
#define _JAVASOFT_JNI_MD_H_
//windows如果需要生成动态库,并且需要将该动态库交给其他项目使用,需要再方法前加入特殊标识
//才能在外部程序代码中调用DLL动态库中定义的方法,返回值前加入__declspec(dllexport)
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
//__stdcall 是一种函数调用参数的约定,在windows中调用函数时,该函数参数是以栈的形式保存的
//栈元素是后进先出的,__stdcall表示参数从右到左保存的。
#define JNICALL __stdcall
typedef long jint;
typedef __int64 jlong;
typedef signed char jbyte;
#endif /* !_JAVASOFT_JNI_MD_H_ */
- Linux
#define JNIIMPORT
//返回值前加 __attribute__ ((visibility ("default")))
//该声明的作用是保证在本动态库中声明的方法 , 能够在其他项目中可以被调用 ;
#define JNIEXPORT __attribute__ ((visibility ("default")))
//没有进行定义 , 直接置空
#define JNICALL
extern "C"//表示 C 语言 和 C++ 的兼容 ;
JNIEXPORT jstring JNICALL
Java_kim_hsl_jni_MainActivity_stringFromJNI(
JNIEnv *env,//JNIEnv *env 代表了 JNI 环境
jobject /* this */) {
// 创建 C++ 字符串
std::string hello = "Hello from C++";
// 返回 jstring 类型的字符串
return env->NewStringUTF(hello.c_str());
}
3.2 JNI *env参数解析(JNIEnv *env 参数)
#if defined(__cplusplus)//__cplusplus 是 C++ 编译器中定义的宏
typedef _JNIEnv JNIEnv;//_JNIEnv 结构体类型声明为 JNIEnv 类型
typedef _JavaVM JavaVM;
#else
//JNINativeInterface 结构体指针 类型 声明为 JNIEnv 类型
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif
3.2.1 c 语言环境中JNIEnv *env 参数解析
- typedef const struct JNINativeInterface* JNIEnv;
- JNINativeInterface结构体定义了229个函数
- 调用JNINativeInterface 结构体中的函数指针
- 类型转换 给定的参数是 JNIEnv *env 类型的 , 即 JNINativeInterface ** env 类型 , 是一个 JNINativeInterface 结构体边来那个的二级指针 ;
- 通过 JNINativeInterface 指针调用 ( 推荐) : 首先要 解引用 ( *env ) , 该变量就变成了 JNINativeInterface 结构体变量的 一级指针 , 使用 -> 即可调用指定方法 , ( *env ) -> NewStringUTF( “Hello World !” );
- 通过 JNINativeInterface 结构体调用 ( 不常用 ) : ( **env ).NewStringUTF( “Hello World !” ) , 相当于将上面的 -> 操作符变成 * . 操作符
3.2.3 c++ JNIEnv *env参数解析
-
typedef const struct JNINativeInterface* JNIEnv;
-
通过 _JNIEnv 指针调用 ( 推荐) : 直接使用 -> 符号访问该方法即可 , env-> NewStringUTF( “Hello World !” );
-
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); } ... }
3.3 基本数据类型,引用数据类型(数组)
3.3.1 jintArray类型定义
class _jobject {}; // 定义 _jobject 类 , 这是一个空类
class _jarray : public _jobject {}; // 定义 _jarray 类 继承 _jobject 类
class _jintArray : public _jarray {}; // 定义 _jintArray 类 , 继承 _jarray 类
typedef _jintArray* jintArray; // 定义 _jintArray* 别名 , jintArray
( jintArray -> jint * ) jintArray 是java环境中的int数组的内存地址,jint* GetIntArrayElements(jintArray array, jboolean* isCopy) 方法 , 可以实现上述转化 ;
3.3.2 jboolean
typedef unsigned char __uint8_t; // 定义 char 类型别名 __uint8_t
typedef __uint8_t uint8_t; // 定义 __uint8_t 类型别名 uint8_t
typedef uint8_t jboolean; // 定义 uint8_t 类型别名 jboolean
3.3.3 GetIntArrayElements
-
将java环境中的int数组类型变量(jintArray),转为c/c++环境中的jint数组指针,返回一个指针指向jint数组首元素地址
jnit本质就是int类型 GetIntArrayElements函数作用就是将jntArray转为int* 指针
-
struct _JNIEnv { /* _JNIEnv 结构体中封装了 JNINativeInterface 结构体指针 */ const struct JNINativeInterface* functions; ... jint* GetIntArrayElements(jintArray array, jboolean* isCopy) { // 调用 JNINativeInterface 结构体中封装的 GetIntArrayElements 方法 //isCopy 指向JNI_TRUE 将int数组拷贝到一个新的内存空间中,并将该内存空间首地址返回 // JNI_FALSE 直接使用java中int数组地址,返回java中的int数组的首地址 //NULL(推荐)不关心如何实现让系统自动选择指针生成方式。 return functions->GetIntArrayElements(this, array, isCopy); } ... }
3.3.4 GetArrayLength
//获取 jarray 数组长度 , 该 jarray 类型可以是下面定义的类型
typedef _jarray* jarray;
//下面是 9 个是 Java 传入的数组类型别名
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
typedef _jbyteArray* jbyteArray;
typedef _jcharArray* jcharArray;
typedef _jshortArray* jshortArray;
typedef _jintArray* jintArray;
typedef _jlongArray* jlongArray;
typedef _jfloatArray* jfloatArray;
typedef _jdoubleArray* jdoubleArray;
//GetArrayLength 函数原型
struct _JNIEnv {
/* _JNIEnv 结构体中封装了 JNINativeInterface 结构体指针 */
const struct JNINativeInterface* functions;
...
jsize GetArrayLength(jarray array)
{
调用 JNINativeInterface 结构体中封装的 GetArrayLength 方法
return functions->GetArrayLength(this, array);
}
...
}
3.3.5 日志打印
-
导入日志库 #include <android/log>
-
CMake设置日志库
# 设置动态库名称 add_library( native-lib SHARED native-lib.cpp) # 查找日志库 find_library( log-lib log) # 连接日志库 target_link_libraries( native-lib ${log-lib})
-
日志打印函数原型
int __android_log_print(int prio, const char* tag, const char* fmt, ...) //调用 __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) //推荐 #define LOG_TAG "C_TAG" #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO , LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN , LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR , LOG_TAG, __VA_ARGS__)
3.3.6 ReleaseIntArrayElements(释放c/c++ 中的int数组)
struct _JNIEnv { /* _JNIEnv 结构体中封装了 JNINativeInterface 结构体指针 */ const struct JNINativeInterface* functions; ... void ReleaseIntArrayElements(jintArray array, jint* elems, jint mode) //调用的是 JNINativeInterface 结构体中封装的 ReleaseIntArrayElements 方法 { functions->ReleaseIntArrayElements(this, array, elems, mode); } ... }
① 模式 0 : 刷新 Java 数组 , 释放 C/C++ 数组
② 模式 1 ( JNI_COMMIT ) : 刷新 Java 数组 , 不释放 C/C ++ 数组
③ 模式 2 ( JNI_ABORT ) : 不刷新 Java 数组 , 释放 C/C++ 数组3.3.6 代码示例
#include <jni.h> #include <string> //导入日志库 #include <android/log.h> //定义日志宏 , 其中的 __VA_ARGS__ 表示可变参数 #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,"JNI",__VA_ARGS__); extern "C" JNIEXPORT jstring JNICALL Java_kim_hsl_jni_MainActivity_stringFromJNI( JNIEnv *env, jobject /* this */) { // 创建 C++ 字符串 std::string hello = "Hello from C++"; // 返回 jstring 类型的字符串 // 将 C/C++ 的 char* 字符串转为 Java 中的 jstring 类型字符串 return env->NewStringUTF(hello.c_str()); } extern "C" JNIEXPORT void JNICALL Java_kim_hsl_jni_MainActivity_jniTest(JNIEnv *env, jobject instance, jint i, jstring s_) { // 将 jstring 类型数据转为 char 类型数据 const char *s = env->GetStringUTFChars(s_, 0); // 释放 env->ReleaseStringUTFChars(s_, s); } extern "C" JNIEXPORT void JNICALL Java_kim_hsl_jni_MainActivity_jniArrayTest(JNIEnv *env, jobject instance, jintArray intArray_, jobjectArray stringArray) { // I . 基本类型数组操作 // 1 . jboolean 类型 /* jboolean 类型的值可以设置成 true 或 false , 也可以不设置 如果将值传递给 GetIntArrayElements 方法 , 需要将 isCopy 的地址放在第二个参数位置 当做参数的格式 : env->GetIntArrayElements(intArray_, &isCopy); 可取值 JNI_FALSE 0 和 JNI_TRUE 1 两个值 */ jboolean isCopy = JNI_TRUE; //2 . GetIntArrayElements 方法参数解析 /* GetIntArrayElements 方法参数解析 方法作用 : 将 Java 的 int 数组 , 转为 jint 数组 , 返回一个指针指向 jint 数组首元素地址 函数原型 : jint* GetIntArrayElements(jintArray array, jboolean* isCopy) 第一个参数 : jintArray array 是参数中的 jintArray 类型变量 jintArray 类型说明 : class _jobject {}; C ++ 中定义了 _jobject 类 class _jarray : public _jobject {}; 定义 _jarray 类 继承 _jobject 类 public 继承 : 父类成员在子类中访问级别不变 class _jintArray : public _jarray {}; 定义 _jintArray 类 继承 _jarray 类 typedef _jintArray* jintArray; 将 _jintArray* 类型 声明成 jintArray 类型 第二个参数 : jboolean* isCopy 该参数用于指定将 jintArray 类型的变量 , 转为 jint * 指针类型的变量 , 新的指针变量的生成方式 将 该参数设置成指向 JNI_TRUE 的指针 : 将 int 数组数据拷贝到一个新的内存空间中 , 并将该内存空间首地址返回 将 该参数设置成指向 JNI_FALSE 的指针 : 直接使用 java 中的 int 数组地址 , 返回 java 中的 int 数组的首地址 将 该参数设置成 NULL ( 推荐 ) : 表示不关心如何实现 , 让系统自动选择指针生成方式 , 一般情况下都不关心该生成方式 注意如果是 其它类型的数组 如果是布尔类型的数组 , 使用 GetBooleanArrayElements 方法 如果是浮点型的数组 , 使用 GetFloatArrayElements 方法 如果是字符型的数组 , 使用 GetCharArrayElements 方法 ... */ jint *intArray = env->GetIntArrayElements(intArray_, NULL); //注意区别 //jint array[5]; //env->GetIntArrayRegion(int_array, 0, 5, array) //3 . 操作 jint * 指针变量 , 循环获取数组中每个元素的值 /* 获取数组长度 函数原型 : jsize GetArrayLength(jarray array) 返回值类型 jsize : jsize 类型 : 由下面可知 jsize 只是 int 类型的别名 typedef jint jsize; typedef int32_t jint; typedef __int32_t int32_t; typedef int __int32_t; */ jsize len = env->GetArrayLength(intArray_); //4 . 循环打印 int 数组中的元素 /* 使用指针进行访问 intArray 是数组首元素地址 intArray + 1 是第 1 个元素的首地址 intArray + k 是第 k 个元素的首地址 使用 *(intArray + k) 可以获取第 k 个元素的值 */ for(int i = 0; i < len; i ++){ //获取第 i 个元素的首地址 , 使用 *num 可以获取第 i 个元素的值 int *num = intArray + i; /* __android_log_print 打印 Android 日志函数 函数原型 : int __android_log_print(int prio, const char* tag, const char* fmt, ...) int prio 参数 : 日志的等级 , 定义在 jni.h 的 android_LogPriority 枚举中 ANDROID_LOG_VERBOSE ANDROID_LOG_DEBUG ANDROID_LOG_INFO ANDROID_LOG_WARN ANDROID_LOG_ERROR const char* tag 参数 : 日志打印的 TAG 标签 , 这是一个 C/C++ char* 类型字符串 const char* fmt, ... 参数 : 可变参数 */ __android_log_print(ANDROID_LOG_INFO, "JNI_TAG" , "%d . %d" , i , *num); //修改数组中的值 *num = 8888; } //5 . 释放 jint * 类型的指针变量 /* 函数原型 : void ReleaseIntArrayElements(jintArray array, jint* elems, jint mode) 第一参数 jintArray array : 是 Java 层传入的 int 数组 参数 , 即 Native 层的调用函数的参数 第二参数 jint* elems : 通过 GetIntArrayElements 方法将 jintArray 变量转成的 jint* 变量 第三参数 jint mode : 设置处理模式 , 有三种处理模式 模式 0 : 刷新 Java 数组 , 释放 C/C++ 数组 模式 1 ( JNI_COMMIT ) : 刷新 Java 数组 , 不释放 C/C ++ 数组 模式 2 ( JNI_ABORT ) : 不刷新 Java 数组 , 释放 C/C++ 数组 下面是 jni.h 中的定义的模式 : #define JNI_COMMIT 1 copy content, do not free buffer #define JNI_ABORT 2 free buffer w/o copying back 如果设置 0 和 1 , 那么 如果修改了 int 数组的值 , 那么最终 Java 层的值会被修改 如果设置 2 , 那么 如果修改了 int 数组的值 , 那么最终 Java 层的值不会被修改 */ env->ReleaseIntArrayElements(intArray_, intArray, 0); }
GetIntArrayElements vs GetArrayRegion
-
GetIntArrayRegion 把java数组拷贝到c++新建数组内存区域中
- c++ 不会改变java中的数值
- 不用拷贝占用额外的内存
-
GetIntArrayElements把c++的一个数组指针指向java的数组,这样c++ 修改数组后java数组也会改变
- 要想使GetIntArrayElements让java层收到,还需要释放掉c++指针,否则会造成内存泄漏
-
3.4 c/c++调用java方法
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_jni_MainActivity_jniObjectTest(JNIEnv *env, jobject instance, jobject student) {
/*
参数解析 :
JNIEnv *env : JNI 环境 , 结构体指针 , 结构体中封装了 209 个方法
jobject instance : 是 MainActivity 对象
jobject student : Java 层创建的 Student 对象 , 传入 Native 层
*/
//在 C/C++ 中调用 Student 对象的 get 方法
//1 . 获取 Java 对应的 Class 对象
jclass student_class = env->GetObjectClass(student);
//2 . 通过 Class 的反射获取要调用的方法
/*
函数原型 :
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
{ return functions->GetMethodID(this, clazz, name, sig); }
参数说明 :
jclass clazz : 使用 GetObjectClass 方法获取的返回值
const char* name : 要调用的方法名称
const char* sig : 函数签名 , 具体的签名规则查看签名表格
public int getAge() 函数签名 : ()I
左侧的 () : 左侧的括号是参数列表类型签名
右侧的 I : 括号右的 I 是返回值类型 int
public void setAge(int age) 函数签名 : (I)V
(I) 表示左侧的参数列表
右侧的 V 表示其返回值类型是 void 类型
引用类型签名 : L + 全限定名 + ;
javap 工具 :
可以使用 javap 工具获取方法签名
*/
//获取 Student 的 public int getAge() 方法
jmethodID method_getAge = env->GetMethodID(student_class, "getAge" , "()I");
//获取 Student 的 public void setAge(int age) 方法
jmethodID method_setAge = env->GetMethodID(student_class, "setAge" , "(I)V");
//获取 Student 的 public static void logInfo(String info) 方法
// 注意这里要使用 GetStaticMethodID 方法反射该静态方法
jmethodID method_logInfo = env->GetStaticMethodID(student_class, "logInfo" , "(Ljava/lang/String;)V");
//3 . 调用 Java 对象的方法
/*
调用 Java 引用对象的方法 : 要根据 返回值类型不同 , 调用不同的方法
如果返回值是 int 类型 , 那么就需要调用 CallIntMethod 方法
如果返回值是 void 类型 , 那么就需要调用 CallVoidMethod 方法
如果调用的是静态方法 , 那么需要调用
( 注意 : 调用方法时传入的参数都必须是 C/C++ 中的 Java 类型参数 , 如 jint , jstring 等 )
*/
//调用 Student 对象的 public void setAge(int age) 方法
env->CallVoidMethod(student, method_setAge, 18);
//调用 Student 的 public int getAge() 方法
jint age = env->CallIntMethod(student, method_getAge);
/*
调用静态方法 :
1 . 创建 Java 字符串
2 . 调用静态方法
3 . 释放 Java 字符串
*/
// 创建 Java 字符串
jstring info = env->NewStringUTF("C/C++ 创建的 Java 字符串");
// 调用静态方法 : 注意传入的参数
env->CallStaticVoidMethod(student_class, method_logInfo, info);
// jstring info 在方法中创建新的字符串 , 需要在方法结束之前释放该引用对象
env->DeleteLocalRef(info);
//4 . 设置 Student 对象属性
/*
反射获取属性
函数原型 :
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
{ return functions->GetFieldID(this, clazz, name, sig); }
参数说明 :
jclass clazz : Java 类对象
const char* name : 属性名称
const char* sig : 属性类型签名
设置反射属性值 :
函数原型 :
void SetIntField(jobject obj, jfieldID fieldID, jint value)
{ functions->SetIntField(this, obj, fieldID, value); }
参数说明 :
jobject obj : 设置对象
jfieldID fieldID : 通过 GetFieldID 方法获取的属性 ID
jint value : 要设置的值
注意 : 设置不同类型的值 , 调用不同的设置方法
*/
jfieldID age_field_id = env->GetFieldID(student_class, "age", "I");
env->SetIntField(student, age_field_id, 90);
// 验证是否设置成功
age = env->CallIntMethod(student, method_getAge);
//5 . 在 JNI 中创建 java 对象 , 并设置给另一个对象
/*
获取 Teacher 类 : 通过调用 FindClass 方法获取 Teacher 类
目前已知两种获取 jclass 的方法
获取 Teacher 类的构造方法 public Student(int age, String name)
构造方法的方法名都是 "<init>"
构造方法的函数签名为
此处还要特别注意 : 传入到 Java 方法中的参数 , 必须都是 Java 参数类型
如 jstring , jint , jintArray 等类型 , 不能将 C/C++ 类型传入参数
尤其是 char* 字符串 , 需要使用 NewStringUTF 将 C/C++ 字符串转为 jstring 类型字符串
创建 Teacher 对象
将 Teacher 对象设置给 Student 对象
*/
// 5.1 获取 Student 的 public void setTeacher(Teacher teacher) 方法
// 注意这个 Teacher 的类型签名是 Lkim/hsl/jni/Teacher;
jmethodID method_setTeacher = env->GetMethodID(student_class, "setTeacher" , "(Lkim/hsl/jni/Teacher;)V");
LOGE("method_setTeacher");
// 5.2 获取 Teacher 类 ( 该变量需要释放 )
jclass class_teacher = env->FindClass("kim/hsl/jni/Teacher");
// 5.3 查找构造方法
jmethodID method_init = env->GetMethodID(class_teacher, "<init>", "(ILjava/lang/String;)V");
// 5.4 准备 Java 类型参数 ( 该变量需要释放 )
// 此处特别注意 : 传入到 Java 方法中的参数都必须是 Java 参数
jint teacher_age = 88;
jstring teacher_name = env->NewStringUTF("Tom Wang");
// 5.5 创建 Teacher 对象 ( 该变量需要释放 )
jobject teacher = env->NewObject(class_teacher, method_init, teacher_age, teacher_name);
// 5.6 调用 Student 对象的 setTeacher 设置方法
env->CallVoidMethod(student, method_setTeacher, teacher);
// 5.7 释放上面通过 FindClass NewStringUTF NewObject 创建的引用变量 , 便于节省内存 , 也可以等到 作用域结束 自动释放
// 使用完这三个引用之后 , 不再使用 ; 这里特别建议手动释放三个引用
// 如果不手动释放 , 在 该引用 作用域 结束后 , 也会自动释放掉
env->DeleteLocalRef(teacher_name);
env->DeleteLocalRef(teacher);
env->DeleteLocalRef(class_teacher);
}
3.5 jni引用(局部引用,全局引用)
- 局部引用
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_jni_MainActivity_jniLocalReferenceTest(JNIEnv *env, jobject instance) {
/*
局部引用
局部引用只能在当前作用域有效
超出作用域
手动释放
上面 两种情况 都会导致 该局部变量都会失效
局部引用作用范围 :
空间 : 不能 跨线程 , 跨方法调用 , 仅在本作用域有效
时间 : 创建后可以使用 , 手动释放 或 作用域结束 引用被释放不可使用
局部引用 创建 : 使用 NewXXX / FindXXX 等 大多数 JNI 方法 默认创建的都是局部引用
释放 : 调用 DeleteLocalRef 方法 释放该局部引用
关于上面的三个创建的 局部引用 有两种释放方式
方式一 : 在方法作用域结束后 , VM 自动释放上述变量
方式二 : 通过调用 DeleteLocalRef 方法手动释放
建议使用方式二 :
局部引用 释放尽量灵活 , 不要等待自动释放 , 在使用完毕后 建议就手动释放 , 今早回收内存
如果该 引用 一直到最后都要使用 , 那么可以不进行手动释放 ;
建议用法 : 局部引用建议都要手动释放 , 哪怕是在作用域最后 , 也要进行手动释放
局部引用传递到 Java 层 , 该传递是拷贝传递 , JNI 中该释放还是释放 , 不影响 Java 层使用
引用概念 :
这里要将 引用 和 指针的概区分清楚 ;
class_teacher 引用在 作用域结束时 会被释放 , 不能将其用于 JNI 反射 Java 类的方法和字段
其指针值不为空 , 仍然有值 , 其仍然指向一个地址 , 但是地址中的数据被释放了
*/
// 1 . 获取 Teacher 类 ( 该变量需要释放 )
jclass class_teacher = env->FindClass("kim/hsl/jni/Teacher");
// 2 . 查找构造方法
jmethodID method_init = env->GetMethodID(class_teacher, "<init>", "(ILjava/lang/String;)V");
// 3 . 准备 Java 类型参数 ( 该变量需要释放 )
// 此处特别注意 : 传入到 Java 方法中的参数都必须是 Java 参数
jint teacher_age = 88;
jstring teacher_name = env->NewStringUTF("Tom Wang");
// 4 . 创建 Teacher 对象 ( 该变量需要释放 )
jobject teacher = env->NewObject(class_teacher, method_init, teacher_age, teacher_name);
// 5 . 释放上面通过 FindClass NewStringUTF NewObject 创建的引用变量 , 否则会造成内存泄漏
// 使用完这三个引用之后 , 不再使用 ; 这里特别建议手动释放三个引用
// 如果不手动释放 , 在 该引用 作用域 结束后 , 也会自动释放掉
env->DeleteLocalRef(teacher_name);
env->DeleteLocalRef(teacher);
env->DeleteLocalRef(class_teacher);
}
- 全局引用
// 全局引用
// 访问时如果局部变量也有同名变量 , 可以使用 域作用符 访问
// ::class_teacher 表示访问全局的变量
jclass class_teacher_global;
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_jni_MainActivity_jniGlobalReferenceTest(JNIEnv *env, jobject instance) {
/*
全局引用 作用域 :
空间 : 可以 跨方法 , 跨线程使用
时间 : 创建后可以使用 , 手动释放后全局引用失效
全局引用创建 : NewGlobalRef
全局引用释放 : DeleteGlobalRef
全局引用会阻止 JVM 回收该引用
这里注意域作用符的使用 , 本方法中没有 class_teacher_global 同名变量 , :: 可用 可 不用
*/
// 1 . 获取 Teacher 类 ( 该变量需要释放 )
if(::class_teacher_global == NULL) {
//生成局部引用 , 该局部引用使用完毕后可释放
jclass tmp_class = env->FindClass("kim/hsl/jni/Teacher");
//将上述生成的局部引用变成 全局引用
// 全局引用释放时 , env->DeleteGlobalRef(class_teacher_global) 即可释放下面转换的 全局引用
::class_teacher_global = static_cast<jclass>(env->NewGlobalRef(tmp_class));
//将局部引用释放掉
env->DeleteLocalRef(tmp_class);
}
}
// 弱全局引用
// 访问时如果局部变量也有同名变量 , 可以使用 域作用符 访问
// ::class_teacher_weak_global 表示访问全局的变量
jclass class_teacher_weak_global;
extern "C"
JNIEXPORT void JNICALL
Java_kim_hsl_jni_MainActivity_jniWeakGlobalReferenceTest(JNIEnv *env, jobject instance) {
/*
弱全局引用
弱全局引用 与 Java 引用类似
弱全局引用 作用域 :
空间 : 都可以 跨方法 , 跨线程使用
时间 : 创建弱全局引用后可以开始使用 , JVM 自动回收 或 手动释放 该弱全局引用不可用
弱全局引用 与 全局引用 区别 :
全局引用 : 不能被 回收 , 如果内存不足就 抛出异常
弱全局引用 : 当内存不足时 , 会被系统自动回收
弱全局引用 判定是否被回收 :
使用 IsSameObject(弱引用变量 , NULL) 判断该对象是否被回收了 , 将其与 NULL 比较 , 即可判定
返回 true ( 该对象与NULL相等 ) : 该若全局引用已经被释放
返回 false ( 该对象与NULL不相等 ) : 还可以使用
*/
// 1 . 获取 Teacher 类 ( 该变量需要释放 )
//如果 class_teacher_weak_global 对象被回收 , 返回 true ; 没有被回收返回 false ;
jboolean isClassReleased = env->IsSameObject(class_teacher_weak_global, NULL);
if( class_teacher_weak_global == NULL || isClassReleased ) {
//生成局部引用 , 该局部引用使用完毕后可释放
jclass tmp_class = env->FindClass("kim/hsl/jni/Teacher");
//将上述生成的局部引用变成弱全局引用
// 弱全局引用释放时 , env->DeleteWeakGlobalRef(class_teacher_weak_global) 即可释放下面转换的弱全局引用
class_teacher_weak_global = static_cast<jclass>(env->NewWeakGlobalRef(tmp_class));
//将局部引用释放掉
env->DeleteLocalRef(tmp_class);
}
}
3.6 动态注册
package kim.hsl.onload;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
// Used to load the 'native-lib' library on application startup.
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
dynamicRegisterJavaMethod();
dynamicRegisterJavaMethod2(250);
}
/**
* 动态注册 JNI 方法 , Java 方法
*/
public native void dynamicRegisterJavaMethod();
public native int dynamicRegisterJavaMethod2(int i);
}
#include <jni.h>
#include <string>
#include <android/log.h>
/*
I . JNI_Onload 方法
JNI_Onload 方法在 Java 层执行 System.loadLibrary("native-lib") 代码时调用的方法
主要是执行一些 JNI 初始化操作
JNI_Onload 常见操作 :
① 保存 JavaVM 对象 , 使用全局变量记录该 Java 虚拟机对象
② 动态注册 : 动态注册是该方法中最常见的操作
JNI_Onload 参数说明 :
JavaVM *vm : 代表了 Java 虚拟机
void *r : 一般是 NULL
JNI_Onload 返回值说明 :
int 类型返回值代表了当前 NDK 使用的 JNI 版本
JNI 版本 中可选的有四个值 , 但是只能选择返回后三个 JNI_VERSION_1_2 , JNI_VERSION_1_4 , JNI_VERSION_1_6
返回上述三个值任意一个没有区别
返回 JNI_VERSION_1_1 会报错
#define JNI_VERSION_1_1 0x00010001
#define JNI_VERSION_1_2 0x00010002
#define JNI_VERSION_1_4 0x00010004
#define JNI_VERSION_1_6 0x00010006
这四个值分别代表了 JDK 1.1 , 1.2 , 1.4 , 1.6 对应的 JNI 版本
II . 动态注册
动态注册 :
动态注册与静态注册 :
静态注册 : 使用 Java_包名_类名_方法名(JNIEnv* env, jobject obj, ...) 方式进行注册是静态注册
动态注册 : 将 C/C++ 中的本地方法 与 Java 中的方法对应起来 , 就需要使用动态注册
动态注册 与 静态注册 : 没有太大区别 , 都可以将 C/C++ 本地方法 与 Java 方法对应起来
动态注册流程 :
① 声明 Java 层 Native 方法
② 准备数据 JNINativeMethod methods[] 数组
③ 编写 JNI_OnLoad 方法
④ 获取 JNIEnv 指针
⑤ 获取 Java 类
⑥ 进行动态注册
*/
//使用 全局变量 记录 Java 虚拟机对象
JavaVM *_vm;
/*
动态注册对应的 C/C++ 本地方法
如果动态注册的方法需要传递参数 , 需要加上 前面的 JNIEnv *env, jobject obj 两个参数
如果不传递参数 , 就可以不添加任何参数
不传递参数 , 参数可以空着
*/
void dynamicRegisterCMethod(){
__android_log_print(ANDROID_LOG_INFO, "JNI_TAG", "dynamicRegisterCMethod");
}
/*
动态注册对应的 C/C++ 本地方法
如果动态注册的方法需要传递参数 , 需要加上 前面的 JNIEnv *env, jobject obj 两个参数
如果不传递参数 , 就可以不添加任何参数
传递参数 , 那么需要写上 JNI 调用的完整参数
*/
jint dynamicRegisterCMethod2(JNIEnv *env, jobject obj, jint i){
__android_log_print(ANDROID_LOG_INFO, "JNI_TAG", "dynamicRegisterCMethod2 : %d", i);
return i + 1;
}
/*
该数组中 , 每个元素都是一个 JNI 的 Native 方法
JNINativeMethod 是结构体
typedef struct {
const char* name; //Java 中定义的 Native 方法名 , 注意这是一个 C 字符串
const char* signature; //函数签名 , 可以使用 javap 生成
void* fnPtr; //C/C++ 中的 Native 函数指针
} JNINativeMethod;
*/
static const JNINativeMethod methods[] = {
{"dynamicRegisterJavaMethod", "()V", (void *)dynamicRegisterCMethod},
{"dynamicRegisterJavaMethod2", "(I)I", (void *)dynamicRegisterCMethod2}
};
/*
动态注册的 Java 类名称
注意 : 包名类名之间使用 "/" 分割
*/
static const char* className = "kim/hsl/onload/MainActivity";
int JNI_OnLoad(JavaVM *vm , void* reserved){
__android_log_print(ANDROID_LOG_INFO, "JNI_TAG", "JNI_Onload");
//I . 存储 Java 虚拟机对象
//将 Java 虚拟机对象记录到全局变量中
_vm = vm;
//II . 动态注册
//1 . 获取 JNIEnv JNI 环境 , 需要从 JavaVM 获取
JNIEnv *env = nullptr;
//2 . 调用 JavaVM / _JavaVM 结构体的 jint GetEnv(void** env, jint version) 方法
// 返回值分析 : 动态注册会返回一个结果
// 如果 registerResult < 0 , 则动态注册失败
// 如果 registerResult == 0 , 则动态注册失败
int registerResult = vm->GetEnv( (void **) &env, JNI_VERSION_1_6 );
//3 . 判断结果 : 如果动态注册 Native 方法失败 , 直接退出
if(registerResult != JNI_OK){
return -1;
}
//4 . 获取要动态注册的 Java 类的 Class 对象
jclass jclazz = env->FindClass(className);
/*
5 .正式注册
注册方法解析 :
jint RegisterNatives(
jclass clazz, //要注册的 Java 类
const JNINativeMethod* methods, //JNI注册方法数组
jint nMethods //要注册的 JNI 方法个数
)
sizeof(methods) / sizeof(JNINativeMethod) : 计算 JNINativeMethod methods[] 数组大小
*/
env->RegisterNatives(jclazz, methods, sizeof(methods) / sizeof(JNINativeMethod));
return JNI_VERSION_1_6;
}
3.7 jni 线程创建
package kim.hsl.thread;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.widget.TextView;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary("native-lib");
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
threadDemoJava();
}
/**
* JNI 线程 Demo
*/
public native void threadDemoJava();
/**
* 打印当前线程信息
*/
public void logThread(){
Log.i("JNI_TAG", Thread.currentThread().toString());
}
}
#include <jni.h>
#include <string>
#include <android/log.h>
//导入线程头文件
#include <pthread.h>
//Java 虚拟机指针 , 在 JNI_OnLoad 方法中设置该值
JavaVM *_vm;
//JNI 方法参数中的第二个参数 , 需要先将局部变量转为全局变量 , 然后再其它方法中调用
jobject obj;
/*
线程执行的方法
如果在 Native 层执行耗时操作 , 如下载文件 , 需要在线程中处理
JNI 方法参数中的 JNIEnv 指针是不能跨线程使用的 , 在 主线程中调用 JNI 方法 , 其 JNIEnv 指针不能在子线程中使用
如果在子线程中使用 JNIEnv 指针 , 需要使用 JavaVM 获取 指定线程的 JNIEnv 指针
调用 JavaVM 的 AttachCurrentThread 可以获取本线程的 JNIEnv 指针
注意最后还要将线程从 Java 虚拟机中剥离
关于参数传递 :
传递 int 类型 和 int * 类型 , 传递指针可以在 方法中修改 int 变量值 ;
传递 int * 类型 和 int ** 类型 , 传递二维指针 可以在方法中修改 int * 一维指针值
因此有些参数需要在方法中修改, 并且需要保存该修改状态 , 就需要将该变量的地址当做参数传入
原来的普通变量 变成 指针变量 , 一维指针 变 二维指针
*/
void* threadRun(void *args){
__android_log_print(ANDROID_LOG_INFO, "JNI_TAG", "threadRun");
//JNIEnv 不能跨线程使用 , 这里需要先获取本线程的 JNIEnv
JNIEnv *env;
//将线程附加到 Java 虚拟机中 ( 注意后面对应剥离线程操作 )
// 如果成功返回 0 , 如果失败 , 直接退出
int attachResult = _vm->AttachCurrentThread(&env, 0);
//获取 MainActivity 对应的 jclass 对象
jclass clazz = env->GetObjectClass( obj );
//反射获取 logThread 方法
jmethodID logThreadID = env->GetMethodID(clazz, "logThread", "()V");
//调用 logThread 方法
env->CallVoidMethod(obj, logThreadID);
//释放相关的局部变量
env->DeleteLocalRef(clazz);
//将线程从 Java 虚拟机中剥离
_vm->DetachCurrentThread();
//注意这里一定要返回 0 , 否则执行到结尾会崩溃
return 0;
}
void threadDemoC(JNIEnv *env, jobject instance){
__android_log_print(ANDROID_LOG_INFO, "JNI_TAG", "threadDemoC");
//保存全局变量 , 先将局部变量转为全局变量 , 然后再赋值给全局的 obj 变量
// 使用域作用符访问全局的 ::obj 变量
::obj = env->NewGlobalRef(instance);
//代表一个线程的句柄
pthread_t pid;
//创建线程并执行
pthread_create( &pid , 0 , threadRun, 0 );
}
//下面的代码是动态注册内容
static const JNINativeMethod methods[] = {
{"threadDemoJava", "()V", (void *)threadDemoC}
};
static const char* className = "kim/hsl/thread/MainActivity";
int JNI_OnLoad(JavaVM *vm , void* reserved){
// 1 . 记录 Java 虚拟机指针
_vm = vm;
// 2 . 动态注册方法
//获取 JNIEnv 指针
JNIEnv *env = nullptr;
int registerResult = vm->GetEnv( (void **) &env, JNI_VERSION_1_6 );
if(registerResult != JNI_OK){
return -1;
}
//进行动态注册
jclass jclazz = env->FindClass(className);
env->RegisterNatives(jclazz, methods, sizeof(methods) / sizeof(JNINativeMethod));
return JNI_VERSION_1_6;
}
附录
最后
以上就是粗暴猫咪为你收集整理的jni学习1. 介绍2 案例3 jni方法解析附录的全部内容,希望文章能够帮你解决jni学习1. 介绍2 案例3 jni方法解析附录所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复