zoukankan      html  css  js  c++  java
  • 安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

    目录

    用法解析
    ├── 1、JNI函数
    │ ├── 1.1、extern "C"
    │ ├── 1.2、JNIEXPORT、JNICALL
    │ ├── 1.3、函数名
    │ ├── 1.4、JNIEnv
    │ ├── 1.5、jobject
    ├── 2、Java、JNI、C/C++基本类型映射关系
    ├── 3、JNI描述符(签名)
    ├── 4、函数静态注册、动态注册
    │ ├── 4.1、动态注册原理
    │ ├── 4.2、静态注册原理
    │ ├── 4.3、Java调用native的流程

    当通过AndroidStudio创建了Native C++工程后,首先面对的是*.cpp文件,对于不熟悉C/C++的开发人员而言,往往是望“类”兴叹,无从下手。为此,咱们系统的梳理一下JNI的用法,为后续Native开发做铺垫。

    1、JNI函数

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

    通常,大家看到的JNI方法如上图所示,方法结构与Java方法类似,同样包含方法名、参数、返回类型,只不过多了一些修饰词、特定参数类型而已。

    1.1、extern "C"

    作用:避免编绎器按照C++的方式去编绎C函数

    该关键字可以删掉吗?
    我们不妨动手测试一下:去掉extern “C” , 重新生成so,运行app,结果直接闪退了:

    咱们反编译so文件看一下,原来去掉extern “C” 后,函数名字竟然被修改了:

    //保留extern "C"
    000000000000ea98 T 
    Java_com_qxc_testnativec_MainActivity_stringFromJNI
    
    //去掉extern "C"
    000000000000eab8 T 
    _Z40Java_com_qxc_testnativec_MainActivity_stringFromJNIP7_JNIEnvP8_jobject
    

    原因是什么呢?
    其实这跟C和C++的函数重载差异有关系:

    1、C不支持函数的重载,编译之后函数名不变;
    2、C++支持函数的重载(这点与Java一致),编译之后函数名会改变;
    
    原因:在C++中,存在函数的重载问题,函数的识别方式是通过:函数名,函数的返回类型,函数参数列表
    三者组合来完成的。
    

    所以,如果希望编译后的函数名不变,应通知编译器使用C的编译方式编译该函数(即:加上关键字:extern “C”)。

    扩展:
    如果即想去掉关键字 extern “C”,又希望方法能被正常调用,真的不能实现吗?
    
    非也,还是有解决办法的:“函数的动态注册”,这个后面再介绍吧!!!
    
    1.2、JNIEXPORT、JNICALL
    作用:

    JNIEXPORT 用来表示该函数是否可导出(即:方法的可见性)
    JNICALL 用来表示函数的调用规范(如:__stdcall)

    我们通过JNIEXPORT、JNICALL关键字跳转到jni.h中的定义,如下图:

    通过查看 jni.h 中的源码,原来JNIEXPORT、JNICALL是两个宏定义

    对于安卓开发者来说,宏可这样理解:
    
    ├── 宏 JNIEXPORT 代表的就是右侧的表达式: __attribute__ ((visibility ("default")))
    ├── 或者也可以说: JNIEXPORT 是右侧表达式的别名
    
    宏可表达的内容很多,如:一个具体的数值、一个规则、一段逻辑代码等;
    

    attribute___((visibility ("default"))) 描述的是“可见性”属性 visibility

    1、default :表示外部可见,类似于public修饰符 (即:可以被外部调用)
    2、hidden :表示隐藏,类似于private修饰符 (即:只能被内部调用)
    3、其他 :略
    

    如果,我们想使用hidden,隐藏我们写的方法,可这么写:

    #include <jni.h>
    #include <string>
    
    extern "C" __attribute__ ((visibility ("hidden"))) jstring JNICALL
    Java_com_qxc_testnativec_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    

    重新编译、运行,结果闪退了。
    原因:函数Java_com_qxc_testnativec_MainActivity_stringFromJNI已被隐藏,而我们在java中调用该函数时,找不到该函数,所以抛出了异常,如下图:

    宏JNICALL 右边是空的,说明只是个空定义。上面讲了,宏JNICALL代表的是右边定义的内容,那么,我们代码也可直接使用右边的内容(空)替换调JNICALL(即:去掉JNICALL关键字),编译后运行,调用so仍然是正确的:

    #include <jni.h>
    #include <string>
    
    extern "C" JNIEXPORT jstring
    Java_com_qxc_testnativec_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    JNICALL 知识扩展:
    
    JNICALL的定义,并非所有平台都像Linux一样是空的,如windows平台:
    #ifndef _JAVASOFT_JNI_MD_H_  
    #define _JAVASOFT_JNI_MD_H_  
    #define JNIEXPORT __declspec(dllexport)  
    #define JNIIMPORT __declspec(dllimport)  
    #define JNICALL __stdcall  
    typedef long jint;  
    typedef __int64 jlong;  
    typedef signed char jbyte;  
    #endif
    
    1.3、函数名

    看到.cpp中的函数"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分开发人员都会有疑问:我们定义的native函数名stringFromJNI,为什么对应到cpp中函数名会变成这么长呢?

    public native String stringFromJNI();
    

    这跟JNI native函数的注册方式有关

    JNI Native函数有两种注册方式(后面会详细介绍):
    1、静态注册:按照JNI接口规范的命名规则注册;
    2、动态注册:在.cpp的JNI_OnLoad方法里注册;
    

    JNI接口规范的命名规则:

    Java_<PackageName>_<ClassName>_<MethodName> 
    

    当我们在Java中调用native方法时,JVM 也会根据这种命名规则来查找、调用native方法对应的 C 方法。

    1.4、JNIEnv

    JNIEnv 代表了Java环境,通过JNIEnv*就可以对Java端的代码进行操作,如:
    ├──创建Java对象
    ├──调用Java对象的方法
    ├──获取Java对象的属性等

    我们跳转、查看JNIEnv的源码实现,如下图:

    JNIEnv指向_JNIEnv,而_JNIEnv是定义的一个C++结构体,里面包含了很多通过JNI接口(JNINativeInterface)对象调用的方法。

    那么,我们通过JNIEnv操作Java端的代码,主要使用哪些方法呢?

    函数名称 作用
    NewObject 创建Java类中的对象
    NewString 创建Java类中的String对象
    NewArray 创建类型为Type的数组对象
    GetField 获得类型为Type的字段
    SetField 设置类型为Type的字段
    GetStaticField 获得类型为Type的static的字段
    SetStaticField 设置类型为Type的static的字段
    CallMethod 调用返回值类型为Type的static方法
    CallStaticMethod 调用返回值类型为Type的static方法

    具体用法,后面案例再进行演示。

    1.5、jobject

    jobject 代表了定义native函数的Java类 或 Java类的实例:

    ├── 如果native函数是static,则代表类Class对象
    ├── 如果native函数非static,则代表类的实例对象

    我们可以通过jobject访问定义该native方法的成员方法、成员变量等。

    2、Java、JNI、C/C++基本类型映射关系

    上面,已经介绍了.cpp方法的基本结构、主要关键字。当我们定义了具体方法,写C/C++方法实现时,会用到各种参数类型。那么,在JNI开发中,这些类型应该是怎么写呢?
    举例:定义加、减、乘、除的方法

    //加
    jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a+b;
    }
    //减
    jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a-b;
    }
    //乘
    jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a*b;
    }
    //除
    jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a/b;
    }
    

    通过上面案例可以看到,几个方法的后两个参数、返回值,类型都是 jint

    jint 是JNI中定义的类型别名,对应的是Java、C++中的int类型
    

    我们先源码跟踪、看下jint的定义,jint 原来是 jni.h中 定义的 int32_t 的别名,如下图:

    根据 int32_t 查找,发现 int32_t 是 stdint.h中定义的 __int32_t的别名,如下图:

    再根据 __int32_t 查找,发现 __int32_t 是 stdint.h中定义的 int 的别名(这个也就是C/C++中的int类型了),如下图:

    Java 、C/C++都有一些常用的数据类型,分别是如何与JNI类型对应的呢?如下所示:

    Java 、C/C++中的常用数据类型的映射关系表(通过源码跟踪查找列出来的)
    JNI中定义的别名 Java类型 C/C++类型
    jint / jsize int int
    jshort short short
    jlong long long / long long (__int64)
    jbyte byte signed char
    jboolean boolean unsigned char
    jchar char unsigned short
    jfloat float float
    jdouble double double
    jobject Object _jobject*

    3、JNI描述符 (签名)

    JNI开发时,我们除了写本地C/C++实现,还可以通过 JNIEnv *env 调用Java层代码,如获得某个字段、获取某个函数、执行某个函数等:

    //获得某类中定义的字段id
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
        { return functions->GetFieldID(this, clazz, name, sig); }
    
    //获得某类中定义的函数id
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
        { return functions->GetMethodID(this, clazz, name, sig); }
    

    上面的函数与Java的反射比较类似,参数:

    clazz : 类的class对象
    name : 字段名、函数名
    sig : 字段描述符(签名)、函数描述符(签名)

    写过反射的开发人员对clazz、name这两个参数应该比较熟悉,对sig稍微陌生一些。

    sig 此处是指的:

    1、如果是字段,表示字段类型的描述符
    2、如果是函数,表示函数结构的描述符,即:每个参数类型描述符 + 返回值类型描述符
    

    举例( int 类型的描述符是 大写的 I ):

    Java代码:
    
    public class Hello{
         public int property;
         public int fun(int param, int[] arr){
              return 100;
         }
    }
    
    JNI C/C++代码:
    
    JNIEXPORT void Java_Hello_test(JNIEnv* env, jobject obj){
        jclass myClazz = env->GetObjectClass(obj);
        jfieldId fieldId_prop = env -> GetFieldId(myClazz, "property", "I");
        jmethodId methodId_fun = env -> GetMethodId(myClazz, "fun", "(I[I)I");
    }
    

    由上面的示例可以看到,Java类中的字段类型、函数定义分别对应的描述符:

    int  类型 对应的是  I
    fun  函数 对应的是  (I[I)I
    

    其他类型的描述符(签名)如下表:

    Java类型 字段描述符(签名) 备注
    int I int的首字母、大写
    float F float的首字母、大写
    double D double的首字母、大写
    short S short的首字母、大写
    long L long的首字母、大写
    char C char的首字母、大写
    byte B byte的首字母、大写
    boolean Z 因B已被byte使用,所以JNI规定使用Z
    object L + /分隔完整类名 String 如: Ljava/lang/String
    array [ + 类型描述符 int[] 如:[I
    Java函数 函数描述符(签名) 备注
    void V 无返回值类型
    Method (参数字段描述符...)返回值字段描述符 int add(int a,int b) 如:(II)I

    4、函数静态注册、动态注册

    JNI开发中,我们一般定义了Java native方法,又写了对应的C方法实现。
    那么,当我们在Java代码中调用Java native方法时,虚拟机是怎么知道并调用SO库的对应的C方法的呢?

    Java native方法与C方法的对应关系,其实是通过注册实现的,Java native方法的注册形式有两种,一种是静态注册,另一种是动态注册:

    静态注册:按照JNI规范书写函数名:java_类路径_方法名(路径用下划线分隔)
    动态注册:JNI_OnLoad中指定Java Native函数与C函数的对应关系

    两种注册方式的使用对比:

    静态注册:
    1、优缺点:
    系统默认方式,使用简单;
    灵活性差(如果修改了java native函数所在类的包名或类名,需手动修改C函数名称(头文件、源文件));
    
    2、实现方式:
    1)函数名可以根据规则手写
    2)也可使用javah命令自动生成
    
    3、示例:
    extern "C" JNIEXPORT jstring
    Java_com_qxc_testnativec_MainActivity_stringFromJNI(
            JNIEnv* env,
            jobject /* this */) {
        std::string hello = "Hello from C++";
        return env->NewStringUTF(hello.c_str());
    }
    
    动态注册:
    1、优缺点:
    函数名看着舒服一些,但是需要在C代码中维护Java Native函数与C函数的对应关系;
    灵活性稍高(如果修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息)
    
    2、实现方式
    env->RegisterNatives(clazz, gMethods, numMethods)
    
    3、示例:
    Java类定义Native函数:
    
    package com.qxc.testpage;
    public class JNITools {
        static {
            System.loadLibrary("jnidemo");
        }
    
        //加法
        public static native int  add(int a,int b);
    
        //减法
        public static native int sub(int a,int b);
    
        //乘法
        public static native int mul(int a,int b);
    
        //除法
        public static native int div(int a,int b);
    }
    
    .cpp中动态注册:
    
    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
        //打印日志
        __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload");
        JNIEnv* env = NULL;
        jint result = -1;
        // 判断是否正确
        if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){
            return result;
        }
        // 定义函数映射关系(参数1:java native函数,参数2:函数描述符,参数3:C函数)
        const JNINativeMethod method[]={
                {"add","(II)I",(void*)addNumber},
                {"sub","(II)I",(void*)subNumber},
                {"mul","(II)I",(void*)mulNumber},
                {"div","(II)I",(void*)divNumber}
        };
        //找到对应的JNITools类
        jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools");
        //开始注册
        jint ret = (*env)->RegisterNatives(env,jClassName,method, 4);
         //如果注册失败,打印日志
        if (ret != JNI_OK) {
            __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error");
            return -1;
        }
        return JNI_VERSION_1_6;
    }
    
    //加
    jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a+b;
    }
    //减
    jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a-b;
    }
    //乘
    jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a*b;
    }
    //除
    jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a/b;
    }
    

    上面,带着大家了解了两种注册方式的基本知识。接下来,咱们再深入了解一下动态注册和静态注册的底层差异、以及实现原理。

    4.1、动态注册原理

    动态注册是Java代码调用中System.loadLibray()时完成的

    那么,我们先了解一下System.loadLibray加载动态库时,底层究竟做了哪些操作:

    System.loadLibray的流程图(为了便于大家理解,此图省略了部分流程)

    底层源码:/dalvik/vm/Native.cpp
    
    dvmLoadNativeCode() -> JNI_OnLoad()
    //省略的代码......
    //将pNewEntry保存到gDvm全局变量nativeLibs中,下次可以直接通过缓存获取
    SharedLib* pActualEntry = addSharedLibEntry(pNewEntry);
    //省略的代码......
    //第一次加载so时,调用so中的JNI_OnLoad方法
    vonLoad = dlsym(handle, "JNI_OnLoad");
    

    通过System.loadLibray的流程图,不难看出,Java中加载.so动态库时,最终会调用so中的JNI_OnLoad方法,这也是为什么我们要在C的JNIEXPORT jint JNI_OnLoad(JavaVM vm, void* reserved)方法中注册的原因。

    接下来,咱们再深入了解一下动态注册的具体流程:

    动态注册的具体流程图(为了便于大家理解,此图省略了部分流程)

    如上图所示:

    流程1:是指执行 System.loadLibray函数;
    流程2:是指底层默认调用so中的JNI_OnLoad函数;
    流程3:是指开发人员在JNI_OnLoad中写的注册方法,例如: (*env)->RegisterNatives(env,.....)
    流程4:需要重点讲解一下:
    ├── 在Android中,不管是Java函数还是Java Native函数,它在虚拟机中对应的都是一个Method*对象
    ├── 如果是Java Native函数,那么Method*对象的nativeFunc会指向一个bridge函数dvmCallJNIMethod
    ├── 当调用Java Native函数时,就会执行该bridge函数,bridge函数的作用是调用该Java Native方法对应的
    JNI方法,即: method.insns
    
    流程4的主要作用,如图所示,为Java Native函数对应的Method*对象,绑定属性,建立对应关系:
    ├── nativeFunc 指向函数 dvmCallJNIMethod(通常情况下)
    ├── insns 指向native层的C函数指针 (我们写的C函数)
    

    我们再从源码层面,重点分析一下动态注册的流程3和流程4吧。

    流程3:开发人员在JNI_OnLoad中写的注册方法,注册对应的C函数

    JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved){
        //打印日志
        __android_log_print(ANDROID_LOG_DEBUG,"JNITag","enter jni_onload");
        JNIEnv* env = NULL;
        jint result = -1;
        // 判断是否正确
        if((*vm)->GetEnv(vm,(void**)&env,JNI_VERSION_1_6)!= JNI_OK){
            return result;
        }
        // 定义函数映射关系(参数1:java native函数,参数2:函数描述符,参数3:C函数)
        const JNINativeMethod method[]={
                {"add","(II)I",(void*)addNumber},
                {"sub","(II)I",(void*)subNumber},
                {"mul","(II)I",(void*)mulNumber},
                {"div","(II)I",(void*)divNumber}
        };
        //找到对应的JNITools类
        jclass jClassName=(*env)->FindClass(env,"com/qxc/testpage/JNITools");
        //开始注册
        jint ret = (*env)->RegisterNatives(env,jClassName,method, 4);
         //如果注册失败,打印日志
        if (ret != JNI_OK) {
            __android_log_print(ANDROID_LOG_DEBUG, "JNITag", "jni_register Error");
            return -1;
        }
        return JNI_VERSION_1_6;
    }
    
    //加
    jint addNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a+b;
    }
    //减
    jint subNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a-b;
    }
    //乘
    jint mulNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a*b;
    }
    //除
    jint divNumber(JNIEnv *env,jclass clazz,jint a,jint b){
         return a/b;
    }
    

    C函数的定义比较简单,共加减乘除4个函数。当动态注册时,需调用函数 RegisterNatives(env,jClassName,method, 4)(该方法有不同参数的多个方法重载),我们主要关注的参数:jclass clazz、JNINativeMethod* methods、jint nMethods

    clazz 表示:定义Java Native方法的Java类;
    methods 表示:Java Native方法与C方法的对应关系;
    nMethods 表示:methods注册方法的数量,一般设置成methods数组的长度;

    JNINativeMethod如何表示Java Native方法与C方法的对应关系的呢?查看其源码定义:

    jni.h
    
    //结构体
    typedef struct {
        const char* name;   //Java 方法名称
        const char* signature;  //Java 方法描述符(签名)
        void*       fnPtr;  //C/C++方法实现
    } JNINativeMethod;
    

    了解了JNINativeMethod结构,那么,JNINativeMethod对象是如何与虚拟机中的Method*对象对应的呢?这个有点复杂了,咱们通过流程图简单描述一下吧:

    动态注册的源码流程图(为了便于大家理解,此图省略了部分流程)

    dvmSetNativeFunc源码分析
    如果还希望更清晰的了解底层源码的实现逻辑,可下载Android源码,自行分析一下吧。

    4.2、静态注册原理

    静态注册是在首次调用Java Native函数时完成的

    静态注册的具体流程图(为了便于大家理解,此图省略了部分流程)
    如上图所示:

    流程1:Java代码中调用Java Native函数;
    流程2:获得Method*对象,默认为该函数的Method*设置nativeFunc(dvmResolveNativeMethod);
    流程3:dvmResolveNativeMethod函数中按照特定名称查找对应的C方法;
    流程4:如果找到了对应的C方法,重新为该方法设置Method*属性;
    
    注意:当Java代码中第二次再调用Java Native函数时,Method*的nativeFunc已经有值了
    (即:dvmCallJNIMethod,可参考动态注册流程内容),会直接执行Method*的nativeFunc的函数,不会在
    重新执行特定名称查找了。
    

    静态注册流程2 源码分析

    静态注册流程3、4 源码分析

    4.3、Java调用native的流程

    Java代码中调用Java native的流程图(为了便于大家理解,此图省略了部分流程)
    经过对动态注册、静态注册的实现原理的梳理之后,再看Java代码中调用Java native方法的流程图,就比较简单了:

    1、如果是动态注册的Java native函数,System.loadLibray时就已经设置好了Java native函数与C函数的对应关系,当Java代码中调用Java native方法时,直接执行dvmCallJNIMethod桥函数即可(该函数中执行C函数)。

    2、如果是静态注册的Java native函数,当Java代码中调用Java native方法时,默认为Method.nativeFunc赋值为dvmResolveNativeMethod,并按特定名称查找C方法,重新赋值Method*,最终仍然是执行dvmCallJNIMethod桥函数(只不过Java代码中第二次再调用静态注册的Java native函数时,不会再执行黄色部分的流程图了)

  • 相关阅读:
    关于java异常处理的自我学习
    html学习
    java第七周动手动脑
    作业
    动手动脑
    我要建立自己的java代码仓库
    第三周作业
    day0319 模块
    day0318装饰器和内置函数
    day0315 迭代器
  • 原文地址:https://www.cnblogs.com/qixingchao/p/11911787.html
Copyright © 2011-2022 走看看