由于公司最近在培训JNI,于是结合公司的培训和自己在网上自学的一些东西做一下这方
面的整理,方便为其他初学者提供一个能快速入门的途径。好了,废话不多说,本文主要介绍
一下JNI以及在实际使用过程中用到的一些东西,包括在如何调用C++的代码(因为公司是用
C++的),以及怎么在C/C++中回调java方法,还有向库中动态注册函数。那些想看NDK环境搭
建的随便百度一下一堆blog,这里就不说了哈。
一.Jni理解
Java Native Interface (JNI)标准是java平台的一部分,它允许Java代码和其他语言写
的代码进行交互。JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码
能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行交互操作。实际上,
要实现这一过程比较复杂,我们需要编写C/C++的代码,实现底层函数的同时还要完成从java
方法到该底层函数的映射(即下文要说的向库中注册函数),交叉编译出能在ARM机上执行的
动态库,并把这些动态库和java应用一起打包成apk。可以看出这一过程相当复杂,因此google
为我们提供了一个成为NDK的开发工具,这一工具有效简化了C++部分交叉编译和打包的开发工
作。
二.C/C++代码的调用
有关C和C++代码的调用需要用到下面要讲的向库中注册函数的知识,只有注册之后,完成
从java方法到本地函数的映射,java虚拟机在加载库之后才能在执行的时候找到这些本地函数,
从而执行调用。具体做法见第三部分。
其实这里想说的是C和C++函数调用的差异。首先,想要调用C++需要在Android.mk文件中
增加一句配置:LOCAL_CPP_EXTENSION := .CPP 当然,下面的LOCAL_SRC_FILES后面也应该是
xxx.cpp的文件。其次,如果在C中调用FindClass函数的话需要传一个JNIEnv*类型的参数:
jclass clazz = (*env)->FindClass(env,"classpath");
而如果使用C++调用的话则不需要传递这个参数,只需要:
jclass clazz = env->FindClass("classpath");
有过C/C++开发经验的人应该知道,要在C中实现面向对象编程,需要向函数中传递一个对象的
指针,这个指针在C++中则缺省为this指针,而上文中的env其实就是一个对象指针。我们在jdk
下的include目录中会找到一个叫做jni.h的头文件,这个头文件是编写native函数需要包含的。
在这个头文件中定义了所有java类型在C/C++中的定义,一些数据结构,转换函数和其他一些二
者交互需要用到的函数。在这个头文件中定义了两部分实现,分别是C和C++的部分,C++部分的
函数都是通过包装C中的同名函数实现的,感兴趣的同学可以自己查看一下jni.h的代码。
三.注册函数
向库中注册函数有两种方法:分为静态注册和动态注册。
静态注册的方法是:
1.编写带有native方法的类,生成.class文件;
2.使用javah生成.h文件;
3.编写代码实现头文件中的方法。
但是静态注册有两个弊端:
1.那长到恶心死人的方法名没有人能忍受的了;
2.native方法每次被调用都需要由虚拟机在库中根据包名+类名+方法名进行寻找严重影响
执行效率。
综上所述,在实际的应用性开发中会采用动态方法进行函数注册,优点有二:
1.更有效率的去找到函数;
2.可在执行期间进行替换。
下面我们详细介绍如何动态注册,大概步骤如下:
1.编写native函数;
2.填充映射表,就是一个JNINativeMethod类型的数组;
3.编写JNI_OnLoad函数,如果注册函数比较多的话可以分模块编写。
在jni.h中JNINativeMethod结构的定义如下:
typedef struct {
char* name; //java中native函数名
char* signature; //签名信息,用字符串表示,参数类型及返回值的组合,在
//javah生成的.h文件中通过注释标识出来
void* fnPtr; //Jni层函数函数指针,转换为void*类型
} JNINativeMethod;
举个例子,一个java中声明的native函数如下:public native int sum(int a,int b);
则在javah生成的头文件中生成的函数签名就是"(II)I",具体的映射关系参照网上其他教程。
这个时候如果我们在cpp中编写的对应函数名为native_sum,那么映射表应该按如下填写:
static JNINativeMethod gNativeMethods[] = {
{"sum","(II)I",(void*)native_sum},
};
这里说明一下签名信息,因为JAVA支持函数重载,可以定义同名但不同参数的函数,但直接根
据函数名是没法找到具体函数的,因此利用参数类型及返回类型组成签名信息。
下面讲一下JNI_OnLoad函数:
JNI组件被成功加载和卸载时,会进行函数回调,当VM执行到System.loadLibrary(xxx)
函数时,首先会去执行JNI组件中的JNI_OnLoad()函数,而当VM释放该组件时会呼叫
JNI_OnUnload()函数。
JNI_OnLoad()有两个重要的作用:
1.指定JNI版本:告诉VM该组件使用那一个JNI版本(若未提供JNI_OnLoad()函数,VM会默认
该使用最老的JNI 1.1版),如果要使用新版本的JNI,例如JNI 1.4版,则必须由JNI_OnLoad()
函数返回常量JNI_VERSION_1_4(该常量定义在jni.h中) 来告知VM。
2.初始化设定,当VM执行到System.loadLibrary()函数时,会立即先呼叫JNI_OnLoad()方
法,因此在该方法中进行各种资源的初始化操作最为恰当。
JNI_OnUnload()的作用与JNI_OnLoad()对应,当VM释放JNI组件时会呼叫它,因此在该方法中进
行善后清理,资源释放的动作最为合适。
好了下面可以在JNI_OnLoad函数中实现对本地函数的注册了,网上有很多这方面的文章,他们
写的逻辑也比较复杂,大概看了下,大部分的调用最后都要归于对RegisterNatives函数的调用,
该函数定义在jni.h中,函数声明如下:
jint RegisterNatives(JNIEnv* env,jclass clazz,JNINativeMethod* methods,jint nMethods);
第一个参数是native函数所属的类,第二个参数是映射表,第三个参数是映射表长度。
C++中可直接按如下调用:
env->RegisterNatives(clazz,gNativeMethods,sizeof(gNativeMethods)/sizeof(gNativesMethods[0]));
a sample JNI_OnLoad function like this:
static const char* classname = "com/hy/ndktest/MainActivity";
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if(vm->GetEnv( (void**)&env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
jclass jniAct = env->FindClass(classname);
if(jniAct == NULL) {
return -1;
}
int nelem = int( sizeof(gNativeMethods)/sizeof(gNativeMethods[0]) );
if(env->RegisterNatives(jniAct, gNativeMethods, nelem) < 0) {
return -1;
}
return JNI_VERSION_1_4;
}
四.在C++中回调java方法
主要流程:
1.编写java类;
2.将需要用到的类映射到CPP中;
3.将需要用到的方法映射到cpp中;
4.新建类对象,或者使用已有对象;
5.调用方法。
设计实现:
1.映射类:
需要用到FindClass函数,声明如下:
jclass FindClass(JNIEnv* env,const char* classname);
2.映射方法:
需要用到GetMethodID函数,声明如下:
jmethodID GetMethodID(JNIEnv* env,jclass clazz,const char* methodname,const char* signature);
以及GetStaticMethodID函数,声明如下:
jmethod GetStaticMethodID(JNIEnv* env,jclass,clazz,const char* methodname,const char* signature);
3.新建类对象:
jmethodID construction_id = env->GetMethodID(env,clazz,"<init>",()V);
jobject obj = env->NewObject(env,clazz,construction_id);
4.方法调用:
jobject CallxxxMethod(JNIEnv* env,jobject obj,jmethodID methodID,...);
其中xxx表示本地方法的返回类型,掉用静态方法Call后要加Static 。
例如有个java类:
class Test {
private String mName;
public String getName() {
return mName;
}
public void setName(String name) {
mName = name
}
public native void changeName();
}
则实现changeName方法的代码如下:
jstring native_ChangeName(JNIEnv* env,jobject thiz) {
jclass test = env->FindClass("com/example/Test");
jmethodID setName = env->GetMethodID(test,"setName","(Ljava/lang/String;)V");
jstring str = env->NewStringUTF("Tom");
env->CallVoidMethod(thiz,setName,str);
}
好了,所有内容介绍完了,本文主要为了了结和理清大体的逻辑思路,具体实现的细节还需
要查看jni文档。