当前位置: 首页 > article >正文

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类型说明
ARMv5armeabi第5代和6代的ARM处理器,早期手机用的比较多。
ARMv7armeabi-v7a第7代及以上的 ARM 处理器。
ARMv8arm64-v8a第8代,64位ARM处理器
x86x86一般用在平板,模拟器。
x86_64x86_6464位平板

⑵构建工具: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 abiabiFilters "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方法中的返回值即为jstringexternal 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 版本中的 JNIEnvconst struct JNIInvokeInterface* functions;// 转发给 functions 代理jint DestroyJavaVM(){ return functions->DestroyJavaVM(this); }...
};/** C++ 版本的 JNIEnv,内部是对 JNINativeInterface* 的包装*/struct _JNIEnv {// 相当于 C 版本的 JavaVMconst 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*);//获取静态属性的idjfieldID    (*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 类型描述长度(字节)
booleanjbooleanunsigned char1
bytejbytesigned char1
charjcharunsigned short2
shortjshortsigned short2
intjintsigned int4
longjlongsigned long8
floatjfloatsigned float4
doublejdoublesigned double8
java.lang.ClassjclassClass 类对象1
java.lang.Stringjstrting字符串对象/
All Objectjobject任何Java对象,或者没有对应java类型的对象/
java.lang.Throwablejthrowable异常对象/
boolean[]jbooleanArray布尔数组/
byte[]jbyteArraybyte 数组/
char[]jcharArraychar 数组/
short[]jshortArrayshort 数组/
int[]jinitArrayint 数组/
long[]jlongArraylong 数组/
float[]jfloatArrayfloat 数组/
double[]jdoubleArraydouble 数组/
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) {// OutOfMemoryErrorLOGD("java层传递过来的字符串参数不存在");// return ;}jsize strSize = env->GetStringLength(jstr);LOGE("接收java层传递过来的字符串 jstr  转为 jni/native/c++ 层字符串 = %s, 长度=%d", str, strSize);// 释放 GetStringUTFChars 生成的 UTF-8 字符串strenv->ReleaseStringUTFChars(jstr,str);// 将 C/C++ 字符串 转换为 Java Stringstd::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, 29external 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, 109external 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 类型描述符
booleanZ(容易误写成B)
byteB
charC
shortS
intI
int[][I   ( 数组以"["开始)
longJ(容易误写成L)
floagF
doubleD
voidV
引用类型以 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 {// 加载本地动态库fmfjniSystem.loadLibrary("fmfjni")// 加载本地动态库fmfjni2System.loadLibrary("fmfjni2")}
}override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)//todo  演示 jni/Native层 改变 Java的 静态变量字段sName和实例变量字段mName的值Log.e(TAG,"输出改变静态变量字段sName前值="+ getsName())   //输出改变静态变量字段sName前值=defaultLog.e(TAG,"输出改变实例变量字段mName前值=${mName}")  //    输出改变实例变量字段mName前值=初始值accessField()Log.e(TAG,"输出在jni/native层改变静态变量字段sName后值="+ getsName()) // 输出在jni/native层改变静态变量字段sName后值=fangmingfeiLog.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的 IDjfieldID sFieldId =  env->GetStaticFieldID(clz,"sName","Ljava/lang/String;") ;// 访问静态变量字段值 sNameif(sFieldId){// Java 方法的返回值 String 映射为 jstringjobject 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的值// 获取实例字段 的IDjfieldID  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 中,而不是直接放在当前类中@JvmStaticfun sHelloJava() {Log.e(TAG, "jni/Native层 调用 Java 静态方法 sHelloJava()")}init {// 加载本地动态库fmfjniSystem.loadLibrary("fmfjni")// 加载本地动态库fmfjni2System.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()// 获取静态方法 IDjmethodID  sMethodId = env->GetStaticMethodID(clz,"sHelloJava","()V");if(sMethodId){// CallStaticVoidMethod(jclass clazz, jmethodID methodID, ...)env->CallStaticVoidMethod(clz,sMethodId);}// 示例:调用 Java层的 实例方法 helloJava()// 获取实例方法 IDjmethodID 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数组类型本地类型
GetBooleanArrayElementsjbooleanArrayjboolean
GetByteArrayElementsjbyteArrayjbyte
GetCharArrayElementsjcharArrayjchar
GetShortArrayElementsjshortArrayjshort
GetIntArrayElementsjintArrayjint
GetLongArrayElementsjlongArrayjlong
GetFloatArrayElementsjfloatArrayjfloat
GetDoubleArrayElementsjdoubleArrayjdouble

当你对数组的存取完成后,要确保调用相应的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: ActivityMainBindingprivate   var testJniCpp3: TextView?=nulloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)testJniCpp3 = binding.testJniCpp3testJniCpp3?.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变量的值+1env->SetIntField(thiz,numFieldId,oldValue+1);//todo 重新获取MainActivity中num变量值jint  num  =   env->GetIntField(thiz,numFieldId);//todo 获取MainActivity的TextView  testJniCpp3 变量idjfieldID 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转化为jstringchar 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;}@Overridepublic 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

http://www.lryc.cn/news/2416927.html

相关文章:

  • 两个必看协议-最全面-最直接-最暴力-一篇文章带你读懂-HTTP、HTTPS
  • 探索WebRTC Streamer:实时通信的新境界
  • Oracle:TNS数据传输协议-基础篇
  • C# DataTable 总结常用方法
  • Xamarin移动开发的优点和缺点
  • gcc 编辑器基本使用
  • pr找不到msvcr110.dll无法执行代码怎么解决?总结7个有效方法分享
  • 析构函数
  • Jquery各版本下载,附Jquery官网下载方法
  • 16进制颜色
  • 详解 LPC
  • 坑逼的PL2303与WIN11
  • 安装天堂遇到的问题及解决方法
  • 两款WiFi无线网络扫描工具软件 WirelessMon Xirrus WiFi Inspector
  • DTM、DEM、DSM与DOM的概念
  • ebook_7种开放式eBook格式指南
  • QGC地面站使用教程
  • BootStrap----table
  • 并发编程之CyclicBarrier详解
  • 【笔记】w5500 官方DHCP库 使用
  • c语言钩子函数
  • 无忧启动源码 仿音速启动
  • 简易钓鱼网站的构建(Kali SetoolKit)
  • UNIX环境高级编程——UNIX基础知识
  • 我的ubuntu8.04安装经验
  • 常见几种浏览器兼容性问题与解决方案
  • Autodesk CAD帮助文档总结二 DXF 概述
  • smb+服务器+修复,Microsoft Windows
  • MySQL如何卸载干净以及下载、安装教程
  • oTree学习教程(七)Apps rounds