目录java
用法解析
├── 一、JNI函数
│ ├── 1.一、extern "C"
│ ├── 1.二、JNIEXPORT、JNICALL
│ ├── 1.三、函数名
│ ├── 1.四、JNIEnv
│ ├── 1.五、jobject
├── 二、Java、JNI、C/C++基本类型映射关系
├── 三、JNI描述符(签名)
├── 四、函数静态注册、动态注册
│ ├── 4.一、动态注册原理
│ ├── 4.二、静态注册原理
│ ├── 4.三、Java调用native的流程android
当经过AndroidStudio建立了Native C++工程后,首先面对的是*.cpp文件,对于不熟悉C/C++的开发人员而言,每每是望“类”兴叹,无从下手。为此,我们系统的梳理一下JNI的用法,为后续Native开发作铺垫。windows
#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方法相似,一样包含方法名、参数、返回类型,只不过多了一些修饰词、特定参数类型而已。数组
做用:避免编绎器按照C++的方式去编绎C函数缓存
该关键字能够删掉吗?
咱们不妨动手测试一下:去掉extern “C” , 从新生成so,运行app,结果直接闪退了: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++的函数重载差别有关系:源码分析
一、C不支持函数的重载,编译以后函数名不变; 二、C++支持函数的重载(这点与Java一致),编译以后函数名会改变; 缘由:在C++中,存在函数的重载问题,函数的识别方式是经过:函数名,函数的返回类型,函数参数列表 三者组合来完成的。
因此,若是但愿编译后的函数名不变,应通知编译器使用C的编译方式编译该函数(即:加上关键字:extern “C”)。测试
扩展: 若是即想去掉关键字 extern “C”,又但愿方法能被正常调用,真的不能实现吗? 非也,仍是有解决办法的:“函数的动态注册”,这个后面再介绍吧!!!
做用:
JNIEXPORT 用来表示该函数是否可导出(即:方法的可见性)
JNICALL 用来表示函数的调用规范(如:__stdcall)this
咱们经过JNIEXPORT、JNICALL关键字跳转到jni.h中的定义,以下图:
经过查看 jni.h 中的源码,原来JNIEXPORT、JNICALL是两个宏定义
对于安卓开发者来讲,宏可这样理解: ├── 宏 JNIEXPORT 表明的就是右侧的表达式: __attribute__ ((visibility ("default"))) ├── 或者也能够说: JNIEXPORT 是右侧表达式的别名 宏可表达的内容不少,如:一个具体的数值、一个规则、一段逻辑代码等;
attribute___((visibility ("default"))) 描述的是“可见性”属性 visibility
一、default :表示外部可见,相似于public修饰符 (即:能够被外部调用) 二、hidden :表示隐藏,相似于private修饰符 (即:只能被内部调用) 三、其余 :略
若是,咱们想使用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
看到.cpp中的函数"Java_com_qxc_testnativec_MainActivity_stringFromJNI",大部分开发人员都会有疑问:咱们定义的native函数名stringFromJNI,为何对应到cpp中函数名会变成这么长呢?
public native String stringFromJNI();
这跟JNI native函数的注册方式有关
JNI Native函数有两种注册方式(后面会详细介绍): 一、静态注册:按照JNI接口规范的命名规则注册; 二、动态注册:在.cpp的JNI_OnLoad方法里注册;
JNI接口规范的命名规则:
通常是 Java_
JNIEnv 表明了Java环境,经过JNIEnv*就能够对Java端的代码进行操做,如:
├──建立Java对象
├──调用Java对象的方法
├──获取Java对象的属性等
咱们跳转、查看JNIEnv的源码实现,以下图:
JNIEnv指向_JNIEnv,而_JNIEnv是定义的一个C++结构体,里面包含了不少经过JNI接口(JNINativeInterface)对象调用的方法。
那么,咱们经过JNIEnv操做Java端的代码,主要使用哪些方法呢?
| 函数名称 | 做用 |
|:-----------------:| :-----------------:|
| NewObject | 建立Java类中的对象 |
| NewString | 建立Java类中的String对象 |
| New
| Get
| Set
| GetStatic
| SetStatic
| Call
| CallStatic
具体用法,后面案例再进行演示。
jobject 表明了定义native函数的Java类 或 Java类的实例:
├── 若是native函数是static,则表明类Class对象
├── 若是native函数非static,则表明类的实例对象
咱们能够经过jobject访问定义该native方法的成员方法、成员变量等。
上面,已经介绍了.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类型对应的呢?以下所示:
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* |
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 此处是指的:
一、若是是字段,表示字段类型的描述符 二、若是是函数,表示函数结构的描述符,即:每一个参数类型描述符 + 返回值类型描述符
举例( 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 |
JNI开发中,咱们通常定义了Java native方法,又写了对应的C方法实现。
那么,当咱们在Java代码中调用Java native方法时,虚拟机是怎么知道并调用SO库的对应的C方法的呢?
Java native方法与C方法的对应关系,实际上是经过注册实现的,Java native方法的注册形式有两种,一种是静态注册,另外一种是动态注册:
静态注册:按照JNI规范书写函数名:java_类路径_方法名(路径用下划线分隔)
动态注册:JNI_OnLoad中指定Java Native函数与C函数的对应关系
两种注册方式的使用对比:
一、优缺点: 系统默认方式,使用简单; 灵活性差(若是修改了java native函数所在类的包名或类名,需手动修改C函数名称(头文件、源文件)); 二、实现方式: 1)函数名能够根据规则手写 2)也可以使用javah命令自动生成 三、示例: 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()); }
一、优缺点: 函数名看着舒服一些,可是须要在C代码中维护Java Native函数与C函数的对应关系; 灵活性稍高(若是修改了java native函数所在类的包名或类名,仅调整Java native函数的签名信息) 二、实现方式 env->RegisterNatives(clazz, gMethods, numMethods) 三、示例: 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; }
上面,带着你们了解了两种注册方式的基本知识。接下来,我们再深刻了解一下动态注册和静态注册的底层差别、以及实现原理。
动态注册是Java代码调用中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个函数。当动态注册时,需调用函数 (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*对象对应的呢?这个有点复杂了,我们经过流程图简单描述一下吧:
若是还但愿更清晰的了解底层源码的实现逻辑,可下载Android源码,自行分析一下吧。
静态注册是在首次调用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的函数,不会在 从新执行特定名称查找了。
通过对动态注册、静态注册的实现原理的梳理以后,再看Java代码中调用Java native方法的流程图,就比较简单了:
一、若是是动态注册的Java native函数,System.loadLibray时就已经设置好了Java native函数与C函数的对应关系,当Java代码中调用Java native方法时,直接执行dvmCallJNIMethod桥函数便可(该函数中执行C函数)。
二、若是是静态注册的Java native函数,当Java代码中调用Java native方法时,默认为Method.nativeFunc赋值为dvmResolveNativeMethod,并按特定名称查找C方法,从新赋值Method*,最终仍然是执行dvmCallJNIMethod桥函数(只不过Java代码中第二次再调用静态注册的Java native函数时,不会再执行黄色部分的流程图了)