什么是JNI,怎么使用
JNI——Java Native Interface,它是Java平台的一个特性(并不是android系统特有的)。其实主要是定义了一些JNI函数,让开发者可以通过调用这些函数实现Java代码调用C/C++的代码,C/C++的代码也可以调用Java的代码,这样就可以发挥各个语言的特点了。那么怎么使用JNI呢,一般情况下我们首先是将写好的C/C++代码编译成对应平台的动态库(windows一般是dll文件,linux一般是so文件等),这里我们是针对Android平台,所以只讨论so库。由于JNI编程支持C和C++编程,这里我们的栗子都是使用C++,对于C的版本可能会有些差异,但是主要的内容还是一致的,大家可以触类旁通。
举一个简单的例子
这里还是直接从代码说起,这样更加形象和直观,也便于理解。今天使用的Java代码如下:
public class AndroidJni { static{ System.loadLibrary("main"); } public native void dynamicLog(); public native void staticLog(); }
这里我们定义了两个声明为native的方法,并声明了一块静态区域,在该静态区域类加载名为libmain.so的库,这里我们说是libmain.so库,但是加载的时候却只写了“main”,其实大家只要知道这是约定的就可以了。
C++代码如下:
#include <jni.h> #define LOG_TAG "main.cpp" #include "mylog.h" static void nativeDynamicLog(JNIEnv *evn, jobject obj){ LOGE("hell main"); } JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj) { LOGE("static register log "); } JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog},}; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) { JNIEnv *env; if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { return -1; } LOGE("JNI_OnLoad comming"); jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni"); env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0])); return JNI_VERSION_1_4; }
这里引用了两个头文件,jni.h和mylog.h,其中jni.h是定义了很多我们使用的JNI函数和结构体,mylog.h是我自己定义的打印Android log的函数(功能和Java的Log类相同)。
这里暂时不讨论怎么编译成so库以及so库的一些规范。这里假定将上面的C++程序编译成了一个叫libmain.so的文件。在Java层使用System.loadLibarary("main")方法将该so库加载起来,使得dynamicLog()、staticLog()和对应的Java_com_github_songnick_jni_AndroidJni_staticLog()、nativeDynamicLog()两个native方法链接起来,当然这部分工作都是由Java虚拟机完成的,那么具体是怎么完成的,接下来将根据上面的代码进行分析。
静态注册native方法
在上面的代码中看到了JNIEXPORT和JNICALL关键字,这两个关键字是两个宏定义,他主要的作用就是说明该函数为JNI函数,在Java虚拟机加载的时候会链接对应的native方法,在AndroidJni.java的类中声明了staticLog()为native方法,他对应的JNI函数就是Java_com_github_songnick_jni_AndroidJni_staticLog(),那么是怎么链接的呢,在Java虚拟机加载so库时,如果发现含有上面两个宏定义的函数时就会链接到对应Java层的native方法,那么怎么知道对应Java中的哪个类的哪个native方法呢,我们仔细观察JNI函数名的构成其实是:Java_PkgName_ClassName_NativeMethodName,以Java为前缀,并且用“_”下划线将包名、类名以及native方法名连接起来就是对应的JNI函数了。一般情况下我们可以自己手动的去按照这个规则写,但是如果native方法特别多,那么还是有一定的工作量,并且在写的过程中不小心就有可能写错,其实Java给我们提供了javah的工具帮助生成相应的头文件。在生成的头文件中就是按照上面说的规则生成了对应的JNI函数,我们在开发的时候直接copy过去就可以了。这里上面的代码为例,在AndroidStudio中编译后,进入项目的目录app/build/intermediates/classes/debug下,运行如下命令:
javah -d jni com.github.songnick.jni.AndroidJni
这里-d指定生成.h文件存放的目录(如果没有就会自动创建),com.github.songnick.jni.AndroidJni表示指定目录下的class文件。这里简单介绍一下生成的JNI函数包含两个固定的参数变量,分别是JNIEnv和jobject,其中JNIEnv后面会介绍,jobject就是当前与之链接的native方法隶属的类对象(类似于Java中的this)。这两个变量都是Java虚拟机生成并在调用时传递进来的。
动态注册
上面我们介绍了静态注册native方法的过程,就是Java层声明的native方法和JNI函数是一一对应的,那么有没有方法让Java层的native方法和任意的JNI函数链接起来,当然是可以的,这就得使用动态注册的方法。接下来就看看如何实现动态注册的。
JNI_OnLoad函数
当我们使用System.loadLibarary()方法加载so库的时候,Java虚拟机就会找到这个函数并调用该函数,因此可以在该函数中做一些初始化的动作,其实这个函数就是相当于Activity中的onCreate()方法。该函数前面有三个关键字,分别是JNIEXPORT、JNICALL和jint,其中JNIEXPORT和JNICALL是两个宏定义,用于指定该函数是JNI函数。jint是JNI定义的数据类型,因为Java层和C/C++的数据类型或者对象不能直接相互的引用或者使用,JNI层定义了自己的数据类型,用于衔接Java层和JNI层,至于这些数据类型我们在后面介绍。这里的jint对应Java的int数据类型,该函数返回的int表示当前使用的JNI的版本,其实类似于Android系统的API版本一样,不同的JNI版本中定义的一些不同的JNI函数。该函数会有两个参数,其中*jvm为Java虚拟机实例,JavaVM结构体定义了以下函数:
DestroyJavaVM AttachCurrentThread DetachCurrentThread GetEnv
这里我们使用了GetEnv函数获取JNIEnv变量,上面的JNI_OnLoad函数中有如下代码:
JNIEnv *env; if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) { return -1; }
这里调用了GetEnv函数获取JNIEnv结构体指针,其实JNIEnv结构体是指向一个函数表的,该函数表指向了对应的JNI函数,我们通过调用这些JNI函数实现JNI编程,在后面我们还会对其进行介绍。
获取Java对象,完成动态注册
上面介绍了如何获取JNIEnv结构体指针,得到这个结构体指针后我们就可以调用JNIEnv中的RegisterNatives函数完成动态注册native方法了。该方法如下:
jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)
第一个参数是Java层对应包含native方法的对象(这里就是AndroidJni对象),通过调用JNIEnv对应的函数获取class对象(FindClass函数的参数为需要获取class对象的类描述符):
jclass clz = env->FindClass("com/test/jni/AndroidJni");
第二个参数是JNINativeMethod结构体指针,这里的JNINativeMethod结构体是描述Java层native方法的,它的定义如下:
typedef struct { const char* name;//Java层native方法的名字 const char* signature;//Java层native方法的描述符 void* fnPtr;//对应JNI函数的指针 } JNINativeMethod;
第三个参数为注册native方法的数量。一般会动态注册多个native方法,首先会定义一个JNINativeMethod数组,然后将该数组指针作为RegisterNative函数的参数传入,所以这里定义了如下的JNINativeMethod数组:
JNINativeMethod nativeMethod[] = {{"dynamicLog", "()V", (void*)nativeDynamicLog}};
最后调用RegisterNative函数完成动态注册:
env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));
JNIEnv结构体
上面提到JNIEnv这个结构体,它就老厉害了,指向一个函数表,该函数表指向一系列的JNI函数,我们通过调用这些JNI函数可以实现与Java层的交互,这里简单的看看几个定义的函数:
.......... jfieldID GetFieldID(jclass clazz, const char* name, const char* sig) jboolean GetBooleanField(jobject obj, jfieldID fieldID) jmethodID GetMethodID(jclass clazz, const char* name, const char* sig) CallVoidMethod(jobject obj, jmethodID methodID, ...) CallBooleanMethod(jobject obj, jmethodID methodID, ...) ..........