音视频学习之路--JNI全面解析

2,316 阅读19分钟

前言

复习完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中勾选几个即可:

image.png

这里还是简单介绍一下子:

  • NDK,Native Development Kit,是一个开发工具包,作用就是快速开发C/C++,并自动将so和应用一起打包成APK。
  • JNI,Java Native Interface,通过JNIJava能调用C++。
  • CMake,运行开发者编写一种和平台无关的CMakeList.txt文件来定制整个编译流程,就是一种编译脚本。

既然这个txt是编译脚本,所以在app的gradle中需要把这个脚本加上即可:

image.png

这里的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"的简单描述和使用:

externC.png

其实这个还挺麻烦的,主要就是告诉编译器在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;   //对应JavaOther object
typedef _jclass*        jclass;    //对应Java的java.lang.Class
typedef _jstring*       jstring;   //对应JavaJava.lang.String
typedef _jarray*        jarray;    //对应JavaOther 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这种。

数据类型描述符.png

看到这里就比较疑惑了,这到底是个啥,Java的所有类型都在这里的了但是这里我们还必须要能搞清楚,因为这个东西在后面C/C++代码中需要用到,那你就会问,比如我定义一个Book类,那需要凭空写这些描述符吗,当然不是。

比如有代码如下:

package com.zyh.jnistudy

data class Book(val author: String
    ,val name: String
    ,val price:Float)

点击build,在生成的class文件中,打开命令行界面,

image.png

输入javap -s xxx,然后查看:

image.png

复制出来,对于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类型,那我直接返回一个字符串类型可以吗,把代码改成下面这种:

image.png

很显然是不可以的,所以这个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的一样,只能是一个字符串。

上面代码打印如下:

image.png

对于基本数据类型是可以正常解析的。

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,大概总结如下:

JNI数组.png

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++的字符串类型。

看个简单总结:

处理引用类型数组.png

其他非数组引用数据类型

其实在前面我们说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函数可以创建对象。这里也分为下面几步:

  1. 先拿到类型的全路径,通过findClass函数得到class对象。
  2. 构造函数一定是,然后传入签名。
  3. 调用构造函数,这里IDE会自动生成,然后多参数按构造函数顺序传入即可。

JNI动态注册和静态注册

对于JNI有2种注册方式,分别是静态注册和动态注册,在Android studio自己创建native项目,这个属于静态注册,而还有一种动态注册的方式。

静态注册

在cpp文件中的静态注册方法是:

image.png

从这个名字我们就可以看出,这个方法是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的通用知识。