NDK基础_androidndk-程序员宅基地

技术标签: c++  jni&ndk  

一:ndk简介

ndk全称Native Developer Kits(原生开发工具包),Android NDK也是Android SDK的一个扩展集,用来扩展SDK的功能。 NDK打通了Java和C/C++之间的开发障碍,让Android开发者也可以使用C/C++语言开发APP。

Java是在C/C++之上的语言,语言金字塔越往上对开发者就更加贴近,也就是更容易开发,但是性能相对也就越低。越往下对开发人员的要求也就越高,但是实现后的产品性能也越高,因为可以自己控制内存等模块的使用,而不是让Java虚拟机自行处理。

二:NDK架构分层

使用NDK开发最终目标是为了将C/C++代码编译生成.so动态库或者.a静态库文件,并将库文件提供给Java代码调用。

 NDK分为三层:构建层  Java层  native层

2.1:构建层

要得到目标的so文件,需要有个构建环境以及过程,将这个过程和环境称为构建层

构建层需要将C/C++代码编译为动态库so,那么这个编译的过程就需要一个构建工具,构建工具按照开发者指定的规则方式来构建库文件,类似apk的Gradle构建过程。

⑴CPU架构:Android abi

ABI即Application Binary Interface,定义了二进制接口交互规则,以适应不同的CPU,一个ABI对应一种类型的CPU

Android目前支持以下7种ABI:

CPU 主要abi类型 说明
ARMv5 armeabi 第5代和6代的ARM处理器,早期手机用的比较多。
ARMv7 armeabi-v7a 第7代及以上的 ARM 处理器。
ARMv8 arm64-v8a 第8代,64位ARM处理器
x86 x86 一般用在平板,模拟器。
x86_64 x86_64 64位平板

⑵构建工具:ndk-build构建   Cmake构建

ndk-build构建(已淘汰)

ndk-build其实就是一个脚本。早期的NDK开发一直都是使用这种模式,

使用ndk-build需要配合两个mk文件:Android.mkApplication.mk

Android.mk文件

Android.mk文件更像是一个传统的makefile文件,其定义源代码路径,头文件路径,链接器的路径来定位库,模块名,构建类型等。

Application.mk

其定义了Android app的相关属性。如:Android Sdk版本调试或者发布模式目标平台ABI标准C/C++库

   ② Cmake构建
❶Cmake简介

Cmake 是用来生成makefile文件的,cmake使用一个CmakeLists.txt的配置文件来生成对应的makefile文件。

❷Cmake构建动态库so的过程

步骤1:使用Cmake生成编译的makefiles文件

步骤2:使用Make工具对步骤1中的makefiles文件进行编译为库或者可执行文件。

那使用Cmake优势在哪里呢?相信了解Gradle构建的都知道,为什么现在的apk构建过程会这么快,就是因为其在编译apk之前会生成一个任务依赖树,因此在多核状态下,任务可以在异步状态下执行,所以apk构建过程会非常快。而我们的Cmake也是类似,其在生成makefile过程中会自动分析源代码,创建一个组件之间依赖的关系树,这样就可以大大缩减在make编译阶段的时间。

CMake最大优点就是可以动态调试C/C++代码

 

❸Cmake基本语法:见另一篇博文

❹Cmake构建项目配置
android {
  defaultConfig {
     externalNativeBuild {
       cmake { 
               //声明当前Cmake项目使用的Android abi
            	abiFilters "armeabi-v7a"
              	//提供给Cmake的参数信息 可选
                arguments "-DANDROID_ARM_NEON=TRUE", "-DANDROID_TOOLCHAIN=clang"
               //提供给C编译器的一个标志 可选
                cFlags "-D__STDC_FORMAT_MACROS"
                //提供给C++编译器的一个标志 可选
                cppFlags "-fexceptions", "-frtti","-std=c++11"
               	//指定哪个so库会被打包到apk中去 可选
                targets "libexample-one", "my-executible-demo"



 }
 }
 }



 externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt" //声明cmake配置文件路径
            version "3.10.2" //声明cmake版本
        }

    }

 

}

2.2:Java层

⑴怎么选择正确的so?

我们在编译so的时候就需要确定自己设备类型,根据设备类型选择对应abiFilters

由于不同CPU指令的向前兼容性,假设我们只有arm7代处理器,那么只需要选择armeabi-v7a即可,如果既有7代也有7代之前的,可以同时选择armeabi和armeabi-v7a,设备会自动选择使用正确版本,同理对于32位还是64位处理器也是一样的道理。

注意:使用as编译后的so会自动打包到apk中,如果需要提供给第三方使用,可以到build/intermediates/cmake/debug or release 目录中copy出来

 第三方库一般直接放在main/jniLibs文件夹下,也有放在默认libs目录下的,但是必须在build.gradle中声明jni库的目录:

sourceSets {
    main {
        jniLibs.srcDirs = ['jniLibs']
    }
}
⑵Java层如何调用so文件中的函数?

对于Android上层代码来说,在将包正确导入到项目中后,只需要一行代码就可以完成动态库的加载过程。

System.load("/data/local/tmp/libnative_lib.so"); 
System.loadLibrary("native_lib");

以上两个方法用于加载动态,区别如下:

  • 1.加载路径不同:load是加载so的完整路径,而loadLibrary是加载so的名称,然后加上前缀lib和后缀.so去默认目录下查找。

  • 2.自动加载库的依赖库的不同:load不会自动加载依赖库;而loadLibrary会自动加载依赖库。

  • 3.loadLibrary()和load()都用于加载动态库,loadLibrary()可以方便自动加载依赖库,load()可以方便地指定具体路径的动态库。对于loadLibrary()会将将xxx动态库的名字转换为libxxx.so,再从/data/app/[packagename]-1/lib/arm64,/vendor/lib64,/system/lib64等路径中查询对应的动态库。无论哪种方式,最终都会调用到LoadNativeLibrary()方法,该方法主要操作:

通过dlopen打开动态库文件

通过dlsym找到JNI_OnLoad符号所对应的方法地址

通过JNI_OnLoad去注册对应的jni方法

动态库加载过程调用栈如下:

System.loadLibrary()
  Runtime.loadLibrary()
    Runtime.doLoad()
      Runtime_nativeLoad()
          LoadNativeLibrary()
              dlopen()
              dlsym()
              JNI_OnLoad()

2.3:Native层

一、JNI 涉及的名词概念

1.1、 JNI:Java Native Interface

JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。

1.2 JNI 开发的基本流程
⑴创建 HelloWorld.java,并声明 native 方法 sayHi();

 ⑵使用 javac 命令编译源文件,生成 HelloWorld.class 字节码文件;

⑶使用 javah 命令导出 HelloWorld.h 头文件(头文件中包含了本地方法的函数原型);

⑷在源文件 HelloWorld.cpp 中实现函数原型;

⑸编译本地代码,生成 Hello-World.so 动态原生库文件;

⑹在 Java 代码中调用 System.loadLibrary(...) 加载 so 文件;

⑺使用 Java 命令运行 HelloWorld 程序。

1.3、 注册 JNI 函数的方式

Java 的 native 方法和 JNI 函数是一一对应的映射关系,建立这种映射关系的注册方式有 2 种:

方式 1 - 静态注册: 基于命名约定建立映射关系;

方式 2 - 动态注册: 通过 JNINativeMethod 结构体建立映射关系。

  • 静态库
系统 静态库文件
Windows .lib
Linux .a
MacOS/IOS .a

.a 静态库就是好多个 .o 合并到一块的集合,经常在编译C 库的时候会看到很多.o,这个.o 就是目标文件 由 .c + .h 编译出来的。.c 相当于 .java, .hC 库对外开放的接口声明。对外开放的接口 .h.c 需要一一对应,如果没有一一对应,外部模块调用了接口,编译的时候会提示找不到方法。

.a 存在的意义可以看成 Android aar 存在的意义,方便代码不用重复编译, 最终为了生成 .so (apk)

 

  • 动态库
系统 动态库文件
Windows .dll
Linux .so
MacOS/IOS .dylib

动态库 ,在 Android 环境下就是 .so ,可以直接被java 代码调用的库.

1.4、加载 so 库的时机 

so 库需要在运行时调用 System.loadLibrary(…) 加载,一般有 2 种调用时机:

在类静态初始化中: 如果只在一个类或者很少类中使用到该 so 库,则最常见的方式是在类的静态初始化块中调用;

在 Application 初始化时调用: 如果有很多类需要使用到该 so 库,则可以考虑在 Application 初始化等场景中提前加载。



二:JNI 模板代码

kotlin代码

//helloJni()的返回值为String,映射到jni方法中的返回值即为jstring
  external fun helloJni(): String?
  //helloJni2(int age, boolean isChild),增加了两个参数int和boolean,jni对应的映射为jint和jboolean,同时返回值float映射为jfloat。
  external fun helloJni2(age: Int, isChild: Boolean): Float	   

 jni代码

/**
 * 尽管java中的stringFromJNI()方法没有参数,但cpp中仍然有两个参数,
 * 参数一:JNIEnv* env表示指向可用JNI函数表的接口指针
 * 参数二:jobject是调用该方法的java对象
 */

 extern "C"
JNIEXPORT jstring JNICALL Java_com_jason_jni_JNIDemo_helloJni
        (JNIEnv *env, jclass clazz){
    return env->NewStringUTF("I am from c++");
}


extern "C"
JNIEXPORT jfloat JNICALL Java_com_jason_jni_JNIDemo_helloJni2
        (JNIEnv *env, jclass clazz, jint age, jboolean isChild){
    
}

 
2.1 JNI 函数名

为什么 JNI 函数名要采用Java_com_jason_jni_MainActivity_stringFromJNI 的命名方式呢?—— 这是 JNI 函数静态注册约定的函数命名规则。Java 的 native 方法和 JNI 函数是一一对应的映射关系,而建立这种映射关系的注册方式有 2 种:静态注册 + 动态注册。

静态注册是基于命名约定建立的映射关系,一个 Java 的 native 方法对应的 JNI 函数会采用约定的函数名,即 Java_[类的全限定名 (带下划线)]_[方法名] 。JNI 调用 sayHi() 方法时,就会从 JNI 函数库中寻找函数 Java_com_jason_jni_MainActivity_stringFromJNI()

2.2 关键词 JNIEXPORT

JNIEXPORT 是宏定义,表示一个函数需要暴露给共享库外部使用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:

jni.h

// Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)

// Linux 平台:
#define JNIIMPORT
#define JNIEXPORT  __attribute__ ((visibility ("default")))
2.3 关键词 JNICALL

JNICALL 是宏定义,表示一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:

jni.h

// Windows 平台 :
// __stdcall 是一种函数调用参数的约定 ,表示函数的调用参数是从右往左。
#define JNICALL __stdcall 


// Linux 平台:
#define JNICALL
2.4 参数 jobject

jobject 类型是 JNI 层对于 Java 层应用类型对象的表示。每一个从 Java 调用的 native 方法,在 JNI 函数中都会传递一个当前对象的引用。

  • 1、静态 native 方法: 第二个参数为 jclass 类型,指向 native 方法所在类的 Class 对象;
  • 2、实例 native 方法: 第二个参数为 jobject 类型,指向调用 native 方法的对象。
extern "C"
//  第二个参数为 jclass 类型,指向 native 方法所在类的 Class 对象;
JNIEXPORT jint JNICALL Java_com_jason_jni_MainActivity_getAge(JNIEnv *env, 
jobject thiz) {
    //获取java类的实例对象
   //  第二个参数为 jobject 类型,指向调用 native 方法的对象。
    jclass clazz = env->GetObjectClass(thiz);
    //判断thiz是否为jclass类型
    jboolean result = env->IsInstanceOf(thiz, clazz);
    LOGD("jni->result=%d", result);
    return 1;
}
2.5 JavaVM 和 JNIEnv 的作用

JavaVMJNIEnv 是定义在 jni.h 头文件中最关键的两个数据结构:

JavaVM: 代表 Java 虚拟机,每个 Java 进程有且仅有一个全局的 JavaVM 对象,JavaVM 可以跨线程共享;

JNIEnv: 代表 Java 运行环境,每个 Java 线程都有各自独立的 JNIEnv 对象,JNIEnv 不可以跨线程共享。JNIEnv又是一个指针,所以JNI中有哪些函数,只需要找到JNIEnv的实现体就可以了.

JavaVM 和 JNIEnv 的类型定义在 C 和 C++ 中略有不同,但本质上是相同的,内部由一系列指向虚拟机内部的函数指针组成。 类似于 Java 中的 Interface 概念,不同的虚拟机实现会从它们派生出不同的实现类,而向 JNI 层屏蔽了虚拟机内部实现(例如在 Android ART 虚拟机中,它们的实现分别是 JavaVMExt 和 JNIEnvExt)。

jni.h 

struct _JNIEnv;
struct _JavaVM;


// 如果定义了 __cplusplus 宏,则按照 C++ 编译
#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else

// 按照 C 编译
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif



/*
 * C++ 版本的 _JavaVM,内部是对 JNIInvokeInterface* 的包装
 */

struct _JavaVM {
    // 相当于 C 版本中的 JNIEnv
    const struct JNIInvokeInterface* functions;

    // 转发给 functions 代理
    jint DestroyJavaVM()
    { return functions->DestroyJavaVM(this); }
    ...
};




/*
 * C++ 版本的 JNIEnv,内部是对 JNINativeInterface* 的包装
 */


struct _JNIEnv {
    // 相当于 C 版本的 JavaVM
    const struct JNINativeInterface* functions;

    // 转发给 functions 代理
    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); }



};





可以看到,不管是在 C 语言中还是在 C++ 中,JNINativeInterface*JNIInvokeInterface* 这两个结构体指针才是 JavaVM 和 JNIEnv 的实体。不过 C++ 中加了一层包装,在语法上更简洁,例如:

示例程序

// 在 C 语言中,要使用 (*env)->
// 注意看这一句:typedef const struct JNINativeInterface* JNIEnv;
(*env)->FindClass(env, "java/lang/String");

// 在 C++ 中,要使用 env->
// 注意看这一句:jclass FindClass(const char* name)
//{ return functions->FindClass(this, name); }
env->FindClass("java/lang/String");

后文提到的大量 JNI 函数,其实都是定义在 JNINativeInterface *和 JNIInvokeInterface*内部的函数指针。

jni.h

/*
 * JavaVM
 */
struct JNIInvokeInterface {
    // 一系列函数指针
    jint        (*DestroyJavaVM)(JavaVM*);
    jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
    jint        (*DetachCurrentThread)(JavaVM*);
    jint        (*GetEnv)(JavaVM*, void**, jint);
    jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
};



/*
 * JNIEnv
 */
struct JNINativeInterface {
     /**
     获取当前JNI版本信息:
    */
    jint        (*GetVersion)(JNIEnv *);
     
   /*
	定义一个类:类是从某个字节数组buf中读取出来的
	原型:jclass DefineClass(JNIEnv *env, const char *name, jobject loader,
const jbyte *buf, jsize bufLen);
	*/
   
    jclass      (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize);
     /*
    找到某个类:
    函数原型:
    jclass FindClass(JNIEnv *env, const char *name);
    参数name:为类的全限定名
    如String类:"java/lang/String"
    如java.lang.Object[] : "[Ljava/lang/Object;"
    */
     jclass      (*FindClass)(JNIEnv*, const char*);
    


    /*
    获取当前类的父类:
    通常在使用FindClass获取到类之后,再调用这个函数
    */
    jclass      (*GetSuperclass)(JNIEnv*, jclass);


  
 /*
    定义某个类clazz1是否可以安全的强制转换为另外一个类clazz2
    函数原型:
    jboolean IsAssignableFrom(JNIEnv *env, jclass clazz1,jclass clazz2);
	
    */
    jboolean    (*IsAssignableFrom)(JNIEnv*, jclass, jclass);

  
  /*检测是否发生了异常*/
    jboolean    (*ExceptionCheck)(JNIEnv*);

 /*检测是否发生了异常,并返回异常*/
    jthrowable  (*ExceptionOccurred)(JNIEnv*);

   /*打印出异常描述栈*/
    void        (*ExceptionDescribe)(JNIEnv*);

 /*清除异常*/
    void        (*ExceptionClear)(JNIEnv*);

  /* 抛出一个异常 成功返回0,失败返回其他值*/
    jint        (*Throw)(JNIEnv*, jthrowable);

  /* 创建一个新的Exception,并制定message,然后抛出*/
    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);

	/*抛出一个FatalError*/
    void        (*FatalError)(JNIEnv*, const char*);

/*创建一个全局的引用,需要在不使用的时候调用DeleteGlobalRef解除全局引用*/
    jobject     (*NewGlobalRef)(JNIEnv*, jobject);

 /*删除全局引用*/
    void        (*DeleteGlobalRef)(JNIEnv*, jobject);

 /*删除局部引用*/
    void        (*DeleteLocalRef)(JNIEnv*, jobject);

 /*是否是同一个Object*/
    jboolean    (*IsSameObject)(JNIEnv*, jobject, jobject);

	/*创建一个局部引用*/
    jobject     (*NewLocalRef)(JNIEnv*, jobject);


/*在不调用构造函数的情况下,给jclass创建一个Java对象,注意该方法不能用在数组的情况*/
    jobject     (*AllocObject)(JNIEnv*, jclass);


 /*创建一个Object,对于jmethodID参数必须使用GetMethodID获取到构造函数*/   
    jobject     (*NewObject)(JNIEnv*, jclass, jmethodID, ...);
    jobject     (*NewObjectV)(JNIEnv*, jclass, jmethodID, va_list);
    jobject     (*NewObjectA)(JNIEnv*, jclass, jmethodID, const jvalue*);

  /*获取到当前对象的class类型*/
    jclass      (*GetObjectClass)(JNIEnv*, jobject);


  /*某个对象是否是某个类的实现对象,和Java中instanceof类似*/
    jboolean    (*IsInstanceOf)(JNIEnv*, jobject, jclass);


   /*获取某个类的方法类型id,非静态方法
    clazz:类权限定名
    name:为方法名
    sig:为方法签名描述
    原型:jfieldID GetFieldID(JNIEnv *env, jclass clazz,const char *name, const char *sig);
   
    */
    jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
 


 /*调用某个对象的方法
    jobject:对象
    jmethodID:对象的方法
    返回值:jobject
    */
    jobject     (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
    jobject     (*CallObjectMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jobject     (*CallObjectMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);

 

  /*调用某个对象的方法
    jobject:对象
    jmethodID:对象的方法
    返回值:jboolean
    同理后面的CallByteMethod,CallCharMethodV,CallIntMethod只是返回值不一样而已。
    */
 
 jboolean    (*CallBooleanMethod)(JNIEnv*, jobject, jmethodID, ...);
    jboolean    (*CallBooleanMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jboolean    (*CallBooleanMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jbyte       (*CallByteMethod)(JNIEnv*, jobject, jmethodID, ...);
    jbyte       (*CallByteMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jbyte       (*CallByteMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jchar       (*CallCharMethod)(JNIEnv*, jobject, jmethodID, ...);
    jchar       (*CallCharMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jchar       (*CallCharMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jshort      (*CallShortMethod)(JNIEnv*, jobject, jmethodID, ...);
    jshort      (*CallShortMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jshort      (*CallShortMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jint        (*CallIntMethod)(JNIEnv*, jobject, jmethodID, ...);
    jint        (*CallIntMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jint        (*CallIntMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jlong       (*CallLongMethod)(JNIEnv*, jobject, jmethodID, ...);
    jlong       (*CallLongMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jlong       (*CallLongMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jfloat      (*CallFloatMethod)(JNIEnv*, jobject, jmethodID, ...);
    jfloat      (*CallFloatMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jfloat      (*CallFloatMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    jdouble     (*CallDoubleMethod)(JNIEnv*, jobject, jmethodID, ...);
    jdouble     (*CallDoubleMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    jdouble     (*CallDoubleMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);
    void        (*CallVoidMethod)(JNIEnv*, jobject, jmethodID, ...);
    void        (*CallVoidMethodV)(JNIEnv*, jobject, jmethodID, va_list);
    void        (*CallVoidMethodA)(JNIEnv*, jobject, jmethodID, const jvalue*);

 
/*	
	返回一个类的非静态属性id
	原型:jfieldID GetFieldID(JNIEnv *env, jclass clazz,
const char *name, const char *sig);
	参数name:属性的名字
	sig:属性的签名
	*/
    jfieldID    (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);

 /*
    获取当前类的某个属性值    
    同理:对于后面的GetShortField,GetBooleanField,GetByteField等只是属性的类型不一样。
	在使用GetFieldID得到jfieldID属性id后,就可以使用Get<type>Field获取属性值。
    */

  jobject     (*GetObjectField)(JNIEnv*, jobject, jfieldID);
    jboolean    (*GetBooleanField)(JNIEnv*, jobject, jfieldID);
    jbyte       (*GetByteField)(JNIEnv*, jobject, jfieldID);
    jchar       (*GetCharField)(JNIEnv*, jobject, jfieldID);
    jshort      (*GetShortField)(JNIEnv*, jobject, jfieldID);
    jint        (*GetIntField)(JNIEnv*, jobject, jfieldID);
    jlong       (*GetLongField)(JNIEnv*, jobject, jfieldID);
    jfloat      (*GetFloatField)(JNIEnv*, jobject, jfieldID);
    jdouble     (*GetDoubleField)(JNIEnv*, jobject, jfieldID);

  /*
    设置当前类的某个属性值    
    同理:对于后面的BooleanField,SetByteField,SetShortField等只是属性的类型不一样。
	在使用GetFieldID得到jfieldID属性id后,就可以使用Set<type>Field设置对应属性值。
    */
 void        (*SetObjectField)(JNIEnv*, jobject, jfieldID, jobject);
    void        (*SetBooleanField)(JNIEnv*, jobject, jfieldID, jboolean);
    void        (*SetByteField)(JNIEnv*, jobject, jfieldID, jbyte);
    void        (*SetCharField)(JNIEnv*, jobject, jfieldID, jchar);
    void        (*SetShortField)(JNIEnv*, jobject, jfieldID, jshort);
    void        (*SetIntField)(JNIEnv*, jobject, jfieldID, jint);
    void        (*SetLongField)(JNIEnv*, jobject, jfieldID, jlong);
    void        (*SetFloatField)(JNIEnv*, jobject, jfieldID, jfloat);
    void        (*SetDoubleField)(JNIEnv*, jobject, jfieldID, jdouble);

   /*
    获取某个类的静态方法id
    */
    jmethodID   (*GetStaticMethodID)(JNIEnv*, jclass, const char*, const char*);
 

 /*
    调用某个类的静态方法
    同理:后面的CallStaticBooleanMethod,CallStaticByteMethod等方法只是返回类型不一样而已。
    */

jobject     (*CallStaticObjectMethod)(JNIEnv*, jclass, jmethodID, ...);
    jobject     (*CallStaticObjectMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jobject     (*CallStaticObjectMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jboolean    (*CallStaticBooleanMethod)(JNIEnv*, jclass, jmethodID, ...);
    jboolean    (*CallStaticBooleanMethodV)(JNIEnv*, jclass, jmethodID,
                        va_list);
    jboolean    (*CallStaticBooleanMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jbyte       (*CallStaticByteMethod)(JNIEnv*, jclass, jmethodID, ...);
    jbyte       (*CallStaticByteMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jbyte       (*CallStaticByteMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jchar       (*CallStaticCharMethod)(JNIEnv*, jclass, jmethodID, ...);
    jchar       (*CallStaticCharMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jchar       (*CallStaticCharMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jshort      (*CallStaticShortMethod)(JNIEnv*, jclass, jmethodID, ...);
    jshort      (*CallStaticShortMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jshort      (*CallStaticShortMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jint        (*CallStaticIntMethod)(JNIEnv*, jclass, jmethodID, ...);
    jint        (*CallStaticIntMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jint        (*CallStaticIntMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jlong       (*CallStaticLongMethod)(JNIEnv*, jclass, jmethodID, ...);
    jlong       (*CallStaticLongMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jlong       (*CallStaticLongMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jfloat      (*CallStaticFloatMethod)(JNIEnv*, jclass, jmethodID, ...);
    jfloat      (*CallStaticFloatMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jfloat      (*CallStaticFloatMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    jdouble     (*CallStaticDoubleMethod)(JNIEnv*, jclass, jmethodID, ...);
    jdouble     (*CallStaticDoubleMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    jdouble     (*CallStaticDoubleMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);
    void        (*CallStaticVoidMethod)(JNIEnv*, jclass, jmethodID, ...);
    void        (*CallStaticVoidMethodV)(JNIEnv*, jclass, jmethodID, va_list);
    void        (*CallStaticVoidMethodA)(JNIEnv*, jclass, jmethodID, const jvalue*);

 
 //获取静态属性的id
    jfieldID    (*GetStaticFieldID)(JNIEnv*, jclass, const char*,
                        const char*);


/*
	获取某个类的静态属性的值:
	同理:GetStaticBooleanField,GetStaticByteField等后续函数都只是属性的类型不一样而已
	*/
    jobject     (*GetStaticObjectField)(JNIEnv*, jclass, jfieldID);
    jboolean    (*GetStaticBooleanField)(JNIEnv*, jclass, jfieldID);
    jbyte       (*GetStaticByteField)(JNIEnv*, jclass, jfieldID);
    jchar       (*GetStaticCharField)(JNIEnv*, jclass, jfieldID);
    jshort      (*GetStaticShortField)(JNIEnv*, jclass, jfieldID);
    jint        (*GetStaticIntField)(JNIEnv*, jclass, jfieldID);
    jlong       (*GetStaticLongField)(JNIEnv*, jclass, jfieldID);
    jfloat      (*GetStaticFloatField)(JNIEnv*, jclass, jfieldID);
    jdouble     (*GetStaticDoubleField)(JNIEnv*, jclass, jfieldID);

 
 /*
    设置某个类的静态属性的值
    同理:SetStaticObjectField,SetStaticBooleanField只是设置的值属性类型不同罢了*/
    void        (*SetStaticObjectField)(JNIEnv*, jclass, jfieldID, jobject);
    void        (*SetStaticBooleanField)(JNIEnv*, jclass, jfieldID, jboolean);
    void        (*SetStaticByteField)(JNIEnv*, jclass, jfieldID, jbyte);
    void        (*SetStaticCharField)(JNIEnv*, jclass, jfieldID, jchar);
    void        (*SetStaticShortField)(JNIEnv*, jclass, jfieldID, jshort);
    void        (*SetStaticIntField)(JNIEnv*, jclass, jfieldID, jint);
    void        (*SetStaticLongField)(JNIEnv*, jclass, jfieldID, jlong);
    void        (*SetStaticFloatField)(JNIEnv*, jclass, jfieldID, jfloat);
    void        (*SetStaticDoubleField)(JNIEnv*, jclass, jfieldID, jdouble);

   /*
    从一段unicode字符串中创建一个String对象
    原型:jstring NewString(JNIEnv *env, const jchar *unicodeChars,jsize len);
	
	*/
    jstring     (*NewString)(JNIEnv*, const jchar*, jsize);

 
 /*获取String对象的字符串长度,字符串是默认的UNICODE*/
    jsize       (*GetStringLength)(JNIEnv*, jstring);

  /*
    将jstring转换为一个Unicode字符串数组的指针,在调用ReleaseStringChars之前,这个指针都是有效的
    原型:const jchar * GetStringChars(JNIEnv *env, jstring string,jboolean *isCopy);
    */
    const jchar* (*GetStringChars)(JNIEnv*, jstring, jboolean*);

   /*释放一个Unicode字符串数组的指针*/
    void        (*ReleaseStringChars)(JNIEnv*, jstring, const jchar*);

  
 /*创建一个string对象,使用的字符串是UTF-8类型*/
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    /*获取UTF-8类型的jstring对象的长度*/
    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);

 /*
	返回一个string类型的utf-8类型字符串的指针。生命周期是在调用ReleaseStringUTFChars之前。
	原型:const char * GetStringUTFChars(JNIEnv *env, jstring string,jboolean *isCopy);*/
    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);

  /*释放GetStringUTFChars获取到的指针*/
    void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);


    /*获取一个数组对象的长度*/
    jsize       (*GetArrayLength)(JNIEnv*, jarray);


  /*创建一个Object类型的数组对象
    原型:jobjectArray NewObjectArray(JNIEnv *env, jsize length,jclass elementClass, jobject initialElement);
    elementClass:对象类型
    initialElement:对象初始化元素*/
    jobjectArray (*NewObjectArray)(JNIEnv*, jsize, jclass, jobject);
    /*获取某个数组对象索引上的元素,最后一个参数为索引位置*/
    jobject     (*GetObjectArrayElement)(JNIEnv*, jobjectArray, jsize);
    /*设置某个数组对象索引上的元素,倒数第二个参数为索引位置*/
    void        (*SetObjectArrayElement)(JNIEnv*, jobjectArray, jsize, jobject);
	/*创建一个Boolean类型的数组对象,长度为jsize*/
    jbooleanArray (*NewBooleanArray)(JNIEnv*, jsize);

 

/*创建一个Byte类型的数组对象,长度为jsize*/
    jbyteArray    (*NewByteArray)(JNIEnv*, jsize);
    jcharArray    (*NewCharArray)(JNIEnv*, jsize);
    jshortArray   (*NewShortArray)(JNIEnv*, jsize);
    jintArray     (*NewIntArray)(JNIEnv*, jsize);
    jlongArray    (*NewLongArray)(JNIEnv*, jsize);
    jfloatArray   (*NewFloatArray)(JNIEnv*, jsize);
    jdoubleArray  (*NewDoubleArray)(JNIEnv*, jsize);

 
  /*获取Boolean数组对象的第一个对象的地址指针:注意和ReleaseBooleanArrayElements配合使用
    函数原型:NativeType *Get<PrimitiveType>ArrayElements(JNIEnv *env,ArrayType array, 		jboolean *isCopy);
    isCopy:当前返回的数组对象可能是Java数组的一个拷贝对象
    */
    jboolean*   (*GetBooleanArrayElements)(JNIEnv*, jbooleanArray, jboolean*);

 
 /*获取Byte数组对象的第一个对象的地址指针*/
    jbyte*      (*GetByteArrayElements)(JNIEnv*, jbyteArray, jboolean*);



 /*同上*/
    jchar*      (*GetCharArrayElements)(JNIEnv*, jcharArray, jboolean*);
    jshort*     (*GetShortArrayElements)(JNIEnv*, jshortArray, jboolean*);
    jint*       (*GetIntArrayElements)(JNIEnv*, jintArray, jboolean*);
    jlong*      (*GetLongArrayElements)(JNIEnv*, jlongArray, jboolean*);
    jfloat*     (*GetFloatArrayElements)(JNIEnv*, jfloatArray, jboolean*);
    jdouble*    (*GetDoubleArrayElements)(JNIEnv*, jdoubleArray, jboolean*);

 
  //是否数组对象内存
    void        (*ReleaseBooleanArrayElements)(JNIEnv*, jbooleanArray,
                        jboolean*, jint);
    void        (*ReleaseByteArrayElements)(JNIEnv*, jbyteArray,
                        jbyte*, jint);
    void        (*ReleaseCharArrayElements)(JNIEnv*, jcharArray,
                        jchar*, jint);
    void        (*ReleaseShortArrayElements)(JNIEnv*, jshortArray,
                        jshort*, jint);
    void        (*ReleaseIntArrayElements)(JNIEnv*, jintArray,
                        jint*, jint);
    void        (*ReleaseLongArrayElements)(JNIEnv*, jlongArray,
                        jlong*, jint);
    void        (*ReleaseFloatArrayElements)(JNIEnv*, jfloatArray,
                        jfloat*, jint);
    void        (*ReleaseDoubleArrayElements)(JNIEnv*, jdoubleArray,
                        jdouble*, jint);

 

	/*将一个数组区间的值拷贝到一个新的地址空间,然后返回这个地址空间的首地址,最后一个参数为接收首地址用
	函数原型:
	void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array,jsize start, jsize 		len, NativeType *buf);
	*/

 
 void        (*GetBooleanArrayRegion)(JNIEnv*, jbooleanArray,
                        jsize, jsize, jboolean*);
    void        (*GetByteArrayRegion)(JNIEnv*, jbyteArray,
                        jsize, jsize, jbyte*);
    void        (*GetCharArrayRegion)(JNIEnv*, jcharArray,
                        jsize, jsize, jchar*);
    void        (*GetShortArrayRegion)(JNIEnv*, jshortArray,
                        jsize, jsize, jshort*);
    void        (*GetIntArrayRegion)(JNIEnv*, jintArray,
                        jsize, jsize, jint*);
    void        (*GetLongArrayRegion)(JNIEnv*, jlongArray,
                        jsize, jsize, jlong*);
    void        (*GetFloatArrayRegion)(JNIEnv*, jfloatArray,
                        jsize, jsize, jfloat*);
    void        (*GetDoubleArrayRegion)(JNIEnv*, jdoubleArray,
                        jsize, jsize, jdouble*);


   /*设置某个数组对象的区间的值*/
    void        (*SetBooleanArrayRegion)(JNIEnv*, jbooleanArray,
                        jsize, jsize, const jboolean*);
    void        (*SetByteArrayRegion)(JNIEnv*, jbyteArray,
                        jsize, jsize, const jbyte*);
    void        (*SetCharArrayRegion)(JNIEnv*, jcharArray,
                        jsize, jsize, const jchar*);
    void        (*SetShortArrayRegion)(JNIEnv*, jshortArray,
                        jsize, jsize, const jshort*);
    void        (*SetIntArrayRegion)(JNIEnv*, jintArray,
                        jsize, jsize, const jint*);
    void        (*SetLongArrayRegion)(JNIEnv*, jlongArray,
                        jsize, jsize, const jlong*);
    void        (*SetFloatArrayRegion)(JNIEnv*, jfloatArray,
                        jsize, jsize, const jfloat*);
    void        (*SetDoubleArrayRegion)(JNIEnv*, jdoubleArray,
                        jsize, jsize, const jdouble*);

 	/*注册JNI函数*/
    jint        (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
                        jint);
    /*反注册JNI函数*/
    jint        (*UnregisterNatives)(JNIEnv*, jclass);
    /*加同步锁*/
    jint        (*MonitorEnter)(JNIEnv*, jobject);
    /*释放同步锁*/
    jint        (*MonitorExit)(JNIEnv*, jobject);
    /*获取Java虚拟机VM*/
    jint        (*GetJavaVM)(JNIEnv*, JavaVM**);

 

  /*获取uni-code字符串区间的值,并放入到最后一个参数首地址中*/
    void        (*GetStringRegion)(JNIEnv*, jstring, jsize, jsize, jchar*);
     /*获取utf-8字符串区间的值,并放入到最后一个参数首地址中*/
    void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);

 

/*
	1.类似Get/Release<primitivetype>ArrayElements这两个对应函数,都是获取一个数组对象的地址,但是返回是void*,所以是范式编程,可以返回任何对象的首地址,而Get/Release<primitivetype>ArrayElements是指定类型的格式。
	2.在调用GetPrimitiveArrayCcritical之后,本机代码在调用ReleasePrimitiveArray Critical之前不应长时间运行。我们必须将这对函数中的代码视为在“关键区域”中运行。在关键区域中,本机代码不得调用其他JNI函数,或任何可能导致当前线程阻塞并等待另一个Java线程的系统调用。(例如,当前线程不能对另一个Java线程正在编写的流调用read。)*/
    void*       (*GetPrimitiveArrayCritical)(JNIEnv*, jarray, jboolean*);
    void        (*ReleasePrimitiveArrayCritical)(JNIEnv*, jarray, void*, jint);
 


  /*功能类似 Get/ReleaseStringChars,但是功能会有限制:在由Get/ReleaseStringCritical调用包围的代码段中,本机代码不能发出任意JNI调用,或导致当前线程阻塞
    函数原型:const jchar * GetStringCritical(JNIEnv *env, jstring string, jboolean *isCopy);
    */
    const jchar* (*GetStringCritical)(JNIEnv*, jstring, jboolean*);
    void        (*ReleaseStringCritical)(JNIEnv*, jstring, const jchar*);

 

  //创建一个弱全局引用
    jweak       (*NewWeakGlobalRef)(JNIEnv*, jobject);
    //删除一个弱全局引用
    void        (*DeleteWeakGlobalRef)(JNIEnv*, jweak);
	/*检查是否有挂起的异常exception*/
    jboolean    (*ExceptionCheck)(JNIEnv*);

 
 /*
    创建一个ByteBuffer对象,参数address为ByteBuffer对象首地址,且不为空,capacity为ByteBuffe的容量
    函数原型:jobject NewDirectByteBuffer(JNIEnv* env, void* address, jlong capacity);*/
    jobject     (*NewDirectByteBuffer)(JNIEnv*, void*, jlong);
    /*获取一个Buffer对象的首地址*/
    void*       (*GetDirectBufferAddress)(JNIEnv*, jobject);
    /*获取一个Buffer对象的Capacity容量*/
    jlong       (*GetDirectBufferCapacity)(JNIEnv*, jobject);

 

   /*获取jobject对象的引用类型:
    可能为: a local, global or weak global reference等引用类型:
    如下:
    JNIInvalidRefType = 0,
	JNILocalRefType = 1,
	JNIGlobalRefType = 2,
	JNIWeakGlobalRefType = 3*/
    jobjectRefType (*GetObjectRefType)(JNIEnv*, jobject);

 
 

};









看到这里面方法还是挺多的,可以总结为下面几类:Class操作,异常Exception操作,对象字段以及方法操作,类的静态字段以及方法操作,字符串操作,锁操作等等。

三. 数据类型转换

Java 层与 Native 层之间的数据类型转换。

3.1 Java 类型映射(重点理解)

JNI 对于 Java 的基本数据类型(int float long等)和引用数据类型(Object、Class、数组等)的处理方式不同。这个原理非常重要,理解这个原理才能理解后面所有 JNI 函数的设计思路:

java的基础数据类型:

java的基础数据类型会直接转换为 C/C++ 的基础数据类型

 int 类型映射为 jint 类型

基础数据类型在映射时是直接映射,而不会发生数据格式转换。

如:Java char 类型在映射为 jchar 后旧是保持 Java 层的样子,数据长度依旧是 2 个字节,而字符编码依旧是 UNT-16 编码。

java的引用数据类型:

java的对象只会转换为一个 C/C++ 指针

Object 类型映射为 jobject 类型

由于指针指向 Java 虚拟机内部的数据结构,所以不可能直接在 C/C++ 代码中操作对象,而是需要依赖 JNIEnv 环境对象。

另外,为了避免对象在使用时突然被回收,在本地方法返回前,虚拟机会固定(pin)对象,阻止其 GC。

 Java 类型与 JNI 类型的映射关系总结为下表:

Java 类型 JNI 类型 描述 长度(字节)
boolean jboolean unsigned char 1
byte jbyte signed char 1
char jchar unsigned short 2
short jshort signed short 2
int jint signed int 4
long jlong signed long 8
float jfloat signed float 4
double jdouble signed double 8
java.lang.Class jclass Class 类对象 1
java.lang.String jstrting 字符串对象 /
All Object jobject 任何Java对象,或者没有对应java类型的对象 /
java.lang.Throwable jthrowable 异常对象 /
boolean[] jbooleanArray 布尔数组 /
byte[] jbyteArray byte 数组 /
char[] jcharArray char 数组 /
short[] jshortArray short 数组 /
int[] jinitArray int 数组 /
long[] jlongArray long 数组 /
float[] jfloatArray float 数组 /
double[] jdoubleArray double 数组 /
Object[] jobjectArray 任何对象的数组

#define JNI_FALSE  0
#define JNI_TRUE   1

 jni.h:具体映射关系

typedef uint8_t  jboolean; /* unsigned 8 bits */
typedef int8_t   jbyte;    /* signed 8 bits */
typedef uint16_t jchar;    /* unsigned 16 bits */ /* 注意:jchar 是 2 个字节 */
typedef int16_t  jshort;   /* signed 16 bits */
typedef int32_t  jint;     /* signed 32 bits */
typedef int64_t  jlong;    /* signed 64 bits */
typedef float    jfloat;   /* 32-bit IEEE 754 */
typedef double   jdouble;  /* 64-bit IEEE 754 */
typedef jint     jsize;


#ifdef __cplusplus
// 内部的数据结构由虚拟机实现,只能从虚拟机源码看
class _jobject {};
class _jclass : public _jobject {};
class _jstring : public _jobject {};
class _jarray : public _jobject {};
class _jobjectArray : public _jarray {};
class _jbooleanArray : public _jarray {};



// 说明我们接触到到 jobject、jclass 其实是一个指针

typedef _jobject*       jobject;
typedef _jclass*        jclass;
typedef _jstring*       jstring;
typedef _jarray*        jarray;
typedef _jobjectArray*  jobjectArray;
typedef _jbooleanArray* jbooleanArray;
...
#else /* not __cplusplus */
...
#endif /* not __cplusplus */
3.2 字符串类型操作

 java 对象会映射为一个 jobject 指针,

  java.lang.String 字符串类型也会映射为一个 jstring

// 内部的数据结构还是看不到,由虚拟机实现
class _jstring : public _jobject {};
typedef _jstring*       jstring;

struct JNINativeInterface {
    //GetStringUTFChars: String 转换为 UTF-8 字符串
    const char* (*GetStringUTFChars)(JNIEnv*, jstring, jboolean*);
    // 释放 GetStringUTFChars 生成的 UTF-8 字符串
    void        (*ReleaseStringUTFChars)(JNIEnv*, jstring, const char*);
    // 构造新的 String 字符串
    jstring     (*NewStringUTF)(JNIEnv*, const char*);
    // 获取 String 字符串的长度
    jsize       (*GetStringUTFLength)(JNIEnv*, jstring);
    // 将 String 复制到预分配的 char* 数组中
    void        (*GetStringUTFRegion)(JNIEnv*, jstring, jsize, jsize, char*);
};

  

    

由于 Java 与 C/C++ 默认使用不同的字符编码,因此在操作字符数据时,需要特别注意在 UTF-16 和 UTF-8 两种编码之间转换。

Unicode: 统一化字符编码标准,为全世界所有字符定义统一的码点,例如 U+0011;

UTF-8: Unicode 标准的实现编码之一,使用 1~4 字节的变长编码。UTF-8 编码中的一字节编码与 ASCII 编码兼容。

UTF-16: Unicode 标准的实现编码之一,使用 2 / 4 字节的变长编码。UTF-16 是 Java String 使用的字符编码;

以下为 2 种较为常见的转换场景:

① Java String 对象转换为 C/C++ 字符串:

调用 GetStringUTFChars 函数将一个 jstring 指针转换为一个 UTF-8 的 C/C++ 字符串,并在不再使用时调用 ReleaseStringChars 函数释放内存;

构造 Java String 对象:

调用 NewStringUTF 函数构造一个新的 Java String 字符串对象。

见案例代码:

 binding.sampleText2.text=myJNIMethodTest("方明飞")

  external  fun myJNIMethodTest(str:String):String

/**
 * 将 Java String 转换为 C/C++ 字符串
 * jstring str :Java 层传递过来的 String
 *
 * */
extern "C"
JNIEXPORT jstring JNICALL
Java_com_example_mynativiedemo_MainActivity_myJNIMethodTest(JNIEnv *env, jobject thiz, jstring jstr) {

   // 将java层传递过来的字符串 jstr  转为 jni/native/c++ 层字符串
    const char* str =env->GetStringUTFChars(jstr,JNI_FALSE);
    if(!str) {
        // OutOfMemoryError
        LOGD("java层传递过来的字符串参数不存在");
       // return ;
    }

    jsize strSize = env->GetStringLength(jstr);
    LOGE("接收java层传递过来的字符串 jstr  转为 jni/native/c++ 层字符串 = %s, 长度=%d", str, strSize);

  // 释放 GetStringUTFChars 生成的 UTF-8 字符串str
   env->ReleaseStringUTFChars(jstr,str);
   
 // 将 C/C++ 字符串 转换为 Java String
    std::string native_str = "在 jni/Native 层构造 Java String并返回给java层";
    jstring jstr2 = env->NewStringUTF(native_str.c_str());
    return jstr2;
}

对 GetStringUTFChars(this, string, isCopy) 函数的第 3 个参数 isCopy 做解释:它是一个布尔值参数,将决定使用拷贝模式还是复用模式:

  isCopy=JNI_TRUE: 使用拷贝模式,JVM 将拷贝一份原始数据来生成 UTF-8 字符串;

  isCopy=JNI_FALSE:使用复用模式,JVM 将复用同一份原始数据来生成 UTF-8 字符串。复用模式绝不能修改字符串内容,否则 JVM 中的原始字符串也会被修改,打破 String 不可变性。

另外还有一个基于范围的转换函数:GetStringUTFRegion:预分配一块字符数组缓冲区,然后将 String 数据复制到这块缓冲区中。由于这个函数本身不会做任何内存分配,所以不需要调用对应的释放资源函数,也不会抛出 OutOfMemoryError。另外,GetStringUTFRegion 这个函数会做越界检查并抛出 StringIndexOutOfBoundsException 异常。

jstring jStr = ...; // Java 层传递过来的 String
char outbuf[128];
int len = env->GetStringLength(jStr);
env->GetStringUTFRegion(jStr, 0, len, outbuf);

3.3 数组类型操作

与 jstring 的处理方式类似,JNI 规范将 Java 数组定义为 jobject 的派生类 jarray

  • 基础类型数组:定义为 jbooleanArrayjintArray 等;
  • 引用类型数组:定义为 jobjectArray
⑴操作基础类型数组(以 jintArray 为例):

Java 基本类型数组转换为 C/C++ 数组: 调用 GetIntArrayElements 函数将一个 jintArray 指针转换为 C/C++ int 数组;

修改 Java 基本类型数组: 调用 ReleaseIntArrayElements 函数并使用模式 0;

构造 Java 基本类型数组: 调用 NewIntArray 函数构造 Java int 数组。

见案例代码:

     // 演示 Native 操作基本类型数组
        val mIntArray : IntArray = generateIntArray(10)
        Log.e(TAG, "基础类型数组:" + mIntArray.joinToString())
   // 基础类型数组:20, 21, 22, 23, 24, 25, 26, 27, 28, 29


   external fun generateIntArray(size :Int): IntArray

  /**
   * 示例:把java层基本类型数组传递给 jni/native/c++ 层转为c++数组
   * */
extern "C"
JNIEXPORT jintArray JNICALL
Java_com_example_mynativiedemo_MainActivity_generateIntArray(JNIEnv *env, jobject thiz, jint size) {
    // 通过  NewIntArray(this, length)创建 Java int[]
    jintArray   jarr=  env->NewIntArray(size);
    //再通过GetIntArrayElements(this, array, isCopy)  转换为 C/C++ int[]
    jint*  carr =  env->GetIntArrayElements(jarr,JNI_FALSE);
    // 赋值
    for (int i = 0; i < size; i++){
        carr[i]=20+i;
    }
    // 释放资源并回收 防止内存泄漏
    env->ReleaseIntArrayElements(jarr,carr,0);

    // 返回数组给java层
    return  jarr;
}

  对ReleaseIntArrayElements(this, array, elems, mode)第 3 个参数 mode 做解释:它是一个模式参数:

参数 mode 描述
0 将 C/C++ 数组的数据回写到 Java 数组,并释放 C/C++ 数组
JNI_COMMIT 将 C/C++ 数组的数据回写到 Java 数组,并不释放 C/C++ 数组
JNI_ABORT 不回写数据,但释放 C/C++ 数组

另外 JNI 还提供了基于范围函数:GetIntArrayRegionSetIntArrayRegion,使用方法和注意事项和 GetStringUTFRegion 也是类似的,也是基于一块预分配的数组缓冲区。

⑵操作引用类型数组(jobjectArray):

①将 Java 引用类型数组转换为 C/C++ 数组: 不支持!与基本类型数组不同,引用类型数组的元素 jobject 是一个指针,不存在转换为 C/C++ 数组的概念;

②修改 Java 引用类型数组: 调用 SetObjectArrayElement 函数修改指定下标元素;

③构造 Java 引用类型数组: 先调用 FindClass 函数获取 Class 对象,再调用 NewObjectArray 函数构造对象数组。

案例代码:

  // 演示 Native 操作引用类型数组
        val  mStringArray:Array<String>  = generateStringArray(10)
        Log.e(TAG, "引用类型数组:" + mStringArray.joinToString()) 
//引用类型数组:100, 101, 102, 103, 104, 105, 106, 107, 108, 109

external fun generateStringArray(size: Int): Array<String>
/**
 *   示例:操作引用类型数组
 * */
extern "C"
JNIEXPORT jobjectArray JNICALL
Java_com_example_mynativiedemo_MainActivity_generateStringArray(JNIEnv *env, jobject thiz,jint size) {

    // 通过  FindClass(this, name) 获取java的 String Class对象
    jclass jStringClazz =  env->FindClass("java/lang/String");
    // 初始值(可为空)
    jstring  initialStr  =  env->NewStringUTF("初始值");
    // 创建 Java String[]数组
    jobjectArray  jarr =  env->NewObjectArray(size,jStringClazz,initialStr);

    for (int i = 0; i < size; i++){
        char str[5];
        sprintf(str,"%d",100+i);
        jstring jStr = env->NewStringUTF(str);
        env->SetObjectArrayElement(jarr,i,jStr);
    }
    // 返回数组
    return jarr;

}

四. JNI/Native 访问 Java 字段与方法

如何从 Native 层访问 Java 的字段与方法.JNI 首先要找到想访问的字段和方法,这就依靠字段描述符和方法描述符。

4.1 字段描述符与方法描述符

在 Java 源码中定义的字段和方法,在编译后都会按照既定的规则记录在 Class 文件中的字段表和方法表结构中。例如,一个 public String str; 字段会被拆分为字段访问标记(public)、字段简单名称(str)和字段描述符(Ljava/lang/String)。 因此,从 JNI 层访问 Java 层的字段或方法时,首先就是要获取在 Class 文件中记录的简单名称和描述符。

①字段表结构: 包含字段的访问标记、简单名称、字段描述符等信息。例如字段 String str 的简单名称为 str,字段描述符为 Ljava/lang/String;

方法表结构: 包含方法的访问标记、简单名称、方法描述符等信息。例如方法 void fun(); 的简单名称为 fun,方法描述符为 ()V

4.2 描述符规则

字段描述符规则:字段描述符其实就是描述字段的类型,JVM 对每种基础数据类型定义了固定的描述符,而引用类型则是以 L 开头的形式:

Java 类型 描述符
boolean Z(容易误写成B)
byte B
char C
short S
int I
int[] [I   ( 数组以"["开始)
long J(容易误写成L)
floag F
double D
void V
引用类型 以 L 开头 ; 结尾,中间是 / 分隔的包名和类名。例如 String 的字段描述符为 Ljava/lang/String;
String  "Ljava/lang/String;" (引用类型格式为"L包名类名;" 记得要加";")
Object[] "[Ljava/lang/object;"

方法描述符规则:方法描述符其实就是描述方法的返回值类型和参数表类型,参数类型用一对圆括号括起来,按照参数声明顺序列举参数类型,返回值出现在括号后面。例如方法 void fun(); 的简单名称为 fun,方法描述符为 ()V

4.3 JNI 访问 Java 字段/变量

⑴native代码访问 Java 字段的流程分为 2 步:

①通过 jclass 获取字段 ID,例如:Fid = env->GetFieldId(clz, "name", "Ljava/lang/String;");

②通过字段 ID 访问字段,例如:Jstr = env->GetObjectField(thiz, Fid);

⑵Java 字段分为静态字段和实例字段/成员变量

①jni访问java实例字段/成员变量

方法名 作用
GetFieldId 获取实例方法的字段 ID/根据变量名获取target中成员变量的ID/得到一个实例的域的ID
GetField 获取类型为 Type 的实例字段(例如 GetIntField)
GetIntField 根据变量ID获取int变量的值,对应的还有byte,boolean,long等
SetField 设置类型为 Type 的实例字段(例如 SetIntField)
SetIntField 修改int变量的值,对应的还有byte,boolean,long等

 ②jni访问java静态字段/变量

                   

方法名 作用
GetStaticFieldId 获取静态方法的字段 ID/根据变量名获取target中静态变量的ID/得到一个静态的域的ID
GetStaticField 获取类型为 Type 的静态字段(例如 GetStaticIntField)
GetStaticIntField 根据变量ID获取int静态变量的值,对应的还有byte,boolean,long等
SetStaticField 设置类型为 Type 的静态字段(例如 SetStaticIntField)
SetStaticIntField 修改int静态变量的值,对应的还有byte,boolean,long等

实例代码

 MainActivity

class MainActivity : AppCompatActivity() {

private val mName = "初始值"

 companion object {

        //定义一个静态变量sName
        // 如果使用 const val 或 static final 修饰(静态常量),则这个字段变量则无法从 jni/Native 层进行修改
        private val sName = "default"

        fun getsName(): String {
            return sName
        }

     init {
            // 加载本地动态库fmfjni
            System.loadLibrary("fmfjni")
            // 加载本地动态库fmfjni2
            System.loadLibrary("fmfjni2")
        }
}


 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

   //todo  演示 jni/Native层 改变 Java的 静态变量字段sName和实例变量字段mName的值
        Log.e(TAG,"输出改变静态变量字段sName前值="+ getsName())   //输出改变静态变量字段sName前值=default
        Log.e(TAG,"输出改变实例变量字段mName前值=${mName}")  //    输出改变实例变量字段mName前值=初始值
        accessField()
        Log.e(TAG,"输出在jni/native层改变静态变量字段sName后值="+ getsName()) // 输出在jni/native层改变静态变量字段sName后值=fangmingfei
        Log.e(TAG,"输出在jni/native层改变实例变量字段mName的值=${mName}" )  // 输出在jni/native层改变实例变量字段mName的值=方明飞

}

 external fun accessField()


}

fmf_jni.cpp

/**
 * jni/Native层 访问 Java的 静态字段和实例字段
 *
 * */
extern "C"
JNIEXPORT void JNICALL
Java_com_example_mynativiedemo_MainActivity_accessField(JNIEnv *env, jobject thiz) {
     // 通过 GetObjectClass(this, obj) 获取jclass对象(即MainActivity)
     jclass  clz =   env->GetObjectClass(thiz);
    //todo  示例:修改 Java层jclass对象(即MainActivity)的 静态变量字段值 sName的值
     // 获取通过GetStaticFieldID(this, clazz, name, sig)获取 Java层jclass对象(即MainActivity)的静态字段sName的 ID
     jfieldID sFieldId =  env->GetStaticFieldID(clz,"sName","Ljava/lang/String;") ;

    // 访问静态变量字段值 sName
    if(sFieldId){
        // Java 方法的返回值 String 映射为 jstring
        jobject job=  env->GetStaticObjectField(clz,sFieldId);
        jstring   jStr = static_cast<jstring>(job);

        // 将 jstring 转换为 C/C++ 字符串
        const char*   sStr= env->GetStringUTFChars(jStr,JNI_FALSE);
        LOGE("输出java层的静态字段变量=%s", sStr); // 输出java层的静态字段变量=default

        // 释放资源
        env->ReleaseStringUTFChars(jStr, sStr);

        // 构造 Java String 对象(将 C/C++ 字符串转换为 Java String)
        //在啊jni/native层把Java层jclass对象(即MainActivity)的 静态变量字段值 sName改为"fangmingfei" 在传递到java层
       jstring newStr = env->NewStringUTF("fangmingfei");
       if(newStr){
           // jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
           env->SetStaticObjectField(clz,sFieldId,newStr);
       }

    }


    //todo  示例:修改 Java层jclass对象(即MainActivity)的 实例变量字段值 mName的值
    // 获取实例字段 的ID
    jfieldID  mFieldId = env->GetFieldID(clz,"mName", "Ljava/lang/String;");
    // 访问实例字段
    if (mFieldId) {
        jobject job= env->GetObjectField(thiz,mFieldId);
        jstring jStr = static_cast<jstring>(job);
        // 转换为 C/C++ 字符串
        const char* sStr =  env->GetStringUTFChars(jStr,JNI_FALSE);
        LOGE("输出java层的实例变量字段值=%s", sStr); // 输出java层的实例变量字段值=初始值
        // 释放资源
        env->ReleaseStringUTFChars(jStr, sStr);
        //在啊jni/native层把Java层jclass对象(即MainActivity)的 实例变量字段值mName改为"方明飞" 在传递到java层
        jstring newStr = env->NewStringUTF("方明飞");
        if(newStr){
            // jstring 本身就是 Java String 的映射,可以直接传递到 Java 层
            env->SetObjectField(thiz,mFieldId,newStr);
        }

    }

}
4.4 JNI 调用 Java 方法/函数

⑴ jni访问 Java 层的方法,访问流程分为 2 步: 

通过 jclass 获取「方法 ID」,例如:Mid = env->GetMethodID(jclass, "helloJava", "()V");

通过方法 ID 调用方法,例如:env->CallVoidMethod(thiz, Mid);

⑵Java层的 方法分为静态方法和实例方法

  ① jni访问java实例方法/成员方法

方法名 作用
GetMethodId 获取实例方法 ID/根据方法名获取target中成员方法的ID/得到一个实例的方法的ID
CallMethod 调用返回类型为 Type 的实例方法(例如 GetVoidMethod)
CallVoidMethod 执行无返回值成员方法
CallIntMethod 执行int返回值成员方法,对应的还有byte,boolean,long等
CallNonvirtualMethod 调用返回类型为 Type 的父类方法(例如 CallNonvirtualVoidMethod)

②jni访问java静态方法

                               

方法名 作用
GetStaticMethodId 获取静态方法 ID/根据方法名获取target中静态方法的ID/得到一个静态方法的ID
CallStaticMethod 调用返回类型为 Type 的静态方法(例如 CallStaticVoidMethod)
CallStaticVoidMethod 执行无返回值静态方法
CallStaticIntMethod 执行int返回值静态方法,对应的还有byte,boolean,long等

实例代码:

MainActivity

class MainActivity : AppCompatActivity() {

 

 companion object {
 val TAG ="MainActivity"

  //定义一个静态函数
    // Kotlin static 需要使用 @JvmStatic 修饰,否则该方法会放在 Companion 中,而不是直接放在当前类中
     @JvmStatic
     fun sHelloJava() {
         Log.e(TAG, "jni/Native层 调用 Java 静态方法 sHelloJava()")
     }

        

     init {
            // 加载本地动态库fmfjni
            System.loadLibrary("fmfjni")
            // 加载本地动态库fmfjni2
            System.loadLibrary("fmfjni2")
        }
}


 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

 // todo 演示 jni/Native 访问 Java 静态方法和实例方法
        accessMethod()
     // jni/Native层 调用 Java 静态方法 sHelloJava()
     //jin/Native层 调用 Java 实例方法 helloJava()

   
}

 
external fun accessMethod()

 private fun helloJava() {
        Log.e(TAG, "jin/Native层 调用 Java 实例方法 helloJava()")
    }


}

fmf_jni.cpp

/**
 * 调用 Java层的 方法
 *
 * */
extern "C"
JNIEXPORT void JNICALL
Java_com_example_mynativiedemo_MainActivity_accessMethod(JNIEnv *env, jobject thiz) {
    // 通过GetObjectClass(this, obj)获取 jclass(MainActivity)
     jclass clz = env->GetObjectClass(thiz);

    // 示例:调用 Java层的 静态方法sHelloJava()
    // 获取静态方法 ID
     jmethodID  sMethodId = env->GetStaticMethodID(clz,"sHelloJava","()V");
     if(sMethodId){
         // CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)
       env->CallStaticVoidMethod(clz,sMethodId);
     }

    // 示例:调用 Java层的 实例方法 helloJava()
    // 获取实例方法 ID
    jmethodID mMethodId =  env->GetMethodID(clz,"helloJava","()V");

     if(mMethodId){
         // CallVoidMethod(jobject obj, jmethodID methodID, ...)
         env->CallVoidMethod(thiz,mMethodId);
     }

}

4.5 jni访问调用java对象

JNI提供的另外一个功能是在本地代码中使用Java对象。通过使用合适的JNI函数,你可以创建Java对象,get、set 静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。

方法名 作用
GetObjectClass 获取调用对象的类,我们称其为target
FindClass 根据类名获取某个类,我们称其为target
IsInstanceOf 判断一个类是否为某个类型
IsSameObject 是否指向同一个对象
NewObject 创建对象

 4.6 jni访问调用java数组

JNI通过JNIEnv提供的操作Java数组的功能。它提供了两个函数:一个是操作java的简单型数组的,另一个是操作对象类型数组的。

因为速度的原因,简单类型的数组作为指向本地类型的指针暴露给本地代码。因此,它们能作为常规的数组存取。这个指针是指向实际的Java数组或者Java数组的拷贝的指针。另外,数组的布置保证匹配本地类型。

为了存取Java简单类型的数组,你就要要使用GetXXXArrayElements函数(见B),XXX代表了数组的类型。这个函数把Java数组看成参数,返回一个指向对应的本地类型的数组的指针。

JNI数组存取函数

函数 Java数组类型 本地类型
GetBooleanArrayElements jbooleanArray jboolean
GetByteArrayElements jbyteArray jbyte
GetCharArrayElements jcharArray jchar
GetShortArrayElements jshortArray jshort
GetIntArrayElements jintArray jint
GetLongArrayElements jlongArray jlong
GetFloatArrayElements jfloatArray jfloat
GetDoubleArrayElements jdoubleArray jdouble

当你对数组的存取完成后,要确保调用相应的ReleaseXXXArrayElements函数,参数是对应Java数组和GetXXXArrayElements返回的指针。如果必要的话,这个释放函数会复制你做的任何变化(这样它们就反射到java数组),然后释放所有相关的资源。

为了使用java对象的数组,你必须使用GetObjectArrayElement函数和SetObjectArrayElement函数,分别去get,set数组的元素。GetArrayLength函数会返回数组的长度。

 4.6 jni创建引用

  

方法名 作用
NewGlobalRef 创建全局引用
NewWeakGlobalRef 创建弱全局引用
NewLocalRef 创建局部引用
DeleteGlobalRef 释放全局对象,引用不主动释放会导致内存泄漏
DeleteLocalRef 释放局部对象,引用不主动释放会导致内存泄漏

.

4.7缓存 ID

访问 Java 层字段或方法时,需要先利用字段名 / 方法名和描述符进行检索,获得 jfieldID / jmethodID。这个检索过程比较耗时,优化方法是将字段 ID 和方法 ID 缓存起来,减少重复检索。

提示: 从不同线程中获取同一个字段或方法 的 ID 是相同的,缓存 ID 不会有多线程问题。

缓存字段 ID 和 方法 ID 的方法主要有 2 种:

使用时缓存: 使用时缓存是指在首次访问字段或方法时,将字段 ID 或方法 ID 存储在静态变量中。这样将来再次调用本地方法时,就不需要重复检索 ID 了

类初始化时缓存: 静态初始化时缓存是指在 Java 类初始化的时候,提前缓存字段 ID 和方法 ID。可以选择在 JNI_OnLoad 方法中缓存,也可以在加载 so 库后调用一个 native 方法进行缓存。

两种缓存 ID 方式的主要区别在于缓存发生的时机和时效性:

时机不同: 使用时缓存是延迟按需缓存,只有在首次访问 Java 时才会获取 ID 并缓存,而类初始化时缓存是提前缓存;

时效性不同: 使用时缓存的 ID 在类卸载后失效,在类卸载后不能使用,而类加载时缓存在每次加载 so 动态库时会重新更新缓存,因此缓存的 ID 是保持有效的。

4.8案例实战

上面我们在实现setValueOfNumByJNI()时,可以看到c++里面的方法名很长Java_com_example_mynativiedemo_MainActivity_setValueOfNumByJNI,这是jni静态注册的方式,按照jni规范的命名规则进行查找,格式为Java_类路径_方法名,这种方式在应用层开发用的比较广泛,因为Android Studio默认就是用这种方式,而在framework当中几乎都是采用动态注册的方式来实现java和c/c++的通信。比如之前研究过的《Android MediaPlayer源码分析》,里面就是采用的动态注册的方式。

在Android中,当程序在Java层运行System.loadLibrary("fmfjni");这行代码后,程序会去载入libfmfjni.so文件。于此同时,产生一个Load事件,这个事件触发后,程序默认会在载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,与Load事件相对,在载入的.so文件被卸载时,Unload事件被触发。此时,程序默认会去载入的.so文件的函数列表中查找JNI_OnLoad函数并执行,然后卸载.so文件。因此开发者经常会在JNI_OnLoad中做一些初始化操作,动态注册就是在这里进行的,使用env->RegisterNatives(clazz, gMethods, numMethods)

参数1:Java对应的类

参数2:JNINativeMethod数组

 参数3:JNINativeMethod数组的长度,也就是要注册的方法的个数

typedef struct {
    const char* name; //java中要注册的native方法名
    const char* signature;//方法签名
    void*       fnPtr;//对应映射到C/C++中的函数指针
} JNINativeMethod;

相比静态注册,动态注册的灵活性更高,如果修改了native函数所在类的包名或类名,仅调整native函数的签名信息即可。上述案例改为动态注册,java代码不需要更改,只需要更改native代码

MainActivity

class MainActivity : AppCompatActivity() {

  private lateinit var binding: ActivityMainBinding
  private   var testJniCpp3: TextView?=null


override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
 binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

 testJniCpp3 = binding.testJniCpp3
        testJniCpp3?.setOnClickListener {
             try {
                setValueOfNumByJNI()
            } catch (e: Throwable) {
                e.printStackTrace()
                Log.d(MainActivity.TAG, "native error: " + e.message)
            }
        }

}


  external fun setValueOfNumByJNI()

}

fmf_jni.cpp

/**
 * 尽管java中的setValueOfNumByJNI()方法没有参数,但cpp中仍然有两个参数,
 *  参数一:JNIEnv* env表示指向可用JNI函数表的接口指针,所有跟jni相关的操作都需要通过env来完成
 *    参数二:jobject是调用该方法的java对象,这里是MainActivity调用的,所以thiz代表MainActivity
 *    方法名:Java_包名_类名_方法名
 * */

extern "C"
JNIEXPORT void JNICALL
Java_com_example_mynativiedemo_MainActivity_setValueOfNumByJNI(JNIEnv *env, jobject thiz) {
    //todo 获取MainActivity的class对象
    jclass clazz =  env->GetObjectClass(thiz);
    //todo 获取MainActivity中num变量id
    //todo   参数1:clazz=MainActivity的class对象  参数2:变量名称"num"  参数3:变量类型"I",
    jfieldID numFieldId  =  env->GetFieldID(clazz,"num","I");
    //todo 根据变量id获取num的值
    jint oldValue =  env->GetIntField(thiz,numFieldId);
    //todo 将num变量的值+1
    env->SetIntField(thiz,numFieldId,oldValue+1);
    //todo 重新获取MainActivity中num变量值
    jint  num  =   env->GetIntField(thiz,numFieldId);
    //todo 获取MainActivity的TextView  testJniCpp3 变量id
    jfieldID tvFieldId = env->GetFieldID(clazz,"testJniCpp3", "Landroid/widget/TextView;");
    //todo 根据变量id获取textview对象
    jobject  tvObject =  env->GetObjectField(thiz,tvFieldId);
     //todo 获取textview的class对象
    jclass   tvClass =    env->GetObjectClass(tvObject);
    //获取TextView的setText方法ID
    //todo  参数1:textview的class对象tvClass     参数2:方法名称"setText"  参数3:方法参数类型和返回值类型  "([CII)V"
    jmethodID methodId = env->GetMethodID(tvClass,"setText", "([CII)V");

    //获取setText(CharSequence text)所需的参数
    //先将num转化为jstring
    char buf[64];
    sprintf(buf,"%d",num);
    jstring pJstring =  env->NewStringUTF(buf);
    const char* value  =  env->GetStringUTFChars(pJstring,JNI_FALSE);

    //创建一个char类型的数组,长度为字符串num的长度
    jcharArray charArray   = env->NewCharArray(strlen(value));

    //开辟jchar内存空间
    void*  pVoid  = calloc(strlen(value),sizeof(jchar));
   jchar*   pArray =  (jchar *)pVoid;
    //将num的值缓冲到内存空间中
    for (int i = 0; i < strlen(value); ++i){
        LOGE("value+i=%p",value+i);
        LOGE("*(value+i)=%c",*(value+i));
        //给数组的每个角标存储元素值
        *(pArray+i) = *(value+i);
    }
//将缓冲的值写入到char数组charArray 中
 env->SetCharArrayRegion(charArray,0, strlen(value),pArray);
    //调用setText方法
    env->CallVoidMethod(tvObject,methodId,charArray,0,env->GetArrayLength(charArray));
    //释放资源
    env->ReleaseCharArrayElements(charArray, env->GetCharArrayElements(charArray, JNI_FALSE), 0);
    free(pArray);
    pArray = NULL;

}

五:JNI 中的对象引用管理

5.1 Java 和 C/C++ 中对象内存回收区别(重点理解)

在讨论 JNI 中的对象引用管理,我们先回顾一下 Java 和 C/C++ 在对象内存回收上的区别:

Java: 对象在堆 / 方法区上分配,由垃圾回收器扫描对象可达性进行回收。如果使用局部变量指向对象,在不再使用对象时可以手动显式置空,也可以等到方法返回时自动隐式置空。如果使用全局变量(static)指向对象,在不再使用对象时必须手动显式置空。

C/C++: 栈上分配的对象会在方法返回时自动回收,而堆上分配的对象不会随着方法返回而回收,也没有垃圾回收器管理,因此必须手动回收(free/delete)。

而 JNI 层作为 Java 层和 C/C++ 层之间的桥接层,那么它就会兼具两者的特点:

局部 Java 对象引用: 在 JNI 层可以通过 NewObject 等函数创建 Java 对象,并且返回对象的引用,这个引用就是 Local 型的局部引用。对于局部引用,可以通过 DeleteLocalRef 函数手动显式释放(这类似于在 Java 中显式置空局部变量),也可以等到函数返回时自动释放(这类似于在 Java 中方法返回时隐式置空局部变量);

全局 Java 对象引用: 由于局部引用在函数返回后一定会释放,可以通过 NewGlobalRef 函数将局部引用升级为 Global 型全局变量,这样就可以在方法使用对象(这类似于在 Java 中使用 static 变量指向对象)。在不再使用对象时必须调用 DeleteGlobalRef 函数释放全局引用(这类似于在 Java 中显式置空 static 变量)。

提示: 我们这里所说的 ”置空“ 只是将指向变量的值赋值为 null,而不是回收对象,Java 对象回收是交给垃圾回收器处理的。

5.2 JNI 中的三种引用
局部引用:

局部引用可以直接使用:NewLocalRef来创建,虽然局部引用可以在跳出作用域后被回收,但是还是希望在不使用的时候调用DeleteLocalRef来手动回收掉。

大部分 JNI 函数会创建局部引用,局部引用只有在创建引用的本地方法返回前有效,也只在创建局部引用的线程中有效。在方法返回后,局部引用会自动释放,也可以通过 DeleteLocalRef 函数手动释放;

全局引用:

局部引用要跨方法和跨线程必须升级为全局引用,全局引用通过 NewGlobalRef 函数创建,不再使用对象时必须通过 DeleteGlobalRef 函数释放。

全局引用,多个地方需要使用的时候就会创建一个全局的引用(NewGlobalRef方法创建),全局引用只有在显示调用DeleteGlobalRef的时候才会失效,不然会一直存在与内存中,这点一定要注意。

弱全局引用:

弱引用与全局引用类似,区别在于弱全局引用不会持有强引用,因此不会阻止垃圾回收器回收引用指向的对象。弱全局引用通过 NewGlobalWeakRef 函数创建,不再使用对象时必须通过 DeleteGlobalWeakRef 函数释放。

弱引用可以使用全局声明的方式,区别在于:弱引用在内存不足或者紧张的时候会自动回收掉,可能会出现短暂的内存泄露,但是不会出现内存溢出的情况,建议不需要使用的时候手动调用DeleteWeakGlobalRef释放引用。

示例程序

// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
env->DeleteLocalRef(localRefClz);

// 全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
env->DeleteGlobalRef(globalRefClz);

// 弱全局引用
jclass weakRefClz = env->NewWeakGlobalRef(localRefClz);
env->DeleteGlobalWeakRef(weakRefClz);

5.3 JNI 引用的实现原理

在 JavaVM 和 JNIEnv 中,会分别建立多个表管理引用:

⑴JavaVM 内有 globals 和 weak_globals 两个表管理全局引用和弱全局引用。由于 JavaVM 是进程共享的,因此全局引用可以跨方法和跨线程共享;

⑵JavaEnv 内有 locals 表管理局部引用,由于 JavaEnv 是线程独占的,因此局部引用不能跨线程。另外虚拟机在进入和退出本地方法通过 Cookie 信息记录哪些局部引用是在哪些本地方法中创建的,因此局部引用是不能跨方法的。

5.4 比较引用是否指向相同对象

使用 JNI 函数 IsSameObject 判断两个引用是否指向相同对象(适用于三种引用类型),返回值为 JNI_TRUE 时表示相同,返回值为 JNI_FALSE 表示不同。

示例程序

jclass localRef = ...
jclass globalRef = ...
bool isSampe = env->IsSamObject(localRef, globalRef)

当引用与 NULL 比较时含义略有不同:

  • 局部引用和全局引用与 NULL 比较: 用于判断引用是否指向 NULL 对象;
  • 弱全局引用与 NULL 比较: 用于判断引用指向的对象是否被回收。

六:JNI 中的异常处理

6.1 JNI 的异常处理机制(重点理解)

JNI 中的异常机制与 Java 和 C/C++ 的处理机制都不同:

Java 处理异常

程序使用关键字 throw 抛出异常,虚拟机会中断当前执行流程,转而去寻找匹配的 catch{} 块,或者继续向外层抛出寻找匹配 catch {} 块。

void updateName(String name) throws Exception {
    this.name = name;
    Log.d("HelloJni","你成功调用了HelloCallBack的方法:updateName");
    throw new Exception("dead");
}

JNI/native

处理异常

程序使用 JNI 函数 ThrowNew 抛出异常,程序不会中断当前执行流程,而是返回 Java 层后,虚拟机才会抛出这个异常。

在 JNI 层出现异常时,有 2 种处理选择:

方法 1

native层自行处理这个异常

通过 JNI 函数 ExceptionClear 清除这个异常,再执行异常处理程序(这类似于在 Java 中 try-catch 处理异常)。需要注意的是,当异常发生时,必须先处理-清除异常,再执行其他 JNI 函数调用。 因为当运行环境存在未处理的异常时,只能调用 2 种 JNI 函数:异常护理函数和清理资源函数。

方法 2

直接 return 当前方法,jni/native层抛出异常给Java层处理 

  

JNI 提供了以下与异常处理相关的 JNI 函数:

方法名 作用
Throw 向 Java 层抛出异常;
ThrowNew 向 Java 层抛出自定义异常;
ExceptionDescribe 打印异常描述信息
ExceptionOccurred 检查当前环境是否发生异常,如果存在异常则返回该异常对象;
ExceptionCheck 检查当前环境是否发生异常,如果存在异常则返回 JNI_TRUE,否则返回 JNI_FALSE;
ExceptionClear 清除当前环境的异常。

示例程序

struct JNINativeInterface {
    // 抛出异常
    jint        (*ThrowNew)(JNIEnv *, jclass, const char *);
    // 检查异常
    jthrowable  (*ExceptionOccurred)(JNIEnv*);
    // 检查异常
    jboolean    (*ExceptionCheck)(JNIEnv*);
    // 清除异常
    void        (*ExceptionClear)(JNIEnv*);
};

MainActivity.kt

class MainActivity : AppCompatActivity() {

 override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

  val person = Person("方明飞", 30)

        jni_native_handle_exception(person)
      //在java层捕获native层抛出的异常
     try{
        native_throw_exception_toJava_handle(person)
     }catch ( e:Exception){
         e.printStackTrace();
         Log.d(MainActivity.TAG, "java层捕获native层抛出的异常:" + e.message)
   }



}

}

  Person.java

public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
}

  fmf_jni.cpp


/**
 * native层自行处理异常
 *
 * */
extern "C"
JNIEXPORT void JNICALL
Java_com_example_mynativiedemo_MainActivity_jni_1native_1handle_1exception(JNIEnv *env,
                                                                           jobject thiz,jobject person) {
    //todo 获取Person的class对象
    jclass j_class  = env->GetObjectClass(person);

    //获取Person的getName()方法ID
    //todo  参数1:Person的class对象j_class    参数2:方法名称"getName"  参数3:方法参数类型和返回值类型  "()Ljava/lang/String;"
    jmethodID  j_methodId = env->GetMethodID(j_class,"getAge2", "()I");

    jboolean   hasException =env->ExceptionCheck();
     if(hasException==JNI_TRUE){
         //打印异常,同Java中的printExceptionStack;
         env->ExceptionDescribe();
         //清除当前异常
         env->ExceptionClear();
         LOGD("native occur a error itself handle ");
     }else{

         LOGE("ok");
     }


}


/**
 * native层抛出异常给Java层处理:
 * */

extern "C"
JNIEXPORT void JNICALL
Java_com_example_mynativiedemo_MainActivity_native_1throw_1exception_1toJava_1handle(JNIEnv *env, jobject thiz, jobject person) {
      //todo 获取Person的class对象
    jclass j_class  = env->GetObjectClass(person);

    //获取Person的getName()方法ID
    //todo  参数1:Person的class对象j_class    参数2:方法名称"getName"  参数3:方法参数类型和返回值类型  "()Ljava/lang/String;"
    jmethodID  j_methodId = env->GetMethodID(j_class,"getName2", "()Ljava/lang/String;" );

    /*检测是否有异常*/
    jthrowable throwable = env->ExceptionOccurred();
    if(throwable){

        //打印异常,同Java中的printExceptionStack;
        env->ExceptionDescribe();

        //todo 清除当前异常 否则会崩溃
        env->ExceptionClear();
        /* 抛出异常给java层,让Java层去铺货处理 */

        jclass exceptionClz = env->FindClass("java/lang/Exception");
        std::string header = "找不到该方法getName2";
        env->ThrowNew(exceptionClz, header.c_str() );

        //抛完异常后必须清除异常,否则会导致VM崩溃
        //env->ExceptionClear();  //todo 如果把这行代码注释掉了,就必须在java层try-catch  捕获native层抛出的异常
        LOGD("native occur a error throw to java handle ");
        return;
    }



    jobject object =  env->CallObjectMethod(person,j_methodId);
    jstring name = static_cast<jstring>(object);
    const char*  nameString  =  env->GetStringUTFChars(name,JNI_FALSE);
    jsize nameSize = env->GetStringLength(name);
    LOGD("the name is %s, the name size is %d", nameString, nameSize);
}


6.2 检查是否发生异常的方式

异常处理的步骤我懂了,由于虚拟机在遇到 ThrowNew 时不会中断当前执行流程,那我怎么知道当前已经发生异常呢?有 2 种方法:

方法 1: 通过函数返回值错误码,大部分 JNI 函数和库函数都会有特定的返回值来标示错误,例如 -1、NULL 等。在程序流程中可以多检查函数返回值来判断异常。

方法 2: 通过 JNI 函数 ExceptionOccurredExceptionCheck 检查当前是否有异常发生。

七. JNI 与多线程

7.1 不能跨线程的引用

在 JNI 中,有 2 类引用是无法跨线程调用的,必须时刻谨记:

JNIEnv: JNIEnv 只在所在的线程有效,在不同线程中调用 JNI 函数时,必须使用该线程专门的 JNIEnv 指针,不能跨线程传递和使用。通过 AttachCurrentThread 函数将当前线程依附到 JavaVM 上,获得属于当前线程的 JNIEnv 指针。如果当前线程已经依附到 JavaVM,也可以直接使用 GetEnv 函数。

JNIEnv * env_child;
vm->AttachCurrentThread(&env_child, nullptr);
// 使用 JNIEnv*
vm->DetachCurrentThread();

局部引用: 局部引用只在创建的线程和方法中有效,不能跨线程使用。可以将局部引用升级为全局引用后跨线程使用。

// 局部引用
jclass localRefClz = env->FindClass("java/lang/String");
// 释放全局引用(非必须)
env->DeleteLocalRef(localRefClz);
// 局部引用升级为全局引用
jclass globalRefClz = env->NewGlobalRef(localRefClz);
// 释放全局引用(必须)
env->DeleteGlobalRef(globalRefClz);

7.2 监视器同步

在 JNI 中也会存在多个线程同时访问一个内存资源的情况,此时需要保证并发安全。在 Java 中我们会通过 synchronized 关键字来实现互斥块(背后是使用监视器字节码),在 JNI 层也提供了类似效果的 JNI 函数:

MonitorEnter: 进入同步块,如果另一个线程已经进入该 jobject 的监视器,则当前线程会阻塞;

MonitorExit: 退出同步块,如果当前线程未进入该 jobject 的监视器,则会抛出 IllegalMonitorStateException 异常。

jni.h

struct JNINativeInterface {
    jint        (*MonitorEnter)(JNIEnv*, jobject);
    jint        (*MonitorExit)(JNIEnv*, jobject);
}

示例程序

// 进入监视器
if (env->MonitorEnter(obj) != JNI_OK) {
    // 建立监视器的资源分配不成功等
}


// 此处为同步块
if (env->ExceptionOccurred()) {
    // 必须保证有对应的 MonitorExit,否则可能出现死锁
    if (env->MonitorExit(obj) != JNI_OK) {
        ...
    };
    return;
}

// 退出监视器
if (env->MonitorExit(obj) != JNI_OK) {
    ...
};

7.3 等待与唤醒

JNI 没有提供 Object 的 wati/notify 相关功能的函数,需要通过 JNI 调用 Java 方法的方式来实现:

示例程序

static jmethodID MID_Object_wait;
static jmethodID MID_Object_notify;
static jmethodID MID_Object_notifyAll;



void
JNU_MonitorWait(JNIEnv *env, jobject object, jlong timeout) {
    env->CallVoidMethod(object, MID_Object_wait, timeout);
}

void
JNU_MonitorNotify(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notify);
}

void
JNU_MonitorNotifyAll(JNIEnv *env, jobject object) {
    env->CallVoidMethod(object, MID_Object_notifyAll);
}

7.4 创建线程的方法

在 JNI 开发中,有两种创建线程的方式:

⑴ 方法 1 - 通过 Java API 创建: 使用我们熟悉的 Thread#start() 可以创建线程,优点是可以方便地设置线程名称和调试;

⑵ 方法 2 - 通过 C/C++ API 创建: 使用 pthread_create()std::thread 也可以创建线程

示例程序

void *thr_fn(void *arg) {
    printids("new thread: ");
    return NULL;
}


int main(void) {
    pthread_t ntid;
    // 第 4 个参数将传递到 thr_fn 的参数 arg 中
    err = pthread_create(&ntid, NULL, thr_fn, NULL);
    if (err != 0) {
        printf("can't create thread: %s\n", strerror(err));
    }
    return 0;
}

八. 通用 JNI 开发模板

下面给出一个简单的 JNI 开发模板,将包括上文提到的一些比较重要的知识点。

程序逻辑很简单:Java 层传递一个媒体文件路径到 Native 层后,由 Native 层播放媒体并回调到 Java 层。为了程序简化,所有真实的媒体播放代码都移除了,只保留模板代码。

⑴Java 层:start() 方法开始,调用 startNative() 方法进入 Native 层;

⑵Native 层: 创建 MediaPlayer 对象,其中在子线程播放媒体文件,并通过预先持有的 JavaVM 指针获取子线程的 JNIEnv 对象回调到 Java 层 onStarted() 方法。

九:静态注册 JNI 函数

9.1 静态注册使用方法

静态注册采用的是基于「约定」的命名规则,通过 javah 可以自动生成 native 方法对应的函数声明(IDE 会智能生成,不需要手动执行命令)。例如:

HelloWorld.java(在Java中声明native方法)

package com.xurui.hellojni;

public class HelloWorld {
    public native void sayHi();
}

native-lib.cpp(在native层新建一个C/C++文件,并创建对应的方法)

extern "C"
JNIEXPORT void JNICALL
Java_com_xurui_hellojni_HelloWorld_sayHi(JNIEnv *env, jobject thiz) {
    LOGD("%s", "Java 调用 Native 方法 sayHi:HelloWorld!");
}

静态注册的命名规则分为「无重载」和「有重载」2 种情况:无重载时采用「短名称」规则,有重载时采用「长名称」规则。

短名称规则(short name): Java_[类的全限定名 (带下划线)]_[方法名] ,其中类的全限定名中的 . 改为 _

长名称规则(long name): 在短名称的基础上后追加两个下划线(__)和参数描述符,以区分函数重载。

这里解释下为什么有重载的时候要拼接参数描述符的方式来呢?因为 C 语言是没有函数重载的,无法根据参数来区分函数重载,所以才需要拼接后缀来消除重载。

 9.2 静态注册原理分析

现在,我们来分析下静态注册匹配 JNI 函数的执行过程。由于没有找到直接相关的资料和函数调用入口,我是以 loadLibrary() 加载 so 库的执行流程为线索进行分析的,最终定位到 FindNativeMethod() 这个方法,从内容看应该没错。

十. 动态注册 JNI 函数

静态注册是在首次调用 Java native 方法时搜索对应的 JNI 函数,而动态注册则是提前手动建立映射关系,并且不需要遵守静态注册的 JNI 函数命名规则。

10.1 动态注册使用方法

动态注册需要使用 RegisterNatives(...) 函数,jni注册native方法,其定义在 jni.h 文件中:

jni.h

struct JNINativeInterface {
		// 注册
		// 参数二:Java Class 对象的表示
		// 参数三:JNINativeMethod 结构体数组
		// 参数四:JNINativeMethod 结构体数组长度
		jint        (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*, jint);
		// 注销
		// 参数二:Java Class 对象的表示
    jint        (*UnregisterNatives)(JNIEnv*, jclass);
};

typedef struct {
    const char* name;      // Java 方法名
    const char* signature; // Java 方法描述符
    void*       fnPtr;     // JNI 函数指针
} JNINativeMethod;
10.2 动态注册原理分析

RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针,因此动态注册的 native 方法调用效率更高。此外,还能减少生成 so 库文件中导出符号的数量,则能够优化 so 库文件的体积。更多信息见 Android 对 so 体积优化的探索与实践 中 “精简动态符号表” 章节。

10.3. 注册 JNI 函数的时机

注册 JNI 函数的时机,主要分为 3 种:

注册时机 注册方式 描述
1、在第一次调用该 native 方法时 静态注册 虚拟机会在 JNI 函数库中搜索函数指针并记录下来,后续调用不需要重复搜索
2、加载 so 库时 动态注册 加载 so 库时会自动回调 JNI_OnLoad 函数,在其中调用 RegisterNatives 注册
3、提前注册 动态注册 在加载 so 库后,调用该 native 方法前,通过静态注册的 native 函数触发 RegisterNatives 注册。例如在 App 启动时,很多系统源码会提前做一次注册

10.4. 总结

静态注册和动态注册的区别:

⑴静态注册基于命名约定建立映射关系,而动态注册通过 JNINativeMethod 结构体建立映射关系;

⑵ 静态注册在首次调用该 native 方法搜索并建立映射关系,而动态注册会在调用该 native 方法前建立映射关系;

⑶ 静态注册需要将所有 JNI 函数暴露到动态符号表,而动态注册不需要暴露到动态符号表,可以精简 so 文件体积。

(4)动态注册和静态注册最终都可以将native方法注册到虚拟机中,推荐使用动态注册,更不容易写错,静态注册每次增加一个新的方法都需要查看原函数类的包名。

10.5  静态注册和动态注册的案例

      见我的项目工程:MyJniNativeStudyDemo的模块:JniDynamicRegistrantionDemo

十一:NDK实战

1.native层调用Java层的类的字段和方法

2.native层调用第三方so库的api

  见我的项目工程:MyJniNativeStudyDemo的模块:thirdsoCall

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_33552379/article/details/132906140

智能推荐

WINCE 6.0 5.0 区别_win ce5.0 ce6.0区别-程序员宅基地

文章浏览阅读2.6k次。CE 6.0 R2对CE 6.0的功能增加:1 Core OS2 IE3 Media Player4 RDP 对于从事廋客房端开发的人员有很大的帮助5 VoIP6 Web Services一) CE OS的改变,只有两次1 2.0->3.02 5.0->6.0但是4.2到5.0驱动变化较大,导致从4.2到5.0的移植需要很长的时间二) CE 6.0与5.0的主要区别1 CE6.0的Ker_win ce5.0 ce6.0区别

图像meta信息中XMP[drone-dji]如何获取_xmp.drone-dji.lrftargetdistance-程序员宅基地

文章浏览阅读1.4k次。在处理大疆无人机拍的图像时,官方给的图像处理指南里出现了这么一句:于是去网上查找如何读取图像meta信息,找到了许多花里胡哨的方法,比如python代码、java代码、网站代查之类,最终我发现,最简便的方法就是:用写字板的方式打开图像,然后查找”xmp“ ,需要的参数就全能找到了。唯一的不足就是这个方法的效率太低了,写字板打开tif文件奇慢无比。..._xmp.drone-dji.lrftargetdistance

云服务器(阿里云)安装kafka及相关报错处理(WARN Connection request from old client /58.247.201.56:31365; will be dropp)-程序员宅基地

文章浏览阅读4.2k次。云服务器安装kafka,部署zookeeper时有如下注意点:1、在云服务器安全组中开放:2181、9092端口2、zookeeper.connect改成公网IP3、listeners=PLAINTEXT:// 必须填内网IPlisteners=PLAINTEXT://**.**.**.**:90924、配置外部代理地址必须填公网IPadvertised.listeners=PLAINTEXT://**.**.**.**:9092advertised.host.name=*.._connection request from old client

【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed_failed to initialize pool: this connection has bee-程序员宅基地

文章浏览阅读1.2k次,点赞19次,收藏22次。本文从项目需求出发到项目最终发版提测,讲述一下项目中遇到的问题(MyBatis数据库厂商适配、查看数据库链接、连接池失效等)以及打怪升级过程(思路),文章中会提到涉及到的坑以及解决办法。相信看完,多少会给你提供一些价值。_failed to initialize pool: this connection has been closed.

android listpreference 自定义,自定义android preference组件-程序员宅基地

文章浏览阅读179次。e() {return mDisableDependentsState;}public void setDisableDependentsState(boolean disableDependentsState) {mDisableDependentsState = disableDependentsState;}@Overrideprotected Object onGetDefaultValu..._android 自定义listpreference

射频MOS管和三极管优缺点对比_mos管比起三极管有什么优势-程序员宅基地

文章浏览阅读5.1k次。MOS管优点:1.具有良好的温度特性。2.具有良好的噪声特性。3.输入阻抗高。4.MOS管的漏极电流具有二次函数特性,三极管的集电极电流是指数形式。5.MOS管的上限频率远远超过三极管的上限工作频率。6.MOS管功耗较小。MOS管缺点:1.增益通常较低。2.输入阻抗高,导致匹配网络难设计。3.相对于三极管,MOS管的功率容量偏低..._mos管比起三极管有什么优势

随便推点

java日期工具类、日期格式校验、日期格式化_java校验日期格式-程序员宅基地

文章浏览阅读6.8k次,点赞4次,收藏22次。java项目中经常会使用到对日期进行格式校验、格式化日期、LocalDate与Date互转等等,以下整理一份经常会使用到的日期操作相关的方法。_java校验日期格式

【TCP/IP】 以太网流量控制------pause流控_流控 pause 发送时机 计算-程序员宅基地

文章浏览阅读7.4k次,点赞10次,收藏94次。文章目录一、以太网的流量控制二、pause流控的原理和实现1.pause流控原理2.pause消息格式3.pause流控处理逻辑4.pause流控芯片上的实现三、pause流控的作用与副作用1.pause流控的作用2.pause流控的副作用四、pause流控对性能的影响分析1.性能影响2.风险评估最近定位了一个pause流控引发的产品问题,对pause流控进行了详细的研究,由于网上关于pause流控的相关资料非常少,这里将所有pause流控相关的知识总结整理一下,供大家参考。一、以太网的流量控制以_流控 pause 发送时机 计算

对linux下各种profiling工具的心得_profiling的工作-程序员宅基地

文章浏览阅读4k次。第一次写博_profiling的工作

tf.nn.dropout() 警报信息处理_please use `rate` instead of `keep_prob`. rate sho-程序员宅基地

文章浏览阅读8.2k次,点赞11次,收藏23次。WARNING: Logging before flag parsing goes to stderr.calling dropout (from tensorflow.python.ops.nn_ops) with keep_prob is deprecated and will be removed in a future version.Instructions for updatin..._please use `rate` instead of `keep_prob`. rate should be set to `rate = 1 -

vmware12 的kernel module updater解决方法_vmware kernel module update-程序员宅基地

文章浏览阅读6.9k次。vmware12 的kernel module updater解决方法_vmware kernel module update

Typescript 开发工具Vscode自动编译.ts文件_tsconfig中导入 d.ts-程序员宅基地

文章浏览阅读350次。1.创建tsconfig.json文件tsc–init 生成配置文件首先你需要进入你的项目目录cmd然后输入tsc --init这样的话该目录下就会生成一个tsconfig.json的文件下一步你需要把tsconfig.json文件的outDir 改一下下一步去创建一个ts 文件最后去终端运行一下就会生成js文件了..._tsconfig中导入 d.ts