技术标签: c++ jni&ndk
ndk全称Native Developer Kits(原生开发工具包),Android NDK也是Android SDK的一个扩展集,用来扩展SDK的功能。 NDK打通了Java和C/C++之间的开发障碍,让Android开发者也可以使用C/C++语言开发APP。
Java是在C/C++之上的语言,语言金字塔越往上对开发者就更加贴近,也就是更容易开发,但是性能相对也就越低。越往下对开发人员的要求也就越高,但是实现后的产品性能也越高,因为可以自己控制内存等模块的使用,而不是让Java虚拟机自行处理。
使用NDK开发最终目标是为了将C/C++代码编译生成.so动态库或者.a静态库文件,并将库文件提供给Java代码调用。
NDK分为三层:构建层 Java层 native层
要得到目标的so文件,需要有个构建环境以及过程,将这个过程和环境称为构建层。
构建层需要将C/C++代码编译为动态库so,那么这个编译的过程就需要一个构建工具,构建工具按照开发者指定的规则方式来构建库文件,类似apk的Gradle构建过程。
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其实就是一个脚本。早期的NDK开发一直都是使用这种模式,
使用ndk-build需要配合两个mk文件:Android.mk和Application.mk。
Android.mk文件更像是一个传统的makefile文件,其定义源代码路径,头文件路径,链接器的路径来定位库,模块名,构建类型等。
其定义了Android app的相关属性。如:Android Sdk版本
,调试或者发布模式
,目标平台ABI
,标准C/C++库
等
Cmake 是用来生成makefile文件的,cmake使用一个CmakeLists.txt的配置文件来生成对应的makefile文件。
步骤1:使用Cmake生成编译的makefiles文件
步骤2:使用Make工具对步骤1中的makefiles文件进行编译为库或者可执行文件。
那使用Cmake优势在哪里呢?相信了解Gradle构建的都知道,为什么现在的apk构建过程会这么快,就是因为其在编译apk之前会生成一个任务依赖树,因此在多核状态下,任务可以在异步状态下执行,所以apk构建过程会非常快。而我们的Cmake也是类似,其在生成makefile过程中会自动分析源代码,创建一个组件之间依赖的关系树,这样就可以大大缩减在make编译阶段的时间。
CMake最大优点就是可以动态调试C/C++代码
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版本
}
}
}
我们在编译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']
}
}
对于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()
JNI(Java Native Interface,Java 本地接口)是 Java 生态的特性,它扩展了 Java 虚拟机的能力,使得 Java 代码可以与 C/C++ 代码进行交互。 通过 JNI 接口,Java 代码可以调用 C/C++ 代码,C/C++ 代码也可以调用 Java 代码。
HelloWorld.java
,并声明 native 方法 sayHi(); ⑵使用 javac 命令编译源文件,生成 HelloWorld.class
字节码文件;
⑶使用 javah 命令导出 HelloWorld.h
头文件(头文件中包含了本地方法的函数原型);
⑷在源文件 HelloWorld.cpp
中实现函数原型;
⑸编译本地代码,生成 Hello-World.so
动态原生库文件;
⑹在 Java 代码中调用 System.loadLibrary(...) 加载 so 文件;
⑺使用 Java 命令运行 HelloWorld 程序。
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
代码调用的库.
so 库需要在运行时调用 System.loadLibrary(…)
加载,一般有 2 种调用时机:
⑴在类静态初始化中: 如果只在一个类或者很少类中使用到该 so 库,则最常见的方式是在类的静态初始化块中调用;
⑵在 Application 初始化时调用: 如果有很多类需要使用到该 so 库,则可以考虑在 Application 初始化等场景中提前加载。
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){
}
为什么 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()
JNIEXPORT
是宏定义,表示一个函数需要暴露给共享库外部使用时。JNIEXPORT 在 Window 和 Linux 上有不同的定义:
jni.h
// Windows 平台 :
#define JNIEXPORT __declspec(dllexport)
#define JNIIMPORT __declspec(dllimport)
// Linux 平台:
#define JNIIMPORT
#define JNIEXPORT __attribute__ ((visibility ("default")))
JNICALL
是宏定义,表示一个函数是 JNI 函数。JNICALL 在 Window 和 Linux 上有不同的定义:
jni.h
// Windows 平台 :
// __stdcall 是一种函数调用参数的约定 ,表示函数的调用参数是从右往左。
#define JNICALL __stdcall
// Linux 平台:
#define JNICALL
jobject
类型是 JNI 层对于 Java 层应用类型对象的表示。每一个从 Java 调用的 native 方法,在 JNI 函数中都会传递一个当前对象的引用。
jclass
类型,指向 native 方法所在类的 Class 对象;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;
}
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 版本中的 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 层之间的数据类型转换。
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 */
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);
与 jstring 的处理方式类似,JNI 规范将 Java 数组定义为 jobject 的派生类 jarray
:
jbooleanArray
、jintArray
等;jobjectArray
。①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 还提供了基于范围函数:
GetIntArrayRegion
和SetIntArrayRegion
,使用方法和注意事项和 GetStringUTFRegion 也是类似的,也是基于一块预分配的数组缓冲区。
①将 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;
}
如何从 Native 层访问 Java 的字段与方法.JNI 首先要找到想访问的字段和方法,这就依靠字段描述符和方法描述符。
在 Java 源码中定义的字段和方法,在编译后都会按照既定的规则记录在 Class 文件中的字段表和方法表结构中。例如,一个 public String str; 字段会被拆分为字段访问标记(public)、字段简单名称(str)和字段描述符(Ljava/lang/String)。 因此,从 JNI 层访问 Java 层的字段或方法时,首先就是要获取在 Class 文件中记录的简单名称和描述符。
①字段表结构: 包含字段的访问标记、简单名称、字段描述符等信息。例如字段 String str
的简单名称为 str
,字段描述符为 Ljava/lang/String;
②方法表结构: 包含方法的访问标记、简单名称、方法描述符等信息。例如方法 void fun();
的简单名称为 fun
,方法描述符为 ()V
⑴字段描述符规则:字段描述符其实就是描述字段的类型,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
⑴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);
}
}
}
⑴ 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);
}
}
JNI提供的另外一个功能是在本地代码中使用Java对象。通过使用合适的JNI函数,你可以创建Java对象,get、set 静态(static)和实例(instance)的域,调用静态(static)和实例(instance)函数。JNI通过ID识别域和方法,一个域或方法的ID是任何处理域和方法的函数的必须参数。
方法名 | 作用 |
GetObjectClass | 获取调用对象的类,我们称其为target |
FindClass | 根据类名获取某个类,我们称其为target |
IsInstanceOf | 判断一个类是否为某个类型 |
IsSameObject | 是否指向同一个对象 |
NewObject | 创建对象 |
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函数会返回数组的长度。
方法名 | 作用 |
NewGlobalRef | 创建全局引用 |
NewWeakGlobalRef | 创建弱全局引用 |
NewLocalRef | 创建局部引用 |
DeleteGlobalRef | 释放全局对象,引用不主动释放会导致内存泄漏 |
DeleteLocalRef | 释放局部对象,引用不主动释放会导致内存泄漏 |
.
访问 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 是保持有效的。
上面我们在实现
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 中的对象引用管理,我们先回顾一下 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 对象回收是交给垃圾回收器处理的。
局部引用可以直接使用: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);
在 JavaVM 和 JNIEnv 中,会分别建立多个表管理引用:
⑴JavaVM 内有 globals 和 weak_globals 两个表管理全局引用和弱全局引用。由于 JavaVM 是进程共享的,因此全局引用可以跨方法和跨线程共享;
⑵JavaEnv 内有 locals 表管理局部引用,由于 JavaEnv 是线程独占的,因此局部引用不能跨线程。另外虚拟机在进入和退出本地方法通过 Cookie 信息记录哪些局部引用是在哪些本地方法中创建的,因此局部引用是不能跨方法的。
使用 JNI 函数 IsSameObject
判断两个引用是否指向相同对象(适用于三种引用类型),返回值为 JNI_TRUE
时表示相同,返回值为 JNI_FALSE
表示不同。
示例程序
jclass localRef = ...
jclass globalRef = ...
bool isSampe = env->IsSamObject(localRef, globalRef)
当引用与
NULL
比较时含义略有不同:
- 局部引用和全局引用与 NULL 比较: 用于判断引用是否指向 NULL 对象;
- 弱全局引用与 NULL 比较: 用于判断引用指向的对象是否被回收。
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;
}
@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);
}
异常处理的步骤我懂了,由于虚拟机在遇到 ThrowNew 时不会中断当前执行流程,那我怎么知道当前已经发生异常呢?有 2 种方法:
方法 1: 通过函数返回值错误码,大部分 JNI 函数和库函数都会有特定的返回值来标示错误,例如 -1、NULL 等。在程序流程中可以多检查函数返回值来判断异常。
方法 2: 通过 JNI 函数 ExceptionOccurred
或 ExceptionCheck
检查当前是否有异常发生。
在 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);
在 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) {
...
};
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);
}
在 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 开发模板,将包括上文提到的一些比较重要的知识点。
程序逻辑很简单:Java 层传递一个媒体文件路径到 Native 层后,由 Native 层播放媒体并回调到 Java 层。为了程序简化,所有真实的媒体播放代码都移除了,只保留模板代码。
⑴Java 层: 由 start()
方法开始,调用 startNative()
方法进入 Native 层;
⑵Native 层: 创建 MediaPlayer 对象,其中在子线程播放媒体文件,并通过预先持有的 JavaVM 指针获取子线程的 JNIEnv 对象回调到 Java 层 onStarted()
方法。
静态注册采用的是基于「约定」的命名规则,通过 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 语言是没有函数重载的,无法根据参数来区分函数重载,所以才需要拼接后缀来消除重载。
现在,我们来分析下静态注册匹配 JNI 函数的执行过程。由于没有找到直接相关的资料和函数调用入口,我是以 loadLibrary()
加载 so 库的执行流程为线索进行分析的,最终定位到 FindNativeMethod()
这个方法,从内容看应该没错。
静态注册是在首次调用 Java native 方法时搜索对应的 JNI 函数,而动态注册则是提前手动建立映射关系,并且不需要遵守静态注册的 JNI 函数命名规则。
动态注册需要使用 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;
RegisterNatives 方式的本质是直接通过结构体指定映射关系,而不是等到调用 native 方法时搜索 JNI 函数指针,因此动态注册的 native 方法调用效率更高。此外,还能减少生成 so 库文件中导出符号的数量,则能够优化 so 库文件的体积。更多信息见 Android 对 so 体积优化的探索与实践 中 “精简动态符号表” 章节。
注册 JNI 函数的时机,主要分为 3 种:
注册时机 | 注册方式 | 描述 |
1、在第一次调用该 native 方法时 | 静态注册 | 虚拟机会在 JNI 函数库中搜索函数指针并记录下来,后续调用不需要重复搜索 |
2、加载 so 库时 | 动态注册 | 加载 so 库时会自动回调 JNI_OnLoad 函数,在其中调用 RegisterNatives 注册 |
3、提前注册 | 动态注册 | 在加载 so 库后,调用该 native 方法前,通过静态注册的 native 函数触发 RegisterNatives 注册。例如在 App 启动时,很多系统源码会提前做一次注册 |
静态注册和动态注册的区别:
⑴静态注册基于命名约定建立映射关系,而动态注册通过 JNINativeMethod
结构体建立映射关系;
⑵ 静态注册在首次调用该 native 方法搜索并建立映射关系,而动态注册会在调用该 native 方法前建立映射关系;
⑶ 静态注册需要将所有 JNI 函数暴露到动态符号表,而动态注册不需要暴露到动态符号表,可以精简 so 文件体积。
(4)动态注册和静态注册最终都可以将native方法注册到虚拟机中,推荐使用动态注册,更不容易写错,静态注册每次增加一个新的方法都需要查看原函数类的包名。
见我的项目工程:MyJniNativeStudyDemo的模块:JniDynamicRegistrantionDemo
见我的项目工程:MyJniNativeStudyDemo的模块:thirdsoCall
文章浏览阅读2.6k次。CE 6.0 R2对CE 6.0的功能增加:1 Core OS2 IE3 Media Player4 RDP 对于从事廋客房端开发的人员有很大的帮助5 VoIP6 Web Services一) CE OS的改变,只有两次1 2.0->3.02 5.0->6.0但是4.2到5.0驱动变化较大,导致从4.2到5.0的移植需要很长的时间二) CE 6.0与5.0的主要区别1 CE6.0的Ker_win ce5.0 ce6.0区别
文章浏览阅读1.4k次。在处理大疆无人机拍的图像时,官方给的图像处理指南里出现了这么一句:于是去网上查找如何读取图像meta信息,找到了许多花里胡哨的方法,比如python代码、java代码、网站代查之类,最终我发现,最简便的方法就是:用写字板的方式打开图像,然后查找”xmp“ ,需要的参数就全能找到了。唯一的不足就是这个方法的效率太低了,写字板打开tif文件奇慢无比。..._xmp.drone-dji.lrftargetdistance
文章浏览阅读4.2k次。云服务器安装kafka,部署zookeeper时有如下注意点:1、在云服务器安全组中开放:2181、9092端口2、zookeeper.connect改成公网IP3、listeners=PLAINTEXT:// 必须填内网IPlisteners=PLAINTEXT://**.**.**.**:90924、配置外部代理地址必须填公网IPadvertised.listeners=PLAINTEXT://**.**.**.**:9092advertised.host.name=*.._connection request from old client
文章浏览阅读1.2k次,点赞19次,收藏22次。本文从项目需求出发到项目最终发版提测,讲述一下项目中遇到的问题(MyBatis数据库厂商适配、查看数据库链接、连接池失效等)以及打怪升级过程(思路),文章中会提到涉及到的坑以及解决办法。相信看完,多少会给你提供一些价值。_failed to initialize pool: this connection has been closed.
文章浏览阅读179次。e() {return mDisableDependentsState;}public void setDisableDependentsState(boolean disableDependentsState) {mDisableDependentsState = disableDependentsState;}@Overrideprotected Object onGetDefaultValu..._android 自定义listpreference
文章浏览阅读5.1k次。MOS管优点:1.具有良好的温度特性。2.具有良好的噪声特性。3.输入阻抗高。4.MOS管的漏极电流具有二次函数特性,三极管的集电极电流是指数形式。5.MOS管的上限频率远远超过三极管的上限工作频率。6.MOS管功耗较小。MOS管缺点:1.增益通常较低。2.输入阻抗高,导致匹配网络难设计。3.相对于三极管,MOS管的功率容量偏低..._mos管比起三极管有什么优势
文章浏览阅读6.8k次,点赞4次,收藏22次。java项目中经常会使用到对日期进行格式校验、格式化日期、LocalDate与Date互转等等,以下整理一份经常会使用到的日期操作相关的方法。_java校验日期格式
文章浏览阅读7.4k次,点赞10次,收藏94次。文章目录一、以太网的流量控制二、pause流控的原理和实现1.pause流控原理2.pause消息格式3.pause流控处理逻辑4.pause流控芯片上的实现三、pause流控的作用与副作用1.pause流控的作用2.pause流控的副作用四、pause流控对性能的影响分析1.性能影响2.风险评估最近定位了一个pause流控引发的产品问题,对pause流控进行了详细的研究,由于网上关于pause流控的相关资料非常少,这里将所有pause流控相关的知识总结整理一下,供大家参考。一、以太网的流量控制以_流控 pause 发送时机 计算
文章浏览阅读4k次。第一次写博_profiling的工作
文章浏览阅读8.2k次,点赞11次,收藏23次。WARNING: Logging before flag parsing goes to stderr.calling dropout (from tensorflow.python.ops.nn_ops) with keep_prob is deprecated and will be removed in a future version.Instructions for updatin..._please use `rate` instead of `keep_prob`. rate should be set to `rate = 1 -
文章浏览阅读6.9k次。vmware12 的kernel module updater解决方法_vmware kernel module update
文章浏览阅读350次。1.创建tsconfig.json文件tsc–init 生成配置文件首先你需要进入你的项目目录cmd然后输入tsc --init这样的话该目录下就会生成一个tsconfig.json的文件下一步你需要把tsconfig.json文件的outDir 改一下下一步去创建一个ts 文件最后去终端运行一下就会生成js文件了..._tsconfig中导入 d.ts