NDK基础
一:ndk简介
ndk全称Native Developer Kits(原生开发工具包),Android NDK也是Android SDK的一个扩展集,用来扩展SDK的功能。 NDK打通了Java和C/C++之间的开发障碍,让Android开发者也可以使用C/C++语言开发APP。
Java是在C/C++之上的语言,语言金字塔越往上对开发者就更加贴近,也就是更容易开发,但是性能相对也就越低。越往下对开发人员的要求也就越高,但是实现后的产品性能也越高,因为可以自己控制内存等模块的使用,而不是让Java虚拟机自行处理。
二:NDK架构分层
使用NDK开发最终目标是为了将C/C++代码编译生成.so动态库或者.a静态库文件,并将库文件提供给Java代码调用。
NDK分为三层:构建层 Java层 native层
2.1:构建层
要得到目标的so文件,需要有个构建环境以及过程,将这个过程和环境称为构建层。
构建层需要将C/C++代码编译为动态库so,那么这个编译的过程就需要一个构建工具,构建工具按照开发者指定的规则方式来构建库文件,类似apk的Gradle构建过程。
⑴CPU架构:Android abi
ABI即Application Binary Interface,定义了二进制接口交互规则,以适应不同的CPU,一个ABI对应一种类型的CPU。
Android目前支持以下7种ABI:
CPU | 主要abi类型 | 说明 |
ARMv5 | armeabi | 第5代和6代的ARM处理器,早期手机用的比较多。 |
ARMv7 | armeabi-v7a | 第7代及以上的 ARM 处理器。 |
ARMv8 | arm64-v8a | 第8代,64位ARM处理器 |
x86 | x86 | 一般用在平板,模拟器。 |
x86_64 | x86_64 | 64位平板 |
⑵构建工具:ndk-build构建 Cmake构建
①ndk-build构建(已淘汰)
ndk-build其实就是一个脚本。早期的NDK开发一直都是使用这种模式,
使用ndk-build需要配合两个mk文件:Android.mk和Application.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
,.h
是C
库对外开放的接口声明。对外开放的接口.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 的作用
JavaVM
和 JNIEnv
是定义在 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 类型 | 描述 | 长度(字节) |
---|---|---|---|
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) {// 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
:
- 基础类型数组:定义为
jbooleanArray
、jintArray
等; - 引用类型数组:定义为
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 还提供了基于范围函数:
GetIntArrayRegion
和SetIntArrayRegion
,使用方法和注意事项和 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 类型 | 描述符 |
---|---|
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 {// 加载本地动态库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数组类型 | 本地类型 |
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: 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 处理异常 | 程序使用关键字 | ||||||||||||||||||||
JNI/native 处理异常 | 程序使用 JNI 函数 在 JNI 层出现异常时,有 2 种处理选择:
JNI 提供了以下与异常处理相关的 JNI 函数:
|
示例程序
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 函数 ExceptionOccurred
或 ExceptionCheck
检查当前是否有异常发生。
七. 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