欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

程序员文章站 2022-11-07 07:52:02
目录 "用法解析" "├── 1、JNI函数" "│ ├── 1.1、extern "C"" "│ ├── 1.2、JNIEXPORT、JNICALL" "│ ├── 1.3、函数名" "│ ├── 1.4、JNIEnv" "│ ├── 1.5、jobject" "├── 2、Java、JNI、C/ ......

目录


├── 1、jni函数
│ ├── 1.1、extern "c"
│ ├── 1.2、jniexport、jnicall

│ ├── 1.4、jnienv

├── 2、java、jni、c/c++基本类型映射关系
├── 3、jni描述符(签名)



│ ├── 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,结果直接闪退了:

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

咱们反编译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精细化讲解,让你彻底了解JNI(二):用法解析

通过查看 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中调用该函数时,找不到该函数,所以抛出了异常,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

宏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_ ,当我们在java中调用native方法时,jvm 也会根据这种命名规则来查找、调用native方法对应的 c 方法。

1.4、jnienv

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

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

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

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 的别名,如下图:

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

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

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

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

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

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、示例:
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加载动态库时,底层究竟做了哪些操作:

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

底层源码:/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)方法中注册的原因。

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

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

如上图所示:

流程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个函数。当动态注册时,需调用函数 (env)->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*对象对应的呢?这个有点复杂了,咱们通过流程图简单描述一下吧:

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析
如果还希望更清晰的了解底层源码的实现逻辑,可下载android源码,自行分析一下吧。

4.2、静态注册原理

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

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析
如上图所示:

流程1:java代码中调用java native函数;
流程2:获得method*对象,默认为该函数的method*设置nativefunc(dvmresolvenativemethod);
流程3:dvmresolvenativemethod函数中按照特定名称查找对应的c方法;
流程4:如果找到了对应的c方法,重新为该方法设置method*属性;

注意:当java代码中第二次再调用java native函数时,method*的nativefunc已经有值了
(即:dvmcalljnimethod,可参考动态注册流程内容),会直接执行method*的nativefunc的函数,不会在
重新执行特定名称查找了。

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析

4.3、java调用native的流程

安卓JNI精细化讲解,让你彻底了解JNI(二):用法解析
经过对动态注册、静态注册的实现原理的梳理之后,再看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函数时,不会再执行黄色部分的流程图了)