前言
复习完C和C++的基础就可以来进行NDK相关的开发了,也就是又回到Java,但是用Java来调用C/C++。
所以本章先仔细学习一下JNI,在很久之前我做过有关JNI的开发,但是比较少,没有深入过,所以本篇文章就先介绍一下JNI。
在文章正式开始之前,先给大家分享一个B站上非常不错的视频教程,该视频教程从基础知识讲解再到直播推流实战,非常全面讲的也很细致,大家可以去看看。
音视频开发全系列教程:https://www.bilibili.com/video/BV1fb4y1d7JU?spm_id_from=333.999.0.0
正文
对于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
#include
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
//定义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
,book: Book
,bookList: ArrayList)
基本类型数组
这里先看一下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(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(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 = "";
//拿到构造方法
jmethodID construcotr_methor = env->GetMethodID(book_class,"", "(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 = "";
//拿到构造方法
jmethodID construcotr_methor = env->GetMethodID(book_class,"", "(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(&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 = "";
//拿到构造方法
jmethodID construcotr_methor = env->GetMethodID(book_class,"", "(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的通用知识。
本文转自 https://juejin.cn/post/7022875228690710565,如有侵权,请联系删除。