JNI编程基础(一)

简介: JNI-Java Native Interface,是Java平台提供的一个特性,通过编写JNI函数实现Java代码调用C/C++代码以及C/C++代码调用Java代码的作用。从而达到利用不同语言的特点

JNI-Java Native Interface,是Java平台提供的一个特性,通过编写JNI函数实现Java代码调用C/C++代码以及C/C++代码调用Java代码的作用。从而达到利用不同语言的特点。为什么需要在Java中调用C/C++代码,在我看来最主要有以下三点:

  • C/C++代码相比Java有着更高的性能
  • C/C++代码更难被反编译,有更好的安全性
  • 通过JNI函数可以绕开JVM的限制,完成一些在Java层面实现不了的功能。典型的例子就是Android热修复框架AndFix

既然要实现C/C++和java代码之间的交互,那么JVM就必须提供一整套的机制来实现相互之间的转换,具体来说涉及到以下三个方面:

  • JNI函数的注册
  • JNI层面和Java层面的数据结构对照
  • 描述符-用于描述类名或者数据类型

1.JNI函数的注册

所谓JNI函数的注册就是JVM能够准确的找到对应的JNI函数,并将其链接到主程序。注册分为动态注册和静态注册,接下来通过一个例子来说明如何实现JNI函数的静态和动态注册。

1.例子

public class AndroidJni {
    static{
        System.loadLibrary("main");
    }
    public native void dynamicLog();
    public native void staticLog();
}

这是一个普通的Java类,类中申明了两个native函数,dynamicLog和staticLog。native关键字告诉JVM,两个函数是通过JNI实现的,那么在哪里去找这两个函数JNI实现呢?注意,在这个类初始化的时候加载一个库叫做main。没错,JVM就是会去main(如果是Linux平台,这个库就是libmain.so)这个库中去找对应的函数。对应的C++代码如下:

#include <jni.h>
#define LOG_TAG "main.cpp"
#include "mylog.h"
static void nativeDynamicLog(JNIEnv *evn, jobject obj){
    LOGE("hell main");
}
JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj)
{
      LOGE("static register log ");
}

JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog},};
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env;
    if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        return -1;
    }
    LOGE("JNI_OnLoad comming");
    jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");
    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
    return JNI_VERSION_1_4;
}

这里引用了两个头文件,jni.h和mylog.h,其中jni.h是定义

1.静态注册

在上面的代码中看到了JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数,在Java虚拟机加载的时候会链接对应的native方法,在AndroidJni.java的类中声明了staticLog()为native方法,他对应的JNI函数就是Java_com_github_songnick_jni_AndroidJni_staticLog(),那么是怎么链接的呢,在Java虚拟机加载so库时,如果发现含有上面两个宏定义的函数时就会链接到对应Java层的native方法,那么怎么知道对应Java中的哪个类的哪个native方法呢,我们仔细观察JNI函数名的构成其实是:Java_PkgName_ClassName_NativeMethodName,以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了。一般情况下我们可以自己手动的去按照这个规则写,但是如果native方法特别多,那么还是有一定的工作量,并且在写的过程中不小心就有可能写错,其实Java给我们提供了javah的工具帮助生成相应的头文件。在生成的头文件中就是按照上面说的规则生成了对应的JNI函数,我们在开发的时候直接copy过去就可以了。这里上面的代码为例,在AndroidStudio中编译后,进入项目的目录app/build/intermediates/classes/debug下,运行如下命令:

javah -d jni com.github.songnick.jni.AndroidJni

这里-d指定生成.h文件存放的目录(如果没有就会自动创建),com.github.songnick.jni.AndroidJni表示指定目录下的class文件。这里简单介绍一下生成的JNI函数包含两个固定的参数变量,分别是JNIEnv和jobject,其中JNIEnv后面会介绍,jobject就是当前与之链接的native方法隶属的类对象(类似于Java中的this)。这两个变量都是Java虚拟机生成并在调用时传递进来的。

2.动态注册

上面我们介绍了静态注册native方法的过程,就是Java层声明的native方法和JNI函数是一一对应的,那么有没有方法让Java层的native方法和任意的JNI函数链接起来,当然是可以的,这就得使用动态注册的方法。接下来就看看如何实现动态注册的。

1) JNI_OnLoad函数

 当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数,因此可以在该函数中做一些初始化的动作,其实这个函数就是相当于Activity中的onCreate()方法。该函数前面有三个关键字,分别是JNIEXPORT、JNICALL和jint,其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数是JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层,至于这些数据类型我们在后面介绍。这里的jint对应Java的int数据类型,该函数返回的int表示当前使用的JNI的版本,其实类似于Android系统的API版本一样,不同的JNI版本中定义的一些不同的JNI函数。该函数会有两个参数,其中*jvm为Java虚拟机实例,JavaVM结构体定义了以下函数:

DestroyJavaVM
   AttachCurrentThread
   DetachCurrentThread
   GetEnv

这里我们使用了GetEnv函数获取JNIEnv变量,上面的JNI_OnLoad函数中有如下代码:

JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
    return -1;
}

这里调用了GetEnv函数获取JNIEnv结构体指针,其实JNIEnv结构体是指向一个函数表的,该函数表指向了对应的JNI函数,我们通过调用这些JNI函数实现JNI编程,在后面我们还会对其进行介绍。

获取Java对象,完成动态注册

上面介绍了如何获取JNIEnv结构体指针,得到这个结构体指针后我们就可以调用JNIEnv中的RegisterNatives函数完成动态注册native方法了。该方法如下:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)11

第一个参数是Java层对应包含native方法的对象(这里就是AndroidJni对象),通过调用JNIEnv对应的函数获取class对象(FindClass函数的参数为需要获取class对象的类描述符):

jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");11

第二个参数是JNINativeMethod结构体指针,这里的JNINativeMethod结构体是描述Java层native方法的,它的定义如下:

typedef struct {
    const char* name;//Java层native方法的名字
    const char* signature;//Java层native方法的描述符
    void*       fnPtr;//对应JNI函数的指针
} JNINativeMethod;

第三个参数为注册native方法的数量。一般会动态注册多个native方法,首先会定义一个JNINativeMethod数组,然后将该数组指针作为RegisterNative函数的参数传入,所以这里定义了如下的JNINativeMethod数组:

JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog}};11

最后调用RegisterNative函数完成动态注册:

env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));11

2JNI数据结构

  1. JNIENV结构体

JNIENV是一个JNI环境结构体,结构体重维护了一系列的函数,通过这些环境函数可以实现与Java层的交互。下图是JNIENV成员函数的一部分:

..........
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
CallVoidMethod(jobject obj, jmethodID methodID, ...)
CallBooleanMethod(jobject obj, jmethodID methodID, ...)
..........

从上面罗列的几个方法可以看出,通过JNIENV我们可以轻易地获取到一个Java类中的域,方法并操作这些成员。

  1. JNI数据类型

虽然JNI和Java都包含很多相同的数据类型,但是其定义却并不一样,所以Java的数据类型需要经过转换才能在JNI层面被操作。接下来就是Java和JNI数据类型的对照:

1)基础类型

| Java Type| Native Type | Description |
| --- | --- | --- |
| boolean | jboolean | unsigned 8 bits |
| byte | jbyte | signed 8 bits |
| char | jchar | unsigned 16 bits |
| short | jshort | signed 16 bits |
|int | jint | signed 32 bits |
|long | jlong | signed 64 bits |
|float | jfloat | 32 bits |
|double | jdouble| 64 bits |
|void | void | N/A |

2) 应用类型

jobject                     (all Java objects)
|
|-- jclass                    (java.lang.Class objects)
|-- jstring                    (java.lang.String objects)
|-- jarray                    (array)
|      |--jobjectArray       (object arrays)
|      |--jbooleanArray        (boolean arrays)
|      |--jbyteArray            (byte arrays)
|      |--jcharArray            (char arrays)
|      |--jshortArray        (short arrays)
|      |--jintArray            (int arrays)
|     |--jlongArray            (long arrays)
|      |--jfloatArray        (float arrays)
|      |--jdoubleArray         (double arrays)
|
|--jthrowable

3) 方法和变量的ID

 当需要调用Java中的某个方法的时候我们首先要获取它的ID,根据ID调用JNI函数获取该方法,变量的获取过程也是同样的过程,这些ID的结构体定义如下:

    struct _jfieldID;              /* opaque structure */ 
    typedef struct _jfieldID *jfieldID;   /* field IDs */ 
    
    struct _jmethodID;              /* opaque structure */ 
    typedef struct _jmethodID *jmethodID; /* method IDs */
  1. 描述符

1.类描述符

 前面为了获取Java的AndroidJni对象,是通过调用FindClass()函数获取的,该函数参数只有一个字符串参数,我们发现该字符串如下所示:

    com/github/songnick/jni/AndroidJni11

其实这个就是JNI定义了对类的描述符,它的规则就是将”com.github.songnick.jni.AndroidJni”中的“.”用“/”代替。

2.方法描述符

 前面我们动态注册native方法的时候结构体JNINativeMethod中含有方法描述符,就是确定native方法的参数和返回值,我们这里定义的dynamicLog()方法没有参数,返回值为空所以对应的描述符为:”()V”,括号类为参数,V表示返回值为空。下面还是看看几个栗子吧:

| Method Descriptor | Java Language Type |
| --- | --- |
|“()Ljava/lang/String;” | String f(); |
|“(ILjava/lang/Class;)J”| long f(int i, Class c);|
|“([B)V” | String(byte[] bytes); |

上面的栗子我们看到方法的返回类型和方法参数有引用类型以及boolean、int等基本数据类型,对于这些类型的描述符在下个部分介绍。这里数组的描述符以”[“和对应的类型描述符来表述。对于二维数组以及三维数组则以”[[“和”[[[“表示:

|Descriptor |Java Langauage Type|
| --- | --- |
|“[[I” | int |
|“[[[D” | double[] |

3.数据类型描述符

 前面我们说了方法的描述符,那么针对boolean、int等数据类型描述符是怎样的呢,JNI对基本数据类型的描述符定义如下:

| Field Desciptor | Java Language Type |
| --- | ---- |
| Z | boolean |
| B | byte |
| C | char |
|S | short |
|I | int |
|J | long |
|F | float |
|D | double |

对于引用类型描述符是以”L”开头”;”结尾,示例如下所示:

| Field Desciptor | Java Language Type |
| --- | --- |
| “Ljava/lang/String;” | String |
|“[Ljava/lang/Object;” | Object[] |

相关文章
|
16天前
|
JSON 机器人 Linux
推荐一款嵌入式Linux开源框架与封装-cpp-tbox
推荐一款嵌入式Linux开源框架与封装-cpp-tbox
48 3
|
2月前
|
设计模式 JavaScript 前端开发
简述框架与函数库的区别
简述框架与函数库的区别
10 1
|
3月前
编译期编程
编译期编程
|
3月前
|
Rust Java Linux
【一起学Rust | 进阶篇 | jni库】JNI实现Java与Rust进行交互
【一起学Rust | 进阶篇 | jni库】JNI实现Java与Rust进行交互
73 0
|
6月前
|
编译器 Linux C语言
lua编程基础
lua编程基础
51 0
|
12月前
|
缓存 Java 编译器
JNI基础简介
JNI系列入门连载,开启JNI学习之旅吧。
79 0
|
IDE Java 开发工具
JNI的开发方法
本文通过一个案例,教读者一步一步实现一个简单的JNI项目。
114 0
JNI的开发方法
|
编解码 Java Android开发
so库你应该知道的基础知识
Android系统目前支持以下七种不同的CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MIPS (从2012年起),ARMv8,MIPS64和x86_64 (从2014年起),每一种都关联着一个相应的ABI。
211 0
|
NoSQL Java Linux
JNI 调试技术
如果你像我一样是一个 Java 程序员,并且经常进行 JNI 代码的开发,那么你一定也体会到了调试 JNI 代码的困难,比如有一天突然程序意外崩溃了,我们很难搞清楚它到底是因为什么崩溃的。接下来我要介绍的这几个技术,可以帮助我们快速的解决上述问题。