音视频学习之路--JNI全面解析
前言
复习完C和C++的基础就可以来进行NDK相关的开发了,也就是又回到Java,但是用Java来调用C/C++。
所以本章先仔细学习一下JNI,在很久之前我做过有关JNI的开发,但是比较少,没有深入过,所以本篇文章就先介绍一下JNI。
正文
对于Java来说,因为是需要JVM来运行,所以性能上肯定没有C/C++这种语言高,所以Android就弥补了这个缺陷,使用了JNI特征,使用JNI可以让Java类的某些方法用原生实现,让调用原生(C/C++)方法能够像普通Java方法一样。
环境配置
这里我们就可以使用Android studio了,AS的配置就不用多说了,Android开发都十分熟悉,这里只需要在SDK Tools中勾选几个即可:
这里还是简单介绍一下子:
NDK,Native Development Kit,是一个开发工具包,作用就是快速开发C/C++,并自动将so和应用一起打包成APK。
JNI,Java Native Interface,通过JNIJava能调用C++。
CMake,运行开发者编写一种和平台无关的CMakeList.txt文件来定制整个编译流程,就是一种编译脚本。
既然这个txt是编译脚本,所以在app的gradle中需要把这个脚本加上即可:
这里的JNI项目可以直接在创建Android项目时选择C++即可生成一个native项目。
Hello World
还是传统,先来搞一个Hello World,Android studio默认创建的native项目,运行即是一个Hello World,这里看一下基本代码:
class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) // Example of a call to a native method binding.sampleText.text = stringFromJNI() } /** * A native method that is implemented by the 'jnistudy' native library, * which is packaged with this application. */ external fun stringFromJNI(): String companion object { // Used to load the 'jnistudy' library on application startup. init { System.loadLibrary("jnistudy") } } }复制代码
这里使用external就定义了一个native方法,然后再看一下这个native方法的实现:
#include <jni.h> #include <string> using namespace std; extern "C" JNIEXPORT jstring JNICALL Java_com_zyh_jnistudy_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }复制代码
会发现虽然这只有几行代码,但是看起来还是有点晦涩,下面来单独分析一波。
extern "C"
看到这行代码,就有点不熟悉,下面是extern "C"的简单描述和使用:
其实这个还挺麻烦的,主要就是告诉编译器在C++代码中的C代码要使用C的编译方式来编译,不然源C代码定义的地方生成的文件和C++中编译的代码对不上。
比如上面的代码,应该就是那一行代码需要使用C来编译,而不能使用C++。
JNIEXPORT和JNICALL
虽然我们复习过C++文件,但是一看这个还是有点懵,虽然知道这2个是在头文件里define的,但是具体啥用呢?
JNIEXPORT在头文件中的定义:
#define JNIEXPORT __attribute__ ((visibility ("default")))复制代码
Windows中需要生成动态库,并且需要将动态库交给其他项目使用,需要在方法前加入特殊标识,才能在外部程序代码中,调用该DLL动态库中定义的方法。
JNICALL在头文件中是个空定义:
#define JNICALL复制代码
这个就不说了,主要是在Windows系统中对函数参数的一种约定。
所以这里的代码就是返回一个jstring类型,通过JNIEXPORT来标识这个生成的库可以被别的程序调用,JNICALL暂时无用。
JNI类型--基本数据类型
再回顾一下刚刚的C++代码:
extern "C" JNIEXPORT jstring JNICALL Java_com_zyh_jnistudy_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }复制代码
这里有个很奇怪的类型,就是这个jstring,这个是啥意思呢? 在Java代码中,我们肯定想要的是String类型,但是C++中只有string类型,那如何来进行一一对应呢,所以这里就有了JNI类型的概念。
对于Java那几种基本类型都好解决,就在Java基本类型前面加个j,就认为是JNI类型,比如Java中的boolean类型,就对应jboolean类型,那jboolean类型对应的就是C/C++的unsigned char类型,所以这里只要定义死,那基本类型转换在C和C++中就没啥问题了。
下面是jni.h中的定义:
typedef uint8_t jboolean; /* unsigned 8 bits */ typedef int8_t jbyte; /* signed 8 bits */ typedef uint16_t jchar; /* unsigned 16 bits */ 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 */复制代码
其中这里的uint8_t等类型也是一个别名,最后都是对应着C/C++的基础类型:
typedef signed char __int8_t; typedef unsigned char __uint8_t; typedef short __int16_t; typedef unsigned short __uint16_t; typedef int __int32_t; typedef unsigned int __uint32_t;复制代码
所以啊,在C++函数中返回的是jboolean,其实就是返回的无符号8位整型,然后通过NDK的操作,这个jboolean就能返回到Java代码中是一个boolean类型。
JNI类型--引用数据类型
其实和基本类型一样,当Java代码需要某种类型时,这时只要通过NDK库的定义和C/C++约定好即可,不过与基本数据类型不同的是,引用类型在原生方法时是不透明的,也就是具体这个jxx类型对应的C/C++类型是啥是不知道,可以看代码:
typedef _jobject* jobject; //对应Java的Other object typedef _jclass* jclass; //对应Java的java.lang.Class typedef _jstring* jstring; //对应Java的Java.lang.String typedef _jarray* jarray; //对应Java的Other array typedef _jobjectArray* jobjectArray; //Object[] typedef _jbooleanArray* jbooleanArray; //boolean[] typedef _jbyteArray* jbyteArray; //byte[] typedef _jcharArray* jcharArray; //char[] typedef _jshortArray* jshortArray; //short[] typedef _jintArray* jintArray; //int[] typedef _jlongArray* jlongArray; //long[] typedef _jfloatArray* jfloatArray; //float[] typedef _jdoubleArray* jdoubleArray; //double[] typedef _jthrowable* jthrowable; //Java.lang.Throwable复制代码
这里对几种数组和string、throwable、class都做了特殊标识处理,但是对于具体的类型我们就哟个object的话那肯定不行,比如Java代码想返回一个Book类型,在C/C++代码中给我随便返回的jobject这肯定不行,所以下面继续看JNI类型的知识。
JNI类型--数据类型描述符
在前面我们说了问题,比如我想在C/C++代码中使用Java定义的一个类,肯定不能直接导包啥的,因为这就不能互通,但是当Java代码运行在JVM虚拟机中时,我们就可以根据JVM中保存的类的类型,来通过C/C++中有个findClass函数来找到这个类,然后创建这个类的对象,所以在JVM中数据类型是如何保存的很重要,这里不是使用我们定义的int、float这种。
看到这里就比较疑惑了,这到底是个啥,Java的所有类型都在这里的了但是这里我们还必须要能搞清楚,因为这个东西在后面C/C++代码中需要用到,那你就会问,比如我定义一个Book类,那需要凭空写这些描述符吗,当然不是。
比如有代码如下:
package com.zyh.jnistudy data class Book(val author: String ,val name: String ,val price:Float)复制代码
点击build,在生成的class文件中,打开命令行界面,
输入javap -s xxx,然后查看:
复制出来,对于kotlin的数据类自动包含的方法熟悉Android开发的都知道,我们这里来简单分析一下子:
public final class com.zyh.jnistudy.Book { //构造函数 这里的构造函数返回值居然是V,也就是void //多个参数中间是没有分隔符的,这里的;是其他引用类型 public com.zyh.jnistudy.Book(java.lang.String, java.lang.String, float); descriptor: (Ljava/lang/String;Ljava/lang/String;F)V //无参函数返回String public final java.lang.String getAuthor(); descriptor: ()Ljava/lang/String; //无参函数返回String public final java.lang.String getName(); descriptor: ()Ljava/lang/String; //无参函数返回float public final float getPrice(); descriptor: ()F //获取第一个成员变量 public final java.lang.String component1(); descriptor: ()Ljava/lang/String; public final java.lang.String component2(); descriptor: ()Ljava/lang/String; public final float component3(); descriptor: ()F //返回Book自己 public final com.zyh.jnistudy.Book copy(java.lang.String, java.lang.String, float); descriptor: (Ljava/lang/String;Ljava/lang/String;F)Lcom/zyh/jnistudy/Book; public java.lang.String toString(); descriptor: ()Ljava/lang/String; //返回int public int hashCode(); descriptor: ()I //返回boolean public boolean equals(java.lang.Object); descriptor: (Ljava/lang/Object;)Z }复制代码
通过上面注释我们能看出来所有方法的描述符,其实就是别扭而已,没啥东西,但是我个人认为还是需要注意几点:
方法多个参数时,中间是没有分隔符的
引用类型是 LXXX; 这里的 ; 不要忘了
引用类型 LXXX; 的XXX具体值是通过 / 连接,而不是Java中的 . ,比如java.lang.String变成 了java/lang/String
JNIEnv
这个是啥呢 我们会发现在生成C++函数里第一个参数就是这个,官方说明是JNIEnv标识Java调用native语言的环境,是一个封装了几乎全部JNI方法的指针。
那为什么要这个指针呢 比如看下面代码:
extern "C" JNIEXPORT jstring JNICALL Java_com_zyh_jnistudy_MainActivity_stringFromJNI( JNIEnv* env, jobject /* this */) { string hello = "Hello from C++"; return env->NewStringUTF(hello.c_str()); }复制代码
这里我需要返回的是jstring类型,那我直接返回一个字符串类型可以吗,把代码改成下面这种:
很显然是不可以的,所以这个JNIEnv最直接的用处就是根据函数返回值返回特定的JNI类型返回值。
还有一个特性就是JNIEnv只在创建它的线程中生效,不能跨线程传递,不同线程的JNIEnv独立,为什么是这种,我们也不得知道其原理,等后续再探究。
那直接来看一下这个JNIEnv的结构体:
typedef _JNIEnv JNIEnv;复制代码
这里是_JNIEnv结构体
struct _JNIEnv { /* do not rename this; it does not seem to be entirely opaque */ const struct JNINativeInterface* functions; #if defined(__cplusplus) 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); } //这里有大概600多行复制代码
在这个_JNIEnv大概有6 700行代码,定义了我们常见的返回类型,最最主要的是返回类型都是JNI类型,所以我们目的是达到了,但是会发现其实这里都是用的JNINativeInterface这个类型的指针来完成这一大堆函数实现的,来看一下这个定义:
struct JNINativeInterface { void* reserved0; void* reserved1; void* reserved2; void* reserved3; jint (*GetVersion)(JNIEnv *); jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, jsize); jclass (*FindClass)(JNIEnv*, const char*); jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); jfieldID (*FromReflectedField)(JNIEnv*, jobject); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); jclass (*GetSuperclass)(JNIEnv*, jclass); jboolean (*IsAssignableFrom)(JNIEnv*, jclass, jclass); /* spec doesn't show jboolean parameter */ jobject (*ToReflectedField)(JNIEnv*, jclass, jfieldID, jboolean); //省略几百行复制代码
这是啥,这就是著名的函数指针,那关于具体咋实现的,我们这里就不用管了,这里也没有给出实现的地方,就记住一句话即可,想要返回JNI类型就必须通过这个JNIEnv指针来完成。
JNI处理Java传递过来的基本数据类型
这里就是JNI的关键之处了,C/C++代码能够处理Java传递过来的参数,先从简单的说,看一下基本数据类型,直接在Java代码中定义一个函数,包含所有基本数据类型:
external fun testAll(b: Boolean ,b1: Byte ,c: Char ,s: Short ,l: Long ,f: Float ,d: Double)复制代码
然后在C++代码中:
extern "C" JNIEXPORT void JNICALL Java_com_zyh_jnistudy_MainActivity_testAll(JNIEnv *env , jobject thiz , jboolean b , jbyte b1 , jchar c, jshort s , jlong l , jfloat f , jdouble d) { //接收boolean类型值 unsigned char c_boolean = b; LOGD("boolean -> %d",c_boolean) //接收byte类型值 signed char c_byte = b1; LOGD("byte -> %d",c_byte) //接收char类型值 unsigned short c_char = c; LOGD("char -> %d",c_char) //接收short类型值 short c_short = s; LOGD("short -> %d",c_short) //接收long类型值 long long c_long = l; LOGD("long -> %lld",c_long) //接收float类型值 float c_float = f; LOGD("float -> %f",c_float) //接收double类型值 double c_double = d; LOGD("double -> %f",c_double) }复制代码
这里其实没啥说的,如果记不住这些JNI类型对应的C/C++类型一点也不用怕,直接点击JNI类型就能找到对应的C/C++类型。
这里学个知识点是这个LOG的打印格式,之前都是使用printf或者cout,对于TAG查找很不方便,定义如下:
//导入Android的log包 #include <android/log.h> //定义TAG #define TAG "native-lib" //这里的...代表多参数 #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,TAG,__VA_ARGS__);复制代码
再来看一下这个log的定义:
int __android_log_print(int prio, const char* tag, const char* fmt, ...)复制代码
这里会发现第三个参数开始就是字符串和多参数,所以上面那个__VA_ARGS__代表多个参数,这种写法很巧妙,但是这里的LOGD和Android的一样,只能是一个字符串。
上面代码打印如下:
对于基本数据类型是可以正常解析的。
JNI处理Java传递过来的引用数据类型
对于基本数据类型还是很easy的,所以现在来看一下引用数据类型,先看数组,数组我们可以大概分为基本数据类型的数组和自定义类型的数组,比如下面代码:
external fun testAll2(intArray: IntArray ,stringArray: Array<String> ,string: String ,book: Book ,bookList: ArrayList<Book>)复制代码
基本类型数组
这里先看一下intArray,在C++文件中的解析:
extern "C" JNIEXPORT void JNICALL Java_com_zyh_jnistudy_MainActivity_testAll2(JNIEnv *env , jobject thiz , jintArray int_array , jobjectArray string_array , jstring string , jobject book, jobject book_list) { //解析int数组 引用类型前面也说了,没有直接对应的基本数据类型,被隐藏起来了 jint *intArray = env->GetIntArrayElements(int_array,NULL); //拿到数组长度 jsize intArraySize = env->GetArrayLength(int_array); for (int i = 0; i < intArraySize; ++i) { LOGD("int数组是 -> %d",intArray[i]); } //释放数组 env->ReleaseIntArrayElements(int_array,intArray,0); }复制代码
这里在前面也说了,对于非基本类型的JNI类型是被隐藏实现的,所以必须要使用JNIEnv来获取,关于处理XXX类型的数组,这里也是有迹可循,也就是上面的3步,不同类型对应不同的API,大概总结如下:
string数组
其实处理基本类型数组也非常的简单,接着看一下处理这个String数组,直接看一下代码:
//解析string数组,这里会发现类型是jobjectArray了,不是具体类型了 //这里就不能按照前面的步骤来了,因为可以想一下,这个类型不固定,所以不存在jobject指针来指出数组 //jobject *stringArray = env->GetObjectArrayElement(string_array,NULL); jsize stringArraySize = env->GetArrayLength(string_array); for (int i = 0; i < stringArraySize; ++i) { jobject jobject1 = env->GetObjectArrayElement(string_array,i); //强转 jstring stringArrayData = static_cast<jstring>(jobject1); //把jstring转成C/C++类型 const char *stringData = env->GetStringUTFChars(stringArrayData, NULL); LOGD("string数组值是 -> %s",stringArrayData) env->ReleaseStringUTFChars(stringArrayData,stringData); }复制代码
从上面的方法参数我们看见是jobjectArray类型,而不是预期的jstringArray类型当然也没有,从我们学习Java的角度来说,jobject是基类,肯定不能直接返回一个object数组,不符合正常思路,所以这里先获取数组长度进行遍历。
拿到一个jobect后,对于常理来说,肯定要转成一个具体的JNI类型,所以这里先转成了jstring,再由jstring转成了C/C++的字符串类型。
看个简单总结:
其他非数组引用数据类型
其实在前面我们说Java方法描述符时就说了一个概念,也就是方法签名,同时在JNI库中只有findClass方法来获取jclass文件,所以我们就可以通过这个来解析自定义数据类型:
//解析自定义类 //先获取字节码 char *book_class_str = "com/zyh/jnistudy/Book"; //通过类签名 获取jni的jclass jclass book_class = env->FindClass(book_class_str); //拿到方法签名 先是获取作者的方法 char *book_getAuthor_sign = "()Ljava/lang/String;"; //根据签名获取方法jmethod jmethodID getAuthor = env->GetMethodID(book_class,"getAuthor", "()Ljava/lang/String;"); //调用方法 返回jobject jobject obj_author = env->CallObjectMethod(book,getAuthor); //老办法 jobject强转为jstring jstring string_author = static_cast<jstring>(obj_author); //根据jstring 获取C/C++字符串 const char *author = env->GetStringUTFChars(string_author,NULL); LOGD("book的数组是 -> %s",author); env->DeleteLocalRef(book_class); env->DeleteLocalRef(book);复制代码
其实通过上面注释我们也可以看出基本规律了,对于引用类型都是先找到这个引用类型的签名,也就是包名+类名,使用 / 连接,然后通过findClass获取jclass,拿到jclass后就好做了,通过方法签名调用jclass中的方法,得到我们想要的返回值即可。最后不能忘记了删除本地变量。
JNI返回Java对象
上面一小节我们分析了Java传递各种类型到C++来处理,其实捋一下还是很简单的,主要就是分为2类,基本数据类型和引用数据类型,引用数据类型分为数组类型和非数组类型,上面都有介绍。那现在来讨论一下JNI也就是C/C++返回数据给Java使用,对于基本数据类型没啥说的了,这里说一下引用数据类型。
首先还是在Java代码中定义函数返回Book:
//JNI返回Book对象 external fun getBook():Book复制代码
然后在C/C++代码中就有对象的方法了,直接看下面实现,都有注释也非常好理解:
extern "C" JNIEXPORT jobject JNICALL Java_com_zyh_jnistudy_MainActivity_getBook(JNIEnv *env, jobject thiz) { //这里返回jobject对象 //先拿到Java类的全路径 char *book_java = "com/zyh/jnistudy/Book"; //找到Java对象class jclass book_class = env->FindClass(book_java); //拿到构造方法,这里必须这样写 //char *method = "<init>"; //拿到构造方法 jmethodID construcotr_methor = env->GetMethodID(book_class,"<init>", "(Ljava/lang/String;Ljava/lang/String;F)V"); //创建对象 jstring author = env->NewStringUTF("guo"); jstring name = env->NewStringUTF("第一行代码"); jfloat price = 55; jobject book_obj = env->NewObject(book_class,construcotr_methor ,name ,author ,price); return book_obj; }复制代码
这里可以看到返回一个book对象,对于这几行代码还是可以总结一下:
当需要返回基本数据类型时就不用说了,都是jint等,和C/C++都有对象,直接返回即可。
对于返回string类型,这里JNI有个特殊处理,就是NewStringUTF函数,使用这个也是可以返回String类型。
对于基本数据的数组,JNI中有NewXXXArray的函数,用来返回基本类型数组。
对于其他引用类型,有newObject函数可以创建对象。这里也分为下面几步:
先拿到类型的全路径,通过findClass函数得到class对象。
构造函数一定是,然后传入签名。
调用构造函数,这里IDE会自动生成,然后多参数按构造函数顺序传入即可。
JNI动态注册和静态注册
对于JNI有2种注册方式,分别是静态注册和动态注册,在Android studio自己创建native项目,这个属于静态注册,而还有一种动态注册的方式。
静态注册
在cpp文件中的静态注册方法是:
从这个名字我们就可以看出,这个方法是MainActivity1这个类中注册的,所以问题就是当包名或者类名改了之后,这个方法名也需要改,所以比较麻烦。
动态注册
对于动态注册就稍微复杂点,但是逻辑很清晰。
首先我们得明白一点,就是不管是动态注册还是静态注册,在Java文件中是不变的,就是必须得声明本地方法已经加载so,也就是下面代码必不可少:
//JNI返回Book对象 external fun getBook():Book companion object { // Used to load the 'jnistudy' library on application startup. init { System.loadLibrary("jnistudy") } }复制代码
然后呢,想一下如何动态注册,顾名思义就是C++文件中的名字就不会和静态注册一样和Java类名想关联的,也就是独立开来了,代码如下:
extern "C" JNIEXPORT jobject JNICALL getBook(JNIEnv *env, jobject thiz) { //这里返回jobject对象 //先拿到Java类的全路径 char *book_java = "com/zyh/jnistudy/Book"; //找到Java对象class jclass book_class = env->FindClass(book_java); //拿到构造方法,这里必须这样写 //char *method = "<init>"; //拿到构造方法 jmethodID construcotr_methor = env->GetMethodID(book_class,"<init>", "(Ljava/lang/String;Ljava/lang/String;F)V"); //创建对象 jstring author = env->NewStringUTF("guo"); jstring name = env->NewStringUTF("第一行代码"); jfloat price = 55; jobject book_obj = env->NewObject(book_class,construcotr_methor ,author ,name ,price); return book_obj; }复制代码
然后呢,就要注册关系了,也就是对应关系,Java中定义的方法名 <-> C/C++中定义的方法名,这里也就需要一个JNINativeMethod类型的数组来构建这一关联关系:
const char *classPath = "com/zyh/jnistudy/MainActivity1"; //这里是个数组,而类型是JNINativeMethod类型 static const JNINativeMethod jniNativeMethod[] = { { "getBook" ,"()Lcom/zyh/jnistudy/Book;" ,(void *)(getBook) } };复制代码
然后既然有了对应关系,那就需要把这个关系注册到JNI系统中即可,这里就会有一个回调方法,叫做JNI_OnLoad,通过这个方法就可以把这个关系给注册到JNI中:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){ //这里可以创建新的JNIEnv JNIEnv *jniEnv = nullptr; jint result = vm->GetEnv(reinterpret_cast<void **>(&jniEnv),JNI_VERSION_1_6); if (result != JNI_OK){ return JNI_ERR; } jclass mainActivityClass = jniEnv->FindClass(classPath); jniEnv->RegisterNatives(mainActivityClass ,jniNativeMethod ,sizeof (jniNativeMethod) / sizeof (JNINativeMethod)); return JNI_VERSION_1_6; }复制代码
其中前几行代码是写死的哈,主要就是调用RegisterNatives函数即可,但是这个函数第一个参数是Java类的Class文件,所以当有多个类都需要动态注册,就需要多次调用RegisterNatives方法即可,然后不要忘了也要定义多个该类中的本地方法影视关系数组。
JNI的异常处理
关于Java的异常处理大家都很熟悉了,当出现异常时,JVM会停止该代码的执行,然后看你try catch代码块中有没有捕获该异常的代码,然后进行处理异常。
不过在C/C++代码就不一样了,因为在Java中调用原生代码,原生代码不是执行在JVM中,所以这时发生了异常,C/C++代码不会停止,所以就需要在JNI即C/C++代码中也可以捕获异常和处理异常。
下面我们写个例子,在Java中定义一个方法,会抛出异常,然后在C++中调用,然后捕获异常:
//JNI返回Book对象 在getBook中会发生Java异常 external fun getBook():Book fun testException(){ throw NullPointerException("testException函数发生了空指针异常") }复制代码
然后在getBook的C++方法里调用这个会抛出异常的函数,具体如何调用也是非常简单,找到class,找到方法:
extern "C" JNIEXPORT jobject JNICALL getBook(JNIEnv *env, jobject thiz) { //这里的thiz其实就是注册的MainActivity jclass clazz = env->GetObjectClass(thiz); //方法 jmethodID testEx = env->GetMethodID(clazz,"testException","()V"); //调用Java方法 env->CallVoidMethod(thiz,testEx); //判断是否发生异常 jthrowable exc = env->ExceptionOccurred(); if (exc){ env->ExceptionDescribe(); env->ExceptionClear(); jclass newExcCls = env->FindClass("java/lang/IllegalArgumentException"); // env->ThrowNew(newExcCls,"JNI返回了一个异常"); } //这里返回jobject对象 //先拿到Java类的全路径 char *book_java = "com/zyh/jnistudy/Book"; //找到Java对象class jclass book_class = env->FindClass(book_java); //拿到构造方法,这里必须这样写 //char *method = "<init>"; //拿到构造方法 jmethodID construcotr_methor = env->GetMethodID(book_class,"<init>", "(Ljava/lang/String;Ljava/lang/String;F)V"); //创建对象 jstring author = env->NewStringUTF("guo"); jstring name = env->NewStringUTF("第一行代码"); jfloat price = 55; jobject book_obj = env->NewObject(book_class,construcotr_methor ,author ,name ,price); return book_obj; }复制代码
上面C++代码中关于异常的其实就3个函数:
ExceptionOccurred:有没有异常发生
ExceptionDescirbe:异常的描述信息
ExceptionClear: 清除异常
由于在C/C++代码中对Java的异常不是自动捕获和处理的,所以需要显示的手动判断进行处理,这样处理的话Java代码中虽然会抛出异常,但不会终止程序运行了。
总结
其实关于JNI还有许多东西要说,比如JNI的线程操作、JNI的布局引用、全局引用等,这些知识点等后续具体项目中有用到再说,本章就介绍一些JNI的通用知识。
作者:元浩875
链接:https://juejin.cn/post/7022875228690710565