阅读 283

Android架构师之路:JNI与NDK编程-函数注册与c++调用java详解(c++音视频编码基础)

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

本文章已在公众号【Android开发编程】发表

前言小计

1、jni与ndk的基本知识点前面文章都讲过了,不懂的,可以在公众号首页看;

2、jni中常用的方法比如:类、方法、数组、字符串等前面也讲解过了;

3、这篇文章讲解jni中函数的注册和c++调用java的知识点;

一. JNI函数注册

1、jni函数注解知识点介绍

JNI技术是Java世界与Native世界的通信桥梁,具体到代码,Java层的代码如何同Native层的代码进行调用的呢?我们都知道,在调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.UnsatisfiedLinkError异常 。 那么,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢?

JVM查找native方法有两种方式;

按照JNI规范的命名规则,调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。

第一种方式,可用使用javah工具按照Java类中定义的native方法,按照JNI规范的命名规则的方式自动生成Jni本地C/C++头文件。

第二种方式则需要在本地库的JNI_OnLoad函数中调用RegisterNatives来动态注册。

JNI函数注册是将Java层声明的Native方法同实际的Native函数绑定起来的实现方式,也就是说,只要通过JNI函数注册机制注册了本地方,Java层就可以直接调用定义的这些本地方法了。对应上述JVM查找native方法的两种方式,JNI函数注册方式一般分为静态注册和动态注册两种方式。

2、静态注册

原理:根据函数名来建立 java 方法与 JNI 函数的一一对应关系;

实现流程:

编写 java 代码;

利用 javah 指令生成对应的 .h 文件;

对 .h 中的声明进行实现;

弊端:

编写不方便,JNI 方法名字必须遵循规则且名字很长;

编写过程步骤多,不方便;

程序运行效率低,因为初次调用native函数时需要根据根据函数名在JNI层中搜索对应的本地函数,然后建立对应关系,这个过程比较耗时;

public class Test {

static {

System.loadLibrary("native-lib");

}

public native String textFromJni();

}

使用javah生成对应的本地方法头文件。

#include <jni.h>

#ifndef _Included_test

#define _Included_test

#ifdef __cplusplus

extern "C" {

#endif

JNIEXPORT jstring JNICALL Java_test_Test_textFromJni

(JNIEnv *, jobject);

#ifdef __cplusplus

}

#endif

3、动态注册

对应与上面的静态注册方法,还有一种动态注册JNI函数的方式,即动态注册。动态注册是当Java层调用System.loadLibrary方法加载so库后,本地库的JNI_OnLoad函数会被调用,在JNI_OnLoad函数中通过调用RegisterNatives函数来完成本地方法的注册。

原理:利用 RegisterNatives 方法来注册 java 方法与 JNI 函数的一一对应关系;

实现流程:

利用结构体 JNINativeMethod 数组记录 java 方法与 JNI 函数的对应关系;

实现 JNI_OnLoad 方法,在加载动态库后,执行动态注册;

调用 FindClass 方法,获取 java 对象;

调用 RegisterNatives 方法,传入 java 对象,以及 JNINativeMethod 数组,以及注册数目完成注册;

优点:

流程更加清晰可控;

效率更高;

其中JNINativeMethod结构体用来描述本地方法结构,其定义如下:

typedef struct {

const char* name; // Java方法名

const char* signature; // Java方法签名

void* fnPtr; // jni本地方法对应的函数指针

} JNINativeMethod;

结构体的第一个参数 name 是java 方法名;

第二个参数 signature 用于描述方法的参数与返回值;

第三个参数 fnPtr 是函数指针,指向 jni 函数;

其中,第二个参数 signature 使用字符串记录方法的参数与返回值,具体格式形如“()V”、“(II)V”,其中分为两部分,括号内表示的是参数,括号右侧表示的是返回值;

①、数据类型映射

基本数据类型

Dingtalk_20210722201118.jpg

②. 数组引用类型

如果是一维数组则遵循下表,如果是二维数组或更高维数组则对应的 native 类型为 jobjectArray,域描述符中使用 ‘[’ 的个数表示维数

③. 对象引用类型

对于其它引用类型,即 java 中的对象,其映射规则为

Dingtalk_20210722201205.jpg

④. 对象数组引用类型

如果是一维数组则遵循下表,如果是二维数组或更高维数组则对应的 native 类型为 jobjectArray,域描述符中使用 ‘[’ 的个数表示维数

在Java文件中定义本地方法,加载本地so库

package test.jnitest;

public class Test {

static {

System.loadLibrary("native-lib");

}

public native String textFromJni();

}

在JNI_OnLoad函数中注册本地方法

jstring textFromJni(JNIEnv* env, jobject thiz) {

return env->NewStringUTF("text from jni");

}

static JNINativeMethod gMethods[] = {

{"textFromJni", "()Ljava/lang/String;", (void*)textFromJni}

};

int registerMethod(JNIEnv *env) {

jclass test = env->FindClass("cc/ccbu/jnitest/Test");

return env->RegisterNatives(test, gMethods, sizeof(gMethods)/ sizeof(gMethods[0]));

}

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {

JNIEnv* env = NULL;

if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {

return JNI_ERR;

}

if (registerMethod(env) != JNI_OK) {

return JNI_ERR;

}

return JNI_VERSION_1_6;

}

注意:

在JNI_OnLoad函数的结尾处,我们一定要有返回值,而且必须是JNI_VERSION_1_4 或 JNI_VERSION_1_6,也就是JNI的版本号,我们一定要返回正确的版本号,否则系统也是无法加载的;

4、c++调用java详解

(1) 找到java对应的Class

(2) 找到要调用的方法的methodID

(3) 在C语言中调用相应方法

①.通过JAVA层的本地方法创建同类对象

步骤:

I.通过对象获取类

II.通过类获取类的构造方法的ID

III.基于方法ID和类,创建新对象

JNIEXPORT void JNICALL JAVA_nativeMethod

(JNIEnv *env, jobject thiz,jint i){

...

jclass clazz = (*env).GetObjectClass(thiz);

jmethodID mid = (*env).GetMethodID(clazz,"","()V");

jobject obj = (*env).NewObject(clazz,mid);

...

return;

}

②.通过C/C++创建不同类对象

步骤:

I.通过FindClass方法获取需要的类

II.通过类获取类的构造方法的ID

III.基于方法ID和类,创建新对象

JNIEXPORT void JNICALL JAVA_nativeMethod

(JNIEnv *env, jobject thiz,jint i){

...

jclass clazz = (*env).FindClass("com/x/test/Test");//参数为类路径

jmethodID mid = (*env).GetMethodID(clazz,"","()V");

jobject obj = (*env).NewObject(clazz,mid);

...

return;

}

③获取上下文环境JNIEnv

如果找不到上下文JNIEnv就要获取

bool AttachCurrentThread(JavaVM* vm, JNIEnv** p_env){    bool bAttached = false;    switch(vm->GetEnv((void**)p_env, JNI_VERSION_1_4))    {        case JNI_OK:            break;        case JNI_EDETACHED:            if (vm->AttachCurrentThread(p_env, 0) < 0)            {                LOGD("%s :test failed!",__func__);                return false;            }            else            {                bAttached = true;            }            break;        case JNI_EVERSION:            LOGE("Invalid java version");            break;    }    return bAttached;}
复制代码

总结

以上总结了JNI中函数注册的两种方法,在实际应用中都很常见都用得到的,要理解到位才可以;

下次文章会继续讲解关于JNI的知识点和高级应用

文章分类
Android
文章标签