NDK基础

NDK基础Android mk 在 Android 上编译需要的配置文件 相当于 build gradle 详细细节后面会讲到

大家好,欢迎来到IT知识分享网。

一: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文件。

NDK基础

❷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 代码。

NDK基础

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;

NDK基础

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

NDK基础

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); } 

NDK基础

NDK基础

NDK基础

NDK基础

NDK基础

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

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/117724.html

(0)
上一篇 2025-11-18 19:20
下一篇 2025-11-18 19:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信