美文网首页
JNI编程基础(一)

JNI编程基础(一)

作者: 李牙刷儿 | 来源:发表于2017-04-23 12:15 被阅读117次

    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
    1. 应用类型

      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

    2. 方法和变量的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[]

    相关文章

      网友评论

          本文标题:JNI编程基础(一)

          本文链接:https://www.haomeiwen.com/subject/rutyzttx.html