JNI技术简单介绍

目标:认识JNI(Java Native Interface)技术,了解Java调用本地C/C++库的简单方法以及一些基本的知识点

什么是JNI,为什么使用JNI

JNI是Java Native Interface的缩写,中文译为“Java本地接口”。通俗地说,JNI是一种技术,通过这种技术可以做到以下两点:

  1. Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。
  2. Native程序中的函数可以调用Java层的函数,也就是说在C/C++程序中可以调用Java的函数。
    也就是Java与C/C++代码互相调用

在平台无关的Java中,为什么要创建一个与Native相关的JNI技术呢?这岂不是破坏了Java的平台无关特性吗?JNI技术的推出有以下几个方面的考虑:

  1. 承载Java世界的虚拟机是用Native语言写的,而虚拟机又运行在具体的平台上,所以虚拟机本身无法做到平台无关。然而,有了JNI技术后就可以对Java层屏蔽不同操作系统平台(如Windows和Linux)之间的差异了(例如同样是打开一个文件,Windows上的API使用OpenFile函数,而Linux上的API是open函数)。这样,就能实现Java本身的平台无关特性。其实Java一直在使用JNI技术,只是我们平时较少用到罢了。
  2. 早在Java语言诞生前,很多程序都是用C/C++语言写的,它们遍布在软件世界的各个角落。Java出世后,它受到了追捧,并迅速得到发展,但仍无法将软件世界彻底改朝换代,于是才有了折中的办法。既然已经有C/C++模块实现了相关功能,那么在Java中通过JNI技术直接使用它们就行了,免得落下重复制造轮子的坏名声。另外,在一些要求效率和速度的场合还是需要C/C++语言参与的。
  3. 在Android平台上,JNI就是一座将C/C++世界和Java世界间的天堑变成通途的桥。
  4. 效率上 C/C++是本地语言,比java更高效
  5. 代码移植,如果之前用C语言开发过模块,可以复用已经存在的c代码
  6. java反编译比C语言容易,一般加密算法都是用C语言编写,不容易被反编译
    在这里插入图片描述
第一个JNI应用

使用Android Studio新建一个Native应用
1.首先需要配置NDK和Cmake
Settings->Android SDK->SDK Tools
勾选NDK和CMake合适的版本,点击OK,Confirm Change点确认,开始下载安装
在这里插入图片描述
2.新建Native应用
File->New->New Project
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这个时候应用建好了,Java和C++代码就在对应的目录下,AS会自动帮我们生成一些demo代码
在这里插入图片描述
其中native-lib.cpp就是具体实现Native Method的地方,CMakeLists.txt是CMake编译配置文件

方法注册

Java Native Interface 总的来说可以被认为是一个Java接口,但是实现是在Native层代码中,为了能让Java代码正常的调用到Native方法,首先我们得让Java知道Native层对应的函数关系是什么,也就是Java接口对应的Native函数指针是什么,这个对应的关系称为方法注册,一般注册可以分为两种:静态注册 & 动态注册

3.1 静态注册

根据函数名来直接建立java方法和JNI函数间的一一对应关系,多用于NDK开发
举例上面的代码:

 public native String stringFromJNI();

对应的JNI代码:

extern "C" JNIEXPORT jstring JNICALL
Java_com_tis_jni_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

我们新建一个Nateive方法,比如:

public native String stringFromJNI2();

在AS中可以用快捷键自动帮我们建立对应的函数名,将鼠标选中stringFromJNI2方法上,按下
Alt+Enter 选中’Create JNI function for’回车

extern "C"
JNIEXPORT jstring JNICALL
Java_com_tis_jni_MainActivity_stringFromJNI2(JNIEnv *env, jobject thiz) {
    // TODO: implement stringFromJNI2()
}

通过方法名对比可以看出,虽然方法名很长,但是很有规律,我们可以了解到JNI函数的命名规则:
(1) Java代码中的函数声明需要添加native关键字 ;
(2) Native的对应函数名要以“Java_”开头,后面依次跟上Java的“package名”、“class名”、“函数名”,中间以下划线“”分割,在package名中的“.”也要改为“”。
(3) 此外,关于函数的参数和返回值也有相应的规则。
(4) 还有一点需要注意的是,在JNI的Native函数中,其前两个参数 JNIEnv *和 jobject 是必需的——前者是一个 JNIEnv 结构体的指针,这个结构体中定义了很多JNI的接口函数指针,使开发者可以使用JNI所定义的接口功能;后者指代的是调用这个JNI函数的Java对象,有点类似于C++中的 this 指针。在上述两个参数之后,还需要根据Java端的函数声明依次对应添加参数。在上例中,Java中声明的JNI函数没有参数,则Native的对应函数只有类型为 JNIEnv *和 jobject 的两个参数。

静态注册就是根据方法名,将Java方法和JNI方法建立关联,但是它有一些缺点:
(1)JNI层的方法名称过长。
(2)声明Native方法的类需要用javah生成头文件。
(3)初次调用JIN方法时需要建立关联,影响效率。

3.2 动态注册

我们知道,静态注册就是Java的Native方法通过方法指针来与JNI进行关联的,如果Native方法知道它在JNI中对应的方法指针,就可以避免上述的缺点,为了解决静态注册中的缺点,动态注册应运而生,通过注册直接告诉native函数其在JNI中对应函数的指针;
JNINativeMethod结构体定义在jni.h

typedef struct {
    char *name;
    char *signature;
    void *fnPtr;
} JNINativeMethod;
static const std::string CLASS_NAME = "com/tis/jni/MainActivity"; 
//(2)在一个JNINativeMethod数组中保存所有native函数和JNI函数的对应关系;
static const JNINativeMethod CLASS_METHODS[] = {
        //(1)利用结构体JNINativeMethod保存Java Native函数和JNI函数的对应关系;
        {"stringFromJNI2", "()Ljava/lang/String;", (void *) (stringFromJNI2)},
};

//(4)JNI_OnLoad中会调用RegisterNatives函数进行函数注册;
static int register_JNI(JNIEnv *env, const char *className, const JNINativeMethod *gMethods, int num) {

    jclass clazz = env->FindClass(className);
    if (clazz == nullptr) {
        LOGW("find class %s failed\n", className);
        return JNI_FALSE;
    }
//(5)AndroidRuntime::registerNativeMethods中最终调用RegisterNatives完成注册。
    if (env->RegisterNatives(clazz, gMethods, num) < 0) {
        return JNI_FALSE;
    }
    return JNI_TRUE;
}

/*
 * When the Java layer loads the JNI dynamic library through System.loadLibrary,
 * it will then look for a function called JNI_OnLoad in the library, and call it if there is one.
 */
//(3)在Java中通过System.loadLibrary加载完JNI动态库之后,调用JNI_OnLoad函数,开始动态注册;
jint JNI_OnLoad(JavaVM *vm, void * /* reserved */) {
    JNIEnv *env = NULL;
    jint result = -1;

    LOGW("JNI_OnLoad\n");
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_4) != JNI_OK) {
        LOGW("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);


    /**
     * When an array is used as a function parameter, the original array is not copied,
     * but the pointer to the original array is copied.
     * So unable to get array size using sizeof(), here you need to provide the array size
     */
    if (register_JNI(env, CLASS_NAME.c_str(), CLASS_METHODS,
                     NELEM(CLASS_METHODS)) < 0) {
        LOGW("ERROR: native registration failed\n");
        goto bail;
    }

    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

    bail:
    return result;
}

这样在加载so库时,自动调用JNI_OnLoad建立关联,如今Android系统AOSP源码中的JNI大部分都是使用的动态注册方式
疑问:若同时存在静态注册和动态注册时哪个生效呢?

4. 类型转换

4.1 基本数据类型转换
在这里插入图片描述

从上表可以看出,基本数据类型转换,除了void,其他的数据类型只需要在前面加上“j”就可以了。第三列的Signature 代表签名格式,后文会介绍它的用处。规律就是除了long–>J,boolean–>Z,其他都是首字母大写。接着来看引用数据类型的转换。
4.2 引用数据类型转换

在这里插入图片描述
从上表可看出,规律也很明显,数组的JNI层数据类型需要以“Array”结尾,签名格式的开头都会有“[”。除了数组以外,其他的引用数据类型的签名格式都会以“;”结尾。
在这里插入图片描述

5. JNI如何调用Java

既然JNI是一个桥梁,使Java能调用到Native代码,那这个桥梁是否是双向的呢?反过来Native能不能调用到Java代码呢?
5.1 方法签名
前面表格已经列举了数据类型的签名格式,方法签名就由签名格式组成,那么,方法签名有什么作用呢?我们看下面的代码。

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

gMethods数组中存储的是MainActivity的Native方法与JNI层方法的对应关系;其中"()Ljava/lang/String;"就是方法stringFromJNI2的方法签名;
我们知道Java是有重载方法的,可以定义方法名相同,存在参数不同的同名方法,正因为如此,在JNI中仅仅通过方法名是无法对应到Java中的具体方法的;JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI的方法签名的格式为: ‘(’+参数签名格式…+‘)’+返回值签名格式

5.2 自动生成方法签名
JNI动态注册时需要方法签名,可以自己手写,但是出错的概率比较高,如果我们每次编写JNI时都要写方法签名,也会是一件比较头疼的事,而Java提供了javap命令来自动生成方法签名。
在android studio里面如何查看一个java类的方法的各种签名
1.在android中创建一个java类,then build ->rebuild Project
2.在android的目录AndroidStudioProjects\JNI\app\build\intermediates\javac\debug\classes> 打开cmd,在出来的命令框里面输入 javap -s -p 包名.类名

javap -s -p MainActivity.class

其中s 表示输出内部类型签名,p表示打印出所有的方法和成员(默认打印public成员),最终在cmd中的打印结果如下:

  protected void onCreate(android.os.Bundle);
    descriptor: (Landroid/os/Bundle;)V

  public native java.lang.String stringFromJNI();
    descriptor: ()Ljava/lang/String;

  public native java.lang.String stringFormJNI2(java.lang.String);
    descriptor: (Ljava/lang/String;)Ljava/lang/String;
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
}

public native String stringFromJNI();

public native String stringFromJNI2(String value);

可以很清晰的看到输出的方法的签名和此前给出类型转换是对应的
5.3 JNIEnv
JNIEnv 是一个指向全部JNI方法的指针,该指针只在创建它的线程有效,不能跨线程传递,因此,不同线程的JNIEnv是彼此独立的;
主要作用有两点:
1.调用Java的方法。
2.操作Java(获取Java中的变量和对象等等)。
JNIEnv的定义
代码位置:libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv;//C++中JNIEnv的类型 
typedef _JavaVM JavaVM; 
#else
typedef const struct JNINativeInterface* JNIEnv;//C中JNIEnv的类型 
typedef const struct JNIInvokeInterface* JavaVM;
#endif

说明:
这里使用预定义宏__cplusplus来区分C和C++两种代码,如果定义了__cplusplus,则是C++代码中的定义,否则就是C代码中的定义。
在这里我们也看到了JavaVM,它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都可以使用这个JavaVM。
JNIEnv中定义了大量的方法,我们可以通过这些方法来实现回调Java的逻辑
5.4 回调Java
先写两个简单的Java层方法,一个静态方法,一个普通方法

public static int nativeCallInt(int para1, int[] para2) {
    Log.w(TAG, "nativeCallInt " + para1 + " " + Arrays.toString(para2));
    return (para1 >= 0 && para1 < para2.length) ? para2[para1] : 0;
}

public String nativeCallString(String para1, String para2) {
    Log.w(TAG, "nativeCallString " + para1 + " " + para2);
    return para1 + para2;
}

他们对应的方法签名:

  public static int nativeCallInt(int, int[]);
    descriptor: (I[I)I

  public java.lang.String nativeCallString(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;

Native回调Java的代码和Java中反射的调用方式类似:
1.通过类名找到对应的jclass对象
2.通过env->GetStaticMethodID或者env-> GetMethodID在jclass对象中寻找符合方法名和方法签名的jmethodID对象
3.通过env->CallStaticIntMethod或者env->CallObjectMethod,传入jclass/thiz,jmethodID,和参数调用Java层的方法

/**
 * Native re-call java, use like reflect in java
 */
extern "C" JNIEXPORT void JNICALL
callNative(JNIEnv *env, jobject thiz) {

    jclass cls;
    jmethodID mid;
    jfieldID jFd;
    // find class object by class name
    cls = env->FindClass(CLASS_NAME.c_str());
    if (cls != nullptr) {
        //find static methodId by method name and signature
        mid = env->GetStaticMethodID(cls, "nativeCallInt", "(I[I)I");
        if (mid != nullptr) {
            jint para1 = 3;
            int temp[] = {1, 2, 3, 4, 5, 6};
            jintArray para2 = env->NewIntArray(sizeof(temp) / sizeof(int));
            env->SetIntArrayRegion(para2, 0, sizeof(temp) / sizeof(int), temp);
            // call the method
            jint result = env->CallStaticIntMethod(cls, mid, para1, para2);
            LOGW("Native call Java nativeCallInt: %d\n", result);
        }
        
        //find methodId by method name and signature
        mid = env->GetMethodID(cls, "nativeCallString",
                               "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;");
        if (mid != nullptr) {
            std::string string1 = "Hello ";
            std::string string2 = "World!";
            jstring para1 = env->NewStringUTF(string1.c_str());
            jstring para2 = env->NewStringUTF(string2.c_str());
            jstring result = (jstring) env->CallObjectMethod(thiz, mid, para1, para2);
            std::string ret = env->GetStringUTFChars(result, nullptr);
            LOGW("Native call Java nativeCallString: %s\n", ret.c_str());
        }

        //find fieldId by field name and signature
        jFd = env->GetStaticFieldID(cls, "TAG", "Ljava/lang/String;");
        if (jFd != nullptr) {
            jstring tag = (jstring) env->GetStaticObjectField(cls, jFd);
            std::string ret = env->GetStringUTFChars(tag, nullptr);
            LOGW("Native get Java TAG: %s\n", ret.c_str());
        }

    }

}

同理也能获取Java层中对象的属性,感兴趣的可以自行尝试一下

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值