美文网首页
jni学习总结

jni学习总结

作者: 会思考的鸭子 | 来源:发表于2020-12-24 14:05 被阅读0次

    一、jni是什么

    java代码要使用native的代码,需要一个桥梁将他们连接起来,这个桥梁就是jni。


    JNI桥梁

    二、JNI的举例

    1、新建一个Android项目,在根目录下创建 jni文件夹,用于存放 C源码。
    2、在java代码中,创建一个本地方法 getStringFromC 本地方法没有方法体。

    private native String getStringFromC();
    

    3、在jni中创建一个C文件,定义一个函数实现本地方法,函数名必须用使用 本地方法的全类名,点改为下划线。

    #include <stdio.h>
    #include <stdlib.h>
    #include <jni.h>
    //方法名必须为本地方法的全类名点改为下划线,传入的两个参数必须这样写,
    //第一个参数为java虚拟机的内存地址的二级指针,用于本地方法与java虚拟机在内存中交互
    //第二个参数为一个java对象,即是哪个对象调用了这个 c方法
    jstring Java_com_mwp_jnihelloworld_MainActivity_getStringFromC(JNIEnv* env,jobject obj){
        //定义一个C语言字符串
        char* cstr = "hello form c";
        //返回值是java字符串,所以要将C语言的字符串转换成java的字符串
        //在jni.h 中定义了字符串转换函数的函数指针
        //jstring   (*NewStringUTF)(JNIEnv*, const char*);
        jstring jstr2 = (*env) -> NewStringUTF(env, cstr);
        return jstr2;
    }
    

    4、在jni中创建 Android.mk文件,用于配置 本地方法

    LOCAL_PATH := $(call my-dir)
    include $(CLEAR_VARS)
    #编译生成的文件的类库叫什么名字
    LOCAL_MODULE    := hello
    #要编译的c文件
    LOCAL_SRC_FILES := Hello.c
    include $(BUILD_SHARED_LIBRARY)
    

    5、在jni目录下执行 ndk-build.cmd指令,编译c文件
    6、在java代码中加载编译后生成的so类库,调用本地方法,将项目部署到虚拟机上之后就会发现toast弹出的C代码定义的字符串
    7、jni打包的C语言类库默认仅支持 arm架构,需要在jni目录下创建 Android.mk 文件添加如下代码可以支持x86架构

    APP_ABI := armeabi armeabi-v7a x86
    

    三、native方法注册

    native方法注册包括静态注册和动态注册,静态注册多用于NDK开发,上述的"二、JNI的举例"就是用的静态注册,而动态注册多用于Fremework开发,下面我们分别了解一下这两种注册方式

    1、静态注册

    在Android Studio中新建一个java library,命名为media,写一个简单的MediaRecorder.java(仿照系统的MediaRecorder.java)

    package com.example;
    public class MediaRecorder {
        static {
            System.loadLibrary("media_jni");
            native_init();
        }
        private static native final void native_init();
        public native void start() throws IllegalStateException;
    }
    

    然后进入项目的media/src/main/java/com/example目录执行如下命令:

    javac -h ./ MediaRecorder.java
    

    说明: javah从java10开始被移除掉,取而代之的是javac -h命令

    然后在当前目录会生成com_example_MediaRecorder.h文件,此文件的内容为

    /* DO NOT EDIT THIS FILE - it is machine generated */
    #include <jni.h>
    /* Header for class com_example_MediaRecorder */
    
    #ifndef _Included_com_example_MediaRecorder
    #define _Included_com_example_MediaRecorder
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*
     * Class:     com_example_MediaRecorder
     * Method:    native_init
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_com_example_MediaRecorder_native_1init
      (JNIEnv *, jclass);
    
    /*
     * Class:     com_example_MediaRecorder
     * Method:    start
     * Signature: ()V
     */
    JNIEXPORT void JNICALL Java_com_example_MediaRecorder_start
      (JNIEnv *, jobject);
    
    #ifdef __cplusplus
    }
    #endif
    #endif
    

    我们可以看到native_init的jni方法对应的是Java_com_example_MediaRecorder_native_1init,以"Java"开头说明是在java平台调用JNI方法的,后面的com_example_MediaRecorder_native_1init指的是包名+类名+方法名,中间用" _ "代替。另外我们注意到,原本"native_init"方法名中就有" _ ",为了消除歧义,这里的" _ "就会替换为" _1 "。
    JNIEnv是native世界中java环境的代表,通过JNIEnv*指针就可以在native世界中访问java世界的代码进行操作,它只能在创建它的线程中有效,不能跨进程传递。
    jclass是jni的数据类型,对应的是java的java.lang.Class实例。
    jobject也是jni的数据类型,对应java的object。

    当我们在java中调用natice_init方法时,就会从jni中寻找Java_com_example_MediaRecorder_native_1init,如果找到了该方法名的jni函数,就会建立联系,建立联系的方法就是保存jni的函数指针。通过方法名来建立联系的方式就是静态注册。

    2、动态注册

    从静态注册我们可以看出,只要java层和native层能够进行关联就能完成注册,静态注册采用的是方法名对应,然后保存jni函数指针的方法。动态注册其实就是不靠方法名来进行关联,而是换一种方式来记录"java的native方法"和"jni中的函数指针"的关系的注册方式。

    在jni中有一个专门的结构体来描述这种对应关系:


    JNINativeMethod

    Android系统的MediaRecorder采用的就是动态注册:


    MediaRecorder的JNINativeMethod数组

    通过这个数组,我们就能获取到java层的native函数和jni层的函数指针的一一对应关系,但知道了对应关系还不够,这里只是数组的声明,我们还得使用他,即调用注册函数,才能真正建立联系。

    注册函数一般流程:jni的register函数--->AndroidRuntime.registerNativeMethods()---->JNIEnv.RegisterNatives()

    四、数据类型转换

    通过natice方法的注册,我们已经找到了java层的函数和jni的函数指针的关联关系,但是他们之间相互调用,数据类型也要相匹配才行。换句话说,java层是一个int型变量,native层也需要有native所能理解的int才行,这就需要数据类型转换。
    数据类型转换我们又分为基本数据类型转换和引用数据类型转换。

    1、基本数据类型转换

    基本数据类型转换

    2、引用数据类型转换

    引用数据类型转换

    五、方法签名

    我们再来回顾下动态注册的JNINativeMethod数组:


    JNINativeMethod数组

    因为java中有函数的重载,所以只通过函数名,我们无法定位到java所指向的函数,于是我们通过方法签名来表示java层的函数的参数和返回值,从而达到定位。如上图数组的元素的第二个参数"()V"和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。
    jni的方法签名的格式为:
    (参数签名格式...)返回值签名格式。

    每次去手写签名格式显然是很心累的一件事,好在java提供了javap命令来自动生成签名。
    我们接着在刚才的shell窗口输入

    javap -s -p ./MediaRecorder.class
    
    javap命令

    六、解析JNIEnv

    JNIEnv是native世界中java环境的代表,它只在创建它的线程中有效,不能跨线程传递,不同线程的JNIEnv彼此独立。

    1、JNIEnv的定义:

    JNIEnv定义

    这里对c和c++做了区分,实际上我们深入源码可以发现,c++中的_JNIEnv结构体实际上又包含了JNINativeInterface,所以无论是c++还是c,最终都是靠JNINativeInterface来实现的,JNINativeInterface的定义如下:


    JNINativeInterface定义

    在JNINativeInterface中有很多JNIEnv结构体对应的函数指针,通过这些函数指针的定义,就能定位到虚拟机中的jni函数表,从而实现了jni层在虚拟机中的函数调用,这样jni层就可以调用java世界的方法了。
    这里我们又提到了虚拟机,我们再回调"JNIEnv定义"那幅图中,可以看到,除了JNINativeInterface,无论是c++还是c,都还有一个JavaVM变量,它就是虚拟机在jni层的代表,一个虚拟机进程只有一个JavaVM,因此所有线程通能使用这个JavaVM。通过JavaVM的attachCurrentThread函数可以获取到这个线程的JNIEnv,这样就能在不同的线程中调用java方法了。顺便提一下,在线程结束的时候,还需要DetachCurrentThread函数来释放资源。

    2、jfieldID和jmethodID

    JNIEnv最终都是调用的JNINativeInterface,在JNINativeInterface里面,我们可以看到,函数指针返回的类型就有jfieldID和jmethodID,当然还有其他的,不过都大同小异。jfieldID和jmethodID分别来代表java类中的成员变量和方法。
    我们来看一下系统层的MediaRecorder框架的jni层是如何使用GetMethodID
    和GetFieldID这两个方法的,如图所示:


    GetMethodID 和GetFieldID

    可以看到,首先会通过FindClass找到java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,clazz就是java层的mediaRecorder在jni层的代表,在注释2和注释3分别找到jfava层的MediaRecorder中名为mNativeContext和surface并缓存到fields中,注释4获取到java层的MediaRecorder中名为postEventFromNative方法,并缓存到fields中。这里为什么要进行缓存呢,第一个是因为效率问题,不用每次都查询,第二个是因为这些成员变量和方法都是本地引用,在android_media_MediaRecorder_native_init函数返回时这些本地引用会自动释放。本地引用后续会提到。

    3、使用jfieldID和jmethodID

    上述只是将jfieldID和jmethodID保存了起来,还没有使用到,那要怎么才能使用呢?如下图所示:


    使用jfieldID和jmethodID

    在注释1出调用了JNIEnv的CallStaticVoidMethod函数,其中就传入了缓存的fields.post_event,它其实是保存了java层MediaRecorder的静态方法postEventFromNative,也就是说JNIEnv的CallStaticVoidMethod函数可以访问java的静态方法,同理如果想要访问java的方法则可以使用JNIEnv的CallVoidMethod函数,如果想要想要访问java的属性,可以使用GetObjectField函数。


    GetObjectField

    七、jni的引用类型

    jni的引用类型分别是本地引用、全局引用、弱全局引用

    1、本地引用

    本地引用有以下三个特点:

    • 当native函数返回时,这个本地引用就会被自动释放
    • 只在创建它的线程中有效,不能够跨线程使用
    • 局部引用是JVM负责的引用类型,受JVM管理


      本地引用

      注释1处的FindClass会返回clazz,这个clazz就是本地引用,它会在android_media_MediaRecorder_native_init函数调用返回后被自动释放。

    2、全局引用

    全局引用有以下三个特点:

    • 在native函数返回时不会被自动释放,因此全局引用需要手动来进行释放,并且不会被GC回收
    • 全局引用是可以跨线程是用的
    • 全局引用不受到JVM管理
      JNIEnv的NewGlobalRef函数用来创建全局引用,JNIEnv的DeleteGlobalRef函数用来释放全局引用


      添加全局引用
      释放全局引用

    3、弱全局引用

    弱全局引用和全局引用特点差不多,区别是弱全局引用可以被GC回收,回收之后指向NULL,JNIEnv的NewWeakGlobalRef用来创建弱全局引用,JNIEnv的DeleteWeakGlobalRef用来释放弱全局引用。由于弱全局引用可能为NULL,因此使用前要想判断是否为空,使用JNIEnv的sSameObject进行判断


    弱全局引用判空

    相关文章

      网友评论

          本文标题:jni学习总结

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