如下举例代码均来自:NDK示例代码java
安卓开发中不少场景须要用到NDK来开发,好比,音视频的渲染,图像的底层绘制,秘籍计算应用,复用C/C++库等等,安卓绝大部分核心代码都是在Native层来完成,也就是用C/C++来完成,有的时候咱们看系统源码的时候追着追着就发现最终调用一个native声明的方法,接下来就须要深刻native层来查看具体逻辑了,那java代码是怎么调用native层代码的呢?或者说java是怎么调用C/C++代码的呢?这里就用到JNI/NDK方面技术了,本系列不会细讲C/C++语言知识,语言方面须要你本身私下学习,若是你想深刻NDK层学习,那么请务必先学习一下C/C++语言知识,起码能看得懂啊,学习的时候能够尝试用C/C++来刷LeetCode,防止不用慢慢就忘记了,好了,接下来咱们进入本篇正题。linux
JNI是java的特性,与安卓无关,用来加强java与本地代码交互的能力,JNI是Java的一个框架,定义了一系列方法能够用于Java与C/C++互相调用。android
NDK是安卓平台的开发工具包,是安卓的特性,与java无关,用来快速开发生成C、 C++的动态库,经过 NDK咱们能够在 Android中将C/C++代码编译到原生库中,而后使用 IDE 集成构建系统 Gradle 将您的库封装入 APK。c++
JNI是Java特性,在window平台能够用java的JNI特性来完成java与C/C++互相调用,linux平台也能够,NDK是安卓平台的开发工具包,在安卓开发的时候咱们能够经过Java的JNI特性来完成java与C/C++互相调用,可是C/C++代码怎么编译到原生库中呢?这时就用安卓平台提供的NDK开发工具了。git
接下来咱们就来看一下具体实现Java与C/C++互调。github
AS配置NDK环境在3.0以上已经十分简单了,环境的配置请自行查阅搭建,这里咱们直接讲解Java与C/C++互调知识。数组
JNI数据类型与Java数据类型对应以下:app
Java类型 | 本地类型 |
---|---|
boolean | jboolean |
byte | jbyte |
char | jchar |
short | jshort |
int | jint |
long | jlong |
float | jfloat |
double | jdouble |
Object | jobject |
Class | jclass |
String | jstring |
Object[] | jobjectArray |
boolean[] | jbooleanArray |
byte[] | jbyteArray |
char[] | jcharArray |
short[] | jshortArray |
int[] | jintArray |
long[] | jlongArray |
float[] | jfloatArray |
double[] | jdoubleArray |
这些对应关系什么意思呢?接下来经过具体实例了解一下:框架
建立新项目,咱们在MainActivity中声明以下native方法:ide
1 native int arrayTest(int i,int[] a1, String[] a2);
意思是这个方法须要native层来实现,java调用的时候会传递三个参数,分别是:int ,int[] , String[] 类型的,接下来咱们须要在native层来实现这个方法,AS中经过快捷键"alt+/"会自动帮助咱们在native层来实现方法的声明:
1 JNIEXPORT jint JNICALL Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env,
2 jobject instance,jint i,jintArray a1_,jobjectArray a2)
方法声明生成规则为:Java_包名_类名_方法名
java中声明的arrayTest方法参数类型分别为int,int[],String[]类型,在JNI中生成的方法声明分别对应jint ,jintArray ,jobjectArray ,这里就用到了上面的数据类型对应表,至于其他参数类型依照上表对应便可。
咱们观察JNI中方法声明还发现生成的方法对了一些额外信息:JNIEXPORT ,JNICALL,参数中多了JNIEnv *env, jobject instance这些又都是什么鬼?咱们一一解释
在 Windows 中,定义为__declspec(dllexport)
。由于Windows编译 dll 动态库规定,若是动态库中的函数要被外部调用,须要在函数声明中添加此标识,表示将该函数导出在外部能够调用。
在 Linux/Unix/Mac os/Android 这种类Unix系统中,定义为__attribute__ ((visibility ("default")))
GCC 有个visibility属性, 该属性是说, 启用这个属性:
当visibility=hidden时
动态库中的函数默认是被隐藏的即 hidden. 除非显示声明为__attribute__((visibility("default")))
.
当visibility=default时
动态库中的函数默认是可见的.除非显示声明为__attribute__((visibility("hidden")))
.
JNIEXPORT 主要用于window平台,在安卓平台可不加,去掉便可。
在类Unix中无定义,在Windows中定义为:_stdcall
,一种函数调用约定
在安卓平台 定义以下:
#define JNICALL 什么也没定义
因此,同JNIEXPORT 同样在安卓平台JNICALL可不加,去掉便可。
在AS中自动为咱们生成的JNI方法声明都会带一个这样的参数,这个instance就表明Java中native方法声明所在的类,好比上面arrayTest方法声明在MainActivity中,这里的instance就表示MainActivity实例。
JNIEnv 指针但是JNI中很是很是重要的一个概念,表明了JNI的环境,JNI层实现的方法都是经过这个指针来调用,经过JNIEnv 指针咱们能够调用JNI层的方法访问Java虚拟机,进而操做Java对象。
JNIEnv 指针只在建立它的线程有效,不能跨线程传递,对于这句话的理解咱们会在后面涉及线程的时候会再次提到,这里不懂能够看彻底文回来再看一下。
咱们看下JNIEnv 是怎么定义的:
jni.h中对JNIEnv定义以下:
1 #if defined(__cplusplus) //c++环境 2 typedef _JNIEnv JNIEnv;//c++环境中JNIEnv为_JNIEnv 3 typedef _JavaVM JavaVM; 4 #else 5 typedef const struct JNINativeInterface* JNIEnv;//c环境JNIEnv为const struct JNINativeInterface* 6 typedef const struct JNIInvokeInterface* JavaVM; 7 #endif
C++中JNIEnv为_JNIEnv 而 C环境JNIEnv为const struct JNINativeInterface*
咱们先看_JNIEnv,定义以下:
1 struct _JNIEnv { 2 3 const struct JNINativeInterface* functions; 4 5 #if defined(__cplusplus) 6 7 jint GetVersion() 8 { return functions->GetVersion(this); } 9 10 jclass DefineClass(const char *name, jobject loader, const jbyte* buf, 11 jsize bufLen) 12 { return functions->DefineClass(this, name, loader, buf, bufLen); } 13 14 jclass FindClass(const char* name) 15 { return functions->FindClass(this, name); } 16 。。。。。 17 }
_JNIEnv 是对 const struct JNINativeInterface类型的包装,间接调用了const struct JNINativeInterface 上定义的方法。
咱们继续看JNINativeInterface定义:
1 struct JNINativeInterface { 2 。。。 3 jint (*GetVersion)(JNIEnv *); 4 jclass (*DefineClass)(JNIEnv*, const char*, jobject, const jbyte*, 5 jsize); 6 jclass (*FindClass)(JNIEnv*, const char*); 7 jmethodID (*FromReflectedMethod)(JNIEnv*, jobject); 8 jfieldID (*FromReflectedField)(JNIEnv*, jobject); 9 /* spec doesn't show jboolean parameter */ 10 jobject (*ToReflectedMethod)(JNIEnv*, jclass, jmethodID, jboolean); 11 。。。 12 }
这里才是接口真正定义的地方,具体的实如今Java虚拟机中。
经过以上分析,咱们得出如下结论:
C++中JNIEnv *env至关于 struct _JNIEnv *env 调用方法只需以下方式便可间接调用JNINativeInterface 中方法:
1 env-> FindClass(JNIEnv*, const char*)
C中JNIEnv *env至关于 JNINativeInterface **env,二级指针,调用方法须要先解引用在调用以下:
1 (*env)-> FindClass(JNIEnv*, const char*)
明白了以上概念后咱们能够继续在native层来实现
1 JNIEXPORT jint JNICALL 2 Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env,jobject instance,jint i,jintArray a1_,jobjectArray a2)
方法了。
Java层传递过来的数据可能为基本数据类型,数组,对象等,不一样数据类型咱们要想使用须要不一样的处理方式,具体以下。
Java层传递过来的基本数据类型无需其他操做,直接使用便可。
数组分为基本数据类型的数组与对象数据类型的数组,好比,int[]与String[],在Native咱们怎么获取数组中的数据呢?以下:
1 JNIEXPORT jint JNICALL 2 Java_com_wanglei55_ndk_MainActivity_arrayTest(JNIEnv *env, 3 jobject instance,jint i,jintArray a1_,jobjectArray a2) { 4 LOGE("i的值为:%d", i); 5 // 第二个参数: 6 // true:拷贝一个新数组 7 // false: 就是使用的java的数组 (地址) 8 jint *a1 = env->GetIntArrayElements(a1_, 0);//返回指针,指向数组地址 9 jsize len = env ->GetArrayLength(a1_);//获取数组长度 10 for (int i = 0; i < len; ++i) { 11 LOGE("int数组的值为:%d", *(a1+i)); 12 //改变java中数组的值,若是下面参数3 mode设置为2则改变不了 13 *(a1+i) = 666; 14 } 15 // 参数3:mode 16 // 0: 刷新java数组 并 释放c/c++数组 17 // 1 = JNI_COMMIT:只刷新java数组 18 // 2 = JNI_ABORT:只释放 19 env->ReleaseIntArrayElements(a1_, a1, 0); 20 // 21 jsize slen = env->GetArrayLength(a2);//获取数组长度 22 for (int i = 0; i < slen; ++i) { 23 jstring str = static_cast<jstring>(env->GetObjectArrayElement(a2, i));//获取数组中的数据 24 const char* s = env->GetStringUTFChars(str,0); 25 LOGE("jni获取java字符串数组:%s", s); 26 env->ReleaseStringUTFChars(str, s); 27 } 28 return 3; 29 }
上面展现了native层获取java传递过来的数组数据,这里只是遍历了一下,能够看到核心方法都是经过JNIEnv 指针来调用方法操做的,因此JNIEnv 是十分重要的。
Java传递过来的对象怎么处理呢?这里须要用到反射了,一样也是经过JNIEnv 指针来调用相应方法的,咱们在MainActivity添加以下方法:
1 native void objectTest(Student s, String str);
Student 类以下:
1 public class Student { 2 3 private int num = 100; 4 5 public int getNum() { 6 return num; 7 } 8 9 public void setNum(int num) { 10 this.num = num; 11 } 12 13 public static void printMsg(Card card){//调用方法须要传递Card类 14 Log.e("JNI","printMsg Card: "+card.id); 15 } 16 17 public static void printMsg(String str){ 18 Log.e("JNI","printMsg: "+str); 19 } 20 }
Card类:
1 public class Card { 2 int id; 3 4 public Card(int id) { 5 this.id = id; 6 } 7 }
都很简单,这里就是演示一下。
接下来咱们看下native层怎么获取传递过来的对象数据以及调用其方法,这里咱们直接看代码,注释给了详细的说明:
1 extern "C" 2 JNIEXPORT void JNICALL 3 Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean, 4 jstring str_) { 5 // 6 const char *str = env->GetStringUTFChars(str_, 0); 7 LOGE("objectTest: %s",str); 8 env->ReleaseStringUTFChars(str_, str); 9 //bean就是java层传递过来的Student对象 10 //反射方式调用bean中的set/get方法 11 jclass beanClass = env->GetObjectClass(bean);//获取class 12 //修改属性值 13 //jfieldID fieldID = env->GetFieldID(beanClass,"num","I"); 14 //env->SetIntField(bean,fieldID,444); 15 16 //调用set方法设置 17 jmethodID setMethodID = env->GetMethodID(beanClass,"setNum","(I)V");//获取方法信息 18 env->CallVoidMethod(bean,setMethodID,999);//调用bean中的setMethodID对应的方法 19 //调用get方法获取 20 jmethodID getMethodID = env->GetMethodID(beanClass,"getNum","()I");//获取方法信息 21 jint result = env->CallIntMethod(bean,getMethodID); 22 LOGE("调用Student中getNum返回值: %d",result); 23 24 //调用静态方法:public static void printMsg(String str) 25 jmethodID staticMID = env->GetStaticMethodID(beanClass,"printMsg","(Ljava/lang/String;)V"); 26 jstring jstring1 = env->NewStringUTF("JNI中的String"); 27 env->CallStaticVoidMethod(beanClass,staticMID,jstring1); 28 env->DeleteLocalRef(jstring1);//释放 29 30 //调用静态方法:public static void printMsg(Card card) 31 jmethodID staticMID2 = env->GetStaticMethodID(beanClass,"printMsg","(Lcom/wanglei55/ndk/Card;)V"); 32 //建立参数Card 33 jclass cardclz = env->FindClass("com/wanglei55/ndk/Card");//经过完整类名获取class 34 jmethodID constructorID = env->GetMethodID(cardclz,"<init>","(I)V");//<init>表示获取构造方法 35 jobject cardObj = env->NewObject(cardclz,constructorID,333);//反射建立Card对象 36 env->CallStaticVoidMethod(beanClass,staticMID2,cardObj); 37 env->DeleteLocalRef(cardObj); 38 }
上面已经给了详细注释,再也不说明,这里须要额外说一下方法的签名。
调用GetMethodID与GetStaticMethodID的时候咱们须要传递方法的签名信息,怎么配置呢?以下有个对应表:
Java类型 | 签名 |
---|---|
boolean | Z |
short | S |
float | F |
byte | B |
int | I |
double | D |
char | C |
long | J |
void | V |
引用类型 | L + 全限定名 + ; |
数组 | [+类型签名 |
若是有内部类 则用$来分隔 如:Landroid/os/FileUtils$FileStatus;
什么意思呢?
好比以Student类中getNum()方法为例,其定义以下:
1 public int getNum()
方法调用不用传递参数,返回值为int类型,int对应签名为I,大写的啊,因此方法签名为"()I",()里面填写参数对应的签名,()右面紧跟方法返回值签名。
再来个复杂的,好比以下方法:
1 String getInfo(long[], List list);
签名是什么呢?其签名为:
1 "([JLjava/util/List)Ljava/lang/String;"
其中 "[J" 表明long[]的签名,"Ljava/util/List" 表明List list的签名,"Ljava/lang/String;" 表明返回值String的签名。
像上面咱们在java层定义native方法:
1 native void objectTest(Student s, String str);
而后在JNI层定义对应方法:
1 JNIEXPORT void JNICALL 2 Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean,jstring str_) { 3 。。。 4 }
当咱们在Java中调用objectTest(Student s, String str)方法时,就会从JNI层寻找Java_com_wanglei55_ndk_MainActivity_objectTest(JNIEnv *env, jobject instance, jobject bean,jstring str_) 方法,并为两者创建联系。
静态注册就是根据方法名,将Java层native方法和JNI层对应方法创建关联,这种方式就是静态注册,静态注册有以下缺点:
JNI层方法名很长
第一次调用native方法会比较耗时,须要查找对应方法创建联系(经过指针记录方法)
有没有一种方式在加载的时候就创建起两者的联系呢?这样第一次调用native方法的时候就不须要查找了,这种方式就是动态注册。
动态注册能够在加载的时候就创建起java层native方法与JNI层方法的联系,那具体怎么创建联系呢?加载的时候是指何时?
咱们在调用动态库so中方法的时候都会先加载对应so库,好比:
1 static { 2 System.loadLibrary("native-lib"); 3 }
在加载native-lib动态库的时候JVM会检查对应C/C++文件中是否有int JNI_OnLoad(JavaVM *vm, void *reserved)方法,有的话则会调用这个方法,在这个方法里面咱们能够作一些初始化的操做,进而能够动态注册一些方法。
接下来咱们具体操做一下看看怎么动态注册:
首先java层一样定义native方法,以下:
1 native void dynamicJavaTest(); 2 native int dynamicJavaTest2(int i);
接下来在JNI层定义对应方法:
1 void dynamicTest(){ 2 LOGE("JNI dynamicTest"); 3 } 4 5 jint dynamicTest2(JNIEnv *env, jobject instance,jint i){ 6 LOGE("JNI dynamicTest2:%d",i); 7 return 9999; 8 }
这里我并无把方法名设置为同样,方法名你能够随便起,若是想接收JNIEnv *env, jobject instance参数能够在方法上加上,Jvm调用的时候会传递这两个参数给JNI层方法,不想接收也能够去掉。
java层方法与JNI层怎么创建起关联呢?接下来咱们还须要定义JNINativeMethod类型的数组,将二者对应起来,JNINativeMethod定义在jni.h中定义以下:
1 typedef struct { 2 const char* name;//java层的方法名 3 const char* signature;//java层方法的签名 4 void* fnPtr;//JNI层对应方法的指针 5 } JNINativeMethod;
这里咱们将java层dynamicJavaTest方法与JNI层dynamicTest对应
java层dynamicJavaTest2方法与JNI层dynamicTest2对应
因此数组定义以下:
1 static const JNINativeMethod methods[] = { 2 {"dynamicJavaTest","()V",(void*)dynamicTest}, 3 {"dynamicJavaTest2","(I)I",(int*)dynamicTest2}, 4 };
接下来就能够在JNI_OnLoad方法中动态注册了:
1 static const char *mClassName = "com/wanglei55/ndk/MainActivity"; 2 3 JavaVM *_vm;//记录JavaVM 4 5 int JNI_OnLoad(JavaVM *vm, void *reserved){ 6 // 7 LOGE("JNI_Onload"); 8 // 9 _vm = vm; 10 // 得到JNIEnv 11 JNIEnv *env = 0; 12 // 小于0 失败 ,等于0 成功 13 int r = vm->GetEnv((void**)&env,JNI_VERSION_1_4); 14 if (r != JNI_OK){ 15 return -1; 16 } 17 //得到 class对象 18 jclass jcls = env->FindClass(mClassName); 19 //动态注册方法 20 env->RegisterNatives(jcls,methods, sizeof(methods)/ sizeof(JNINativeMethod)); 21 return JNI_VERSION_1_4;// 返回native 组件使用的 JNI 版本 22 }
核心就是调用RegisterNatives方法来完成动态注册的逻辑,到此动态注册就完成了,此外动态注册不用定义那么长的方法。
在安卓系统源码中JNI层大量使用了动态注册方法而不是静态注册,静态注册多用于日常NDK的开发。
native调用java须要用到JNIEnv指针,而JNIEnv是由Jvm传入与线程相关的变量,若是咱们在native中开启一个线程完成工做后回调java层方法怎么办呢?能够经过JavaVM的AttachCurrentThread方法来获取到当前线程中JNIEnv指针。
接下来咱们看一下怎么操做。
java层定义native方法与回调的方法:
1 public void callBack(){ 2 if (Looper.myLooper() == Looper.getMainLooper()){ 3 Toast.makeText(this,"MainLooper",Toast.LENGTH_SHORT).show(); 4 }else{ 5 runOnUiThread(new Runnable() { 6 @Override 7 public void run() { 8 Toast.makeText(MainActivity.this,"runOnUiThread",Toast.LENGTH_SHORT).show(); 9 } 10 }); 11 } 12 } 13 14 native void testThread();
JNI层采用静态注册的方式注册对应方法:
1 jobject _instance; 2 3 void* threadTask(void* args){ 4 // native线程 附加 到 Java 虚拟机 5 JNIEnv *env;//JNIEnv *是与线程有关的 6 //调用JavaVM 的AttachCurrentThread方法来获取与线程有关的JNIEnv 7 jint i = _vm->AttachCurrentThread(&env,0);//JNI_OnLoad会传递过来JavaVM *vm参数 8 if (i != JNI_OK){ 9 return nullptr; 10 } 11 //回调 12 //得到MainActivity的class对象 13 jclass cls = env->GetObjectClass(_instance); 14 jmethodID updateUI = env->GetMethodID(cls,"callBack","()V"); 15 env->CallVoidMethod(_instance,updateUI); 16 //释放内存 17 env->DeleteGlobalRef(_instance); 18 //退出线程,释放线程资源 19 _vm->DetachCurrentThread(); 20 return 0; 21} 22 23 extern "C" 24 JNIEXPORT void JNICALL 25 Java_com_wanglei55_ndk_MainActivity_testThread(JNIEnv *env, jobject instance) { 26 27 pthread_t pid; 28 //启动线程 29 _instance = env->NewGlobalRef(instance); 30 pthread_create(&pid,0,threadTask,0);//记得引入头文件 #include <pthread.h> 31}
native线程中使用JNIEnv必定要记得获取当前线程的JNIEnv,由于不一样线程的JNIEnv是不一样的,同时使用完记得调用DetachCurrentThread()方法释放线程资源。
本篇算是NDK开发的入门篇,介绍了一些基础的操做,必定要记住,若是想深刻NDK层先把C/C++语言基础打好,不然上面代码看起来很蒙圈,后续文章读起来也很难受。