JNI编程
JNI是一种本地编程接口。它允许运行在JAVA虚拟机中的JAVA代码和用其他编程语言,诸如C语言、C++、汇编,写的应用和库之间的交互操作。
第一个JNI程序
#引入jni头文件支持include_directories("C:/Program Files/Java/jdk1.8.0_171/include")# windows:win32 Mac:darwininclude_directories("C:/Program Files/Java/jdk1.8.0_171/include/win32")# 修改生成动态库add_library(lsn7jni SHARED lsn7_jni.cpp)
最终你的动态库会生成在下面位置( 可以修改)
dll目录.png
然后打开一个Java IDE (AS/ECLIPSE/文本文档 都行)
例子:在AS的java单元测试下进行编写(此时使用的是电脑的java环境)
@Testpublicvoidaddition_isCorrect(){ assertEquals(4,2+2);//加载指定动态库 注意:当前我的电脑系统是64位System.load("xxxx/lsn7jni.dll");//调用jni方法System.out.println(test(1,"22",3.1f));}nativeStringtest(inti,String j,floatk);
然后在VS中编写
// lsn6_jni.cpp: 定义应用程序的入口点。//#include"jni.h"//c++中需要以c的方式编译extern"C"//JNIEnv: 由Jvm传入与线程相关的变量。定义了JNI系统操作、java交互等方法。//jobject: 表示当前调用对象,即 this , 如果是静态的native方法,则获得jclassJNIEXPORT jstring JNICALLJava_com_dongnao_jniTest_ExampleUnitTest_test(JNIEnv *env, jobject, jint i, jstring j, jfloat k){// 获得c字符串 // 开闭内存x,拷贝java字符串到x中 返回指向x的指针// 参数2 isCopy://提供一个boolean(int)指针,用于接收jvm传给我们的字符串是否是拷贝的。//通常,我们不关心这个,一般传个NULL就可以constchar* str = env->GetStringUTFChars(j, JNI_FALSE);charreturnStr[100];//格式化字符串sprintf(returnStr,"C++ string:%d,%s,%f",i,str,k);//释放掉内存 xenv->ReleaseStringUTFChars(j,str);// 返回java字符串returnenv->NewStringUTF(returnStr);}
现在第一个jni程序就已经能够运行了,在AS中运行单元测试进行jni方法的测试
对于不熟悉的同学可以,使用javah获得方法该如何声明。javah是JDK中提供的工具(Java环境变量不用说了吧?)
javah -o [输出文件名] [全限定名]
D:\Lance\ndk\lsn7_jni\JniTest\app\src\test\java> javah -o ExampleUnitTest.h com.dongnao.jniTest.ExampleUnitTest
JNI数据类型
JNIEXPORT 和 JNICALL,定义在jni_md.h头文件中。
JNIEXPORT:
Windows 中,定义为__declspec(dllexport)。因为Windows编译 dll 动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加此标识,表示将该函数导出在外部可以调用。
在 Linux/Unix/Mac os/Android 这种 Like Unix系统中,定义为__attribute__ ((visibility ("default")))
GCC 有个visibility属性, 启用这个属性gcc -fvisibility=xx:
当-fvisibility=hidden时
动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为__attribute__((visibility("default"))).
当-fvisibility=default时
动态库中的函数默认是可见的.除非显示声明为__attribute__((visibility("hidden"))).
JNICALL:
在类Unix中无定义,在Windows中定义为:_stdcall,一种函数调用约定
类Unix系统中这两个宏可以省略不加。
Java类型本地类型描述
booleanjbooleanC/C++8位整型
bytejbyteC/C++带符号的8位整型
charjcharC/C++无符号的16位整型
shortjshortC/C++带符号的16位整型
intjintC/C++带符号的32位整型
longjlongC/C++带符号的64位整型
floatjfloatC/C++32位浮点型
doublejdoubleC/C++64位浮点型
Objectjobject任何Java对象,或者没有对应java类型的对象
ClassjclassClass对象
Stringjstring字符串对象
Object[]jobjectArray任何对象的数组
boolean[]jbooleanArray布尔型数组
byte[]jbyteArray比特型数组
char[]jcharArray字符型数组
short[]jshortArray短整型数组
int[]jintArray整型数组
long[]jlongArray长整型数组
float[]jfloatArray浮点型数组
double[]jdoubleArray双浮点型数组
C/C++中获取java的数组时:extern"C"JNIEXPORT jstring JNICALLJava_com_dongnao_jnitest_MainActivity_test(JNIEnv *env, jobject instance, jobjectArray a_,jintArray b_){//1、 获得字符串数组//获得数组长度int32_tstr_length = env->GetArrayLength(a_); LOGE("字符串 数组长度:%d",str_length);//获得字符串数组的数据for(inti =0; i < str_length; ++i) { jstring str =static_cast(env->GetObjectArrayElement(a_, i));constchar* c_str = env->GetStringUTFChars(str,0); LOGE("字符串有:%s",c_str);//使用完释放env->ReleaseStringUTFChars(str,c_str); }//2、获得基本数据类型数组int32_tint_length = env->GetArrayLength(b_); LOGE("int 数组长度:%d",int_length);//对应的有 GetBoolean 、GetFloat等jint *b = env->GetIntArrayElements(b_,0);for(inti =0; i < int_length; i++) { LOGE("int 数据有:%d",b[i]); } env->ReleaseIntArrayElements(b_, b,0);returnenv->NewStringUTF("222");}
C/C++反射Java
反射方法
在C/C++中反射创建Java的对象,调用Java的方法
packagecom.dongnao.jnitest;importandroid.util.Log;publicclassHelper{privatestaticfinalString TAG ="Helper";//private和public 对jni开发来说没任何区别 都能反射调用publicvoidinstanceMethod(String a,intb,booleanc){ Log.e(TAG,"instanceMethod a="+a +" b="+b+" c="+c ); }publicstaticvoidstaticMethod(String a,intb,booleanc){ Log.e(TAG,"staticMethod a="+a +" b="+b+" c="+c); }}extern"C"JNIEXPORTvoidJNICALLJava_com_dongnao_jnitest_MainActivity_invokeHelper(JNIEnv *env, jobject instance){ jclass clazz = env->FindClass("com/dongnao/jnitest/Helper");//获得具体的静态方法 参数3:签名(下方说明)//如果不会填 可以使用javapjmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");//调用静态方法jstring staticStr= env->NewStringUTF("C++调用静态方法"); env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);//获得构造方法
基本数据类型的签名采用一系列大写字母来表示, 如下表所示:
Java类型签名
booleanZ
shortS
floatF
byteB
intI
doubleD
charC
longJ
voidV
引用类型L + 全限定名 + ;
数组[+类型签名
可以使用javap来获取反射方法时的签名
#cd 进入 class所在的目录 执行: javap -s 全限定名,查看输出的 descriptorD:\Lance\ndk\lsn7_jni\JniTest\app\build\intermediates\classes\debug>javap -s com.dongnao.jnitest.HelperCompiled from"Helper.java"publicclasscom.dongnao.jnitest.Helper{publiccom.dongnao.jnitest.Helper(); descriptor: ()VpublicvoidinstanceMethod(java.lang.String,int, boolean); descriptor: (Ljava/lang/String;IZ)VpublicstaticvoidstaticMethod(java.lang.String,int, boolean); descriptor: (Ljava/lang/String;IZ)V}
反射属性
inta =10;staticString b ="java字符串";privatestaticfinalString TAG ="Helper";publicvoidtestReflect(){ Log.e(TAG,"修改前 : a = "+a +" b="+b); reflectHelper(); Log.e(TAG,"修改后 : a = "+a +" b="+b);}publicnativevoidreflectHelper();extern"C"JNIEXPORTvoidJNICALLJava_com_dongnao_jnitest_Helper_reflectHelper(JNIEnv *env, jobject instance){//instance 就是 helperjclass clazz = env->GetObjectClass(instance);//获得int a的标示jfieldID a = env->GetFieldID(clazz,"a","I");intavalue = env->GetIntField(instance,a); LOGE("获得java属性a:%d",avalue);//修改属性值env->SetIntField(instance,a,100); jfieldID b = env->GetStaticFieldID(clazz,"b","Ljava.lang.String;");//获取值jstring bstr = static_cast(env->GetStaticObjectField(clazz, b));constchar* bc_str = env->GetStringUTFChars(bstr,0); LOGE("获得java属性b:%s",bc_str);//修改jstring new_str = env->NewStringUTF("C++字符串"); env->SetStaticObjectField(clazz,b,new_str); env->ReleaseStringUTFChars(bstr,bc_str); env->DeleteLocalRef(new_str); env->DeleteLocalRef(clazz);}
JNI引用
在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。
局部引用
大多数JNI函数会创建局部引用。NewObject/FindClass/NewStringUTF 等等都是局部引用。
局部引用只有在创建它的本地方法返回前有效,本地方法返回后,局部引用会被自动释放。
因此无法跨线程、跨方法使用。
extern"C"JNIEXPORT jstring JNICALLxxx(JNIEnv *env, jobject instance){//错误//不能在本地方法中把局部引用存储在静态变量中缓存起来供下一次调用时使用。// 第二次执行 str依然有值,但是其引用的 “C++字符串” 已经被释放staticjstring str;if(str ==NULL){ str = env->NewStringUTF("C++字符串"); }returnstr;}
释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放;
VM会自动释放局部引用,为什么还需要手动释放呢?
因为局部引用会阻止它所引用的对象被GC回收。
为什么需要手动释放局部引用.png
全局引用
全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效 。
由 NewGlobalRef 函数创建
extern"C"JNIEXPORT jstring JNICALLJava_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance){//正确staticjstring globalStr;if(globalStr ==NULL){ jstring str = env->NewStringUTF("C++字符串");//删除全局引用调用 DeleteGlobalRefglobalStr =static_cast(env->NewGlobalRef(str));//可以释放,因为有了一个全局引用使用str,局部str也不会使用了env->DeleteLocalRef(str); }returnglobalStr;}
弱引用
与全局引用类似,弱引用可以跨方法、线程使用。与全局引用不同的是,弱引用不会阻止GC回收它所指向的VM内部的对象 。
在对Class进行弱引用是非常合适(FindClass),因为Class一般直到程序进程结束才会卸载。
在使用弱引用时,必须先检查缓存过的弱引用是指向活动的对象,还是指向一个已经被GC的对象
extern"C"JNIEXPORT jclass JNICALLJava_com_dongnao_jnitest_MainActivity_test1(JNIEnv *env, jobject instance){staticjclass globalClazz =NULL;//对于弱引用 如果引用的对象被回收返回 true,否则为false//对于局部和全局引用则判断是否引用java的null对象jboolean isEqual = env->IsSameObject(globalClazz,NULL);if(globalClazz ==NULL|| isEqual) { jclass clazz = env->GetObjectClass(instance);//删除使用 DeleteWeakGlobalRefglobalClazz =static_cast(env->NewWeakGlobalRef(clazz)); env->DeleteLocalRef(clazz); }returnglobalClazz;}
JNI_OnLoad
调用System.loadLibrary()函数时, 内部就会去查找so中的 JNI_OnLoad 函数,如果存在此函数则调用。
JNI_OnLoad会:
告诉 VM 此 native 组件使用的 JNI 版本。
对应了Java版本,android中只支持JNI_VERSION_1_2 、JNI_VERSION_1_4、JNI_VERSION_1_6
在JDK1.8有 JNI_VERSION_1_8。
jintJNI_OnLoad(JavaVM* vm,void* reserved){// 2、4、6都可以returnJNI_VERSION_1_4;}
动态注册
在此之前我们一直在jni中使用的 Java_PACKAGENAME_CLASSNAME_METHODNAME 来进行与java方法的匹配,这种方式我们称之为静态注册。
而动态注册则意味着方法名可以不用这么长了,在android aosp源码中就大量的使用了动态注册的形式
//Java:nativevoiddynamicNative();nativeStringdynamicNative(inti);//C++:voiddynamicNative1(JNIEnv *env, jobject jobj){ LOGE("dynamicNative1 动态注册");}jstringdynamicNative2(JNIEnv *env, jobject jobj,jint i){returnenv->NewStringUTF("我是动态注册的dynamicNative2方法");}//需要动态注册的方法数组staticconstJNINativeMethod mMethods[] = { {"dynamicNative","()V", (void*)dynamicNative1}, {"dynamicNative","(I)Ljava/lang/String;", (jstring *)dynamicNative2}};//需要动态注册native方法的类名staticconstchar* mClassName ="com/dongnao/jnitest/MainActivity";jintJNI_OnLoad(JavaVM* vm,void* reserved){ JNIEnv* env = NULL;//获得 JniEnvintr = vm->GetEnv((void**) &env, JNI_VERSION_1_4);if( r != JNI_OK){return-1; } jclass mainActivityCls = env->FindClass( mClassName);// 注册 如果小于0则注册失败r = env->RegisterNatives(mainActivityCls,mMethods,2);if(r != JNI_OK ) {return-1; }returnJNI_VERSION_1_4;}
native线程调用Java
native调用java需要使用JNIEnv这个结构体,而JNIEnv是由Jvm传入与线程相关的变量。
但是可以通过JavaVM的AttachCurrentThread方法来获取到当前线程中的JNIEnv指针。
JavaVM* _vm =0;jobject _instance =0;jint JNI_OnLoad(JavaVM* vm, void* reserved){ _vm = vm;returnJNI_VERSION_1_4;}void *task(void *args){ JNIEnv *env;//将本地当前线程附加到jvm,并获得jnienv//成功则返回0_vm->AttachCurrentThread(&env,0); jclass clazz = env->GetObjectClass(_instance);//获得具体的静态方法 参数3:方法签名//如果不会填 可以使用javapjmethodID staticMethod = env->GetStaticMethodID(clazz,"staticMethod","(Ljava/lang/String;IZ)V");//调用静态方法jstring staticStr= env->NewStringUTF("C++调用静态方法"); env->CallStaticVoidMethod(clazz,staticMethod,staticStr,1,1);//获得构造方法jmethodID constructMethod = env->GetMethodID(clazz,"