一、 综述
JNI 是Java Native Interface 的缩写,是java平台的一部分,它允许Java代码和其他语言写的代码进行交互。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行交互操作。通俗的来说,就是用来在java代码中访问C/C++代码以及从C/C++访问Java代码的一种技术。这里会以Android上的JNI使用为蓝本通过两个实例,介绍JNI的使用。Java Native Interface (JNI)标准JNI 是本地编程接口。有了JNI我们就可以将将我们的一些核心代码、核心技术、不希望被别人反编译的代码使用C/C++实现;而且某些时候处于效率的考虑也会用到JNI技术。
二、 重要数据结构
在介绍相关知识之前,首先来看看和JNI相关的数据结构。源码通常位于:$JAVA_PAHT/include/jni.h
;这里我加入了一些注释,阅读起来更加方便。
图2-1 JNINativeMethod数据结构这个数据结构通常在JNI函数动态注册的时候会用到。
- *name 在java文件中用native关键字声明的方法名
- *signature 方法的签名(稍后会介绍)
- fnPtr 一个void类型指针,“指向”本地的方法实现。
图2-2 JVM相关数据结构这里涉及到两个数据结构,和创建JVM相关,没有做深入研究。
在图中列出的还有一个重要的函数 JNI_OnLoad(JavaVM *vm, void *reserved)
;用于将在java中定义的native函数和在C/C++中实现的函数联系起来。这样,有了函数的声明、定义、实现,我们就可以使用了。
图2-3 JNI_OnLoad函数
三、 简单例子
这里会通过两个具体的实例来简单的说明JNI的用法。第一个实例可以算作是一个热身,帮助我们了解编写JNI相关代码的一些步骤、方法。第二个实例则会介绍到从C/C++代码访问到Java代码,以及Java代码中内部类在C/C++中的处理方式。
3.1 Java 访问C/C++
HelloJni.java
public class HelloJni{
//加载生成的库文件
static {
System.loadLibrary("jni");
}
//声明java本地函数,native关键字
native void printStr(String str);
public void print(){
System.out.println("[Java] Hello java!");
}
public static void main(String [] args){
HelloJni my = new HelloJni();
my.print();
//调用jni方法
my.printStr("Hello java");
}
}
完成之后,执行 javac HelloJni.java
生成 HelloJni.class文件,然后使用javah HelloJni
生成HelloJni.h头文件;在这个.h文件中,仅仅是声明了jni的本地函数,具体实现在C或者CPP文件中。顺带说一下,在编写JNI函数的时候,比较麻烦的是从Java到C/C++的相关函数签名;这里可以使用$javap -s -p HelloJni来查看所有函数的签名。用javah生成.h文件中的函数:
JNIEXPORT void JNICALL Java_HelloJni_printStr (JNIEnv *, jobject, jstring);
其中JNIEXPORT 和JNICALL 都是Jni的关键字在jni_md.h
中有定义。从java代码到C/C++代码的过程中,JVM扮演者重要的角色,如下图所示
图3-1 JVM角色
当Java代码访问native函数的时候,会由JVM进行映射;依据其明明规则以及函数签名等信息将native void printstr(String)
;映射为Java_HelloJni_printStr(JNIEnv *, jobject , jstring )
;因此这才是Java代码可以访问到C/C++代码的真正原因。
testjni.c
include "HelloJni.h"
JNIEXPORT void JNICALL Java_HelloJni_printStr(JNIEnv *env, jobject obj, jstring str){
const char *mystr = (*env)->GetStringUTFChars(env,str,0);//important ....
printf("[CPP] %s\n",str );
printf("[CPP] %s\n",mystr );
return ;
}
这里需要注意的是const char *mystr = (*env)->GetStringUTFChars(env,str,0)
;将Java的String转成C下的char*;Java的String为16bit而C没有String的概念,并且C的char*为8bit。如果不进行转换,或导致乱码等其他未知问题。
生成动态库
gcc -shared -Wall -fPIC -o libjni.so testjni.c
注意
−shared
参数,表明生成动态库,如果不加该参数,回导致程序运行失败。
最后一部就是java HelloJni
运行程序了。这里还需要注意一个问题,java.library.path的值。如果直接运行很有可能出现如下异常,其原因就是在指定的java.library.path中找不到
libjni.so
Exception in thread “main” java.lang.UnsatisfiedLinkError: no jni in java.library.path
at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1856)
at java.lang.Runtime.loadLibrary0(Runtime.java:845)
at java.lang.System.loadLibrary(System.java:1084)
at HelloJni.(HelloJni.java:3)
通常情况下,我们只需要显式的指定path即可,java.library.path = /usr/java/packages/lib/i386:/usr/lib/jni:/lib:/usr/lib
针对这个异常有如下解决方案,推荐第二种。
1. cp libjni.so 到java.library.path
用-Djava.library.path=.
2. $java -Djava.library.path=. HelloJni
运行结果
图3-2运行结果
总结一下基本的步骤:
① 编写java代码
② 编译java代码
③ 生成C/C++头文件
④ 编写C/C++代码
⑤ 生成.so共享库
⑥ 运行java程序
3.2 Java代码C/C++代码互相访问
在上一节我们了解到Java代码要想访问当C/C++代码,就必须借助与JVM;JVM在这里扮演这桥梁的角色。那么同理,如果C/C++代码要访问当Java也得借助与JVM。本节将介绍如何在C/C++中借助JVM生成java对象。
3.2.1 整体结构
图3-3代码结构
如上图3-3所示,整个程序由JniFuncMain.java$createJniOjbect()开始。调用到Jni的方法来获取java类并创建java对象;完成java类和对象的创建之后,便调用相应的方法进行操作。
3.2.2 Java代码
Java部分代码分为两个模块,一个部分用于通C/C++进行交互,一部分则用于测试交互的java代码。
JniFuncMain.java
public class JniFuncMain{
private static int staticIntField = 300;
//Load library
static{
System.out.println(System.getProperty("java.library.path"));
System.loadLibrary("jnifunc");
}
public static native JniTest createJniObject();
public static void main(String [] args){
System.out.println("[Java] createJniObject(),Call Java's native method");
JniTest jniObj = createJniObject();//usaged the C/C++ Code to create the java Object.
jniObj.callTest();
System.out.println("[JAVA]to inner class...");
JniTest obj = new JniTest();
obj.callInnerClass();
}
public static class JniFuncMainInner{
public static native void InnerClassNative();
}
}
在这里需要注意一点:jniObj对象并不是在java代码中由new关键字创建,而是由对应Jni的函数创建。其实JNI提供了很多的方法来创建java对象,只不过去过程比较麻烦;可以这么来理解:在java中,java封装了对象创建的细节,但在JNI中其创建“细节”是需要我们去关注的。
3.2.3 C/C++代码
在介绍相关代码之前,还是来先看看对应的数据结构或者调用接口。如如下图3-4所示:这里仅仅列出了几个接下来代码会用到的接口。
AllocObject(jclass );
NewObject(jclass,jmethodId,...);
NewObjectV(jclass,jmethodID,va_list);
IsInstanceOf(jobject,jclass);
图3-4 JNI接口
其实通过上图以及针对jni.h源代码的阅读,我们便可以了解到其实JNI为我们提供了绝大部分的同java对应的函数。其不同点就是,java用起来比较舒服,直接调用函数就ok了;但在JNI中,你得按照相应的构造过程来实例;这个有点儿类似面向对象编程和面向过程编程。
C++源代码如下所示。
JniFuncMain.h
#include "JniFuncMain.h"
#include "JniFuncMain_JniFuncMainInner.h"
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz){
jclass targetClass;
jmethodID mid;
jobject newObject;
//jstring helloStr;
jfieldID fid;
jint staticIntField;
jint result;
//get JniFuncMain.java's staticIntField
fid = env->GetStaticFieldID(clazz,"staticIntField","I");
staticIntField = env->GetStaticIntField(clazz,fid);
printf("[CPP] get JniFuncMain.java's staticIntField = %d\n",staticIntField);
targetClass = env->FindClass("JniTest");
mid = env->GetMethodID(targetClass,"<init>","(I)V");
printf("[CPP] JniTest object be create..\n");
newObject = env->NewObject(targetClass,mid,100);
mid = env->GetMethodID(targetClass,"callByNative","(I)I");
result = env->CallIntMethod(newObject,mid,200);
fid = env->GetFieldID(targetClass,"intFields","I");
printf("[CPP] set JniTest object's intField 200 \n");
env->SetIntField(newObject,fid,result);
return newObject;
}
JNIEXPORT void JNICALL
Java_JniFuncMain_00024JniFuncMainInner_InnerClassNative(JNIEnv *env, jclass clazz){
printf("[CPP]This method is the Inner Class ...\n");
}
int main(){
return 0;
}
代码运行结果如下图3-5所示。
图3-5运行结果
那么我们可以从上面的代码中得出从JNI调用Java函数的基本步骤
获取java中某个变量的域(field):
fid = env->GetStaticFieldID(clazz,"staticIntField","I");
staticIntField = env->GetStaticIntField(clazz,fid);获取java中某个变量的域(field):
targetClass = env->FindClass("JniTest");
mid = env->GetMethodID(targetClass,"<init>","(I)V");
newObject = env->NewObject(targetClass,mid,100);- 用生成的对象来调用Java 函数
mid = env->GetMethodID(targetClass,"callByNative","(I)I");
result = env->CallIntMethod(newObject,mid,200);
基本上就是遵循上述步骤。详细的接口可以在jni.h中查找的到!
四、 Android 上的JNI使用
Android 系统的整个框架如下图 4-1 所示。Android 的应用程序全部是有java代码编写,这些Java代码编译之后会生成Dex类型的(Bytecode)字节码,并借助Dalvik(Android虚拟机)运行,注意是Dalvik不是JVM。
图4-1 Android 框架
由的APP、Frameworks、Librarys、Android Runtime以及Linux Kernel构成了Android的基本架构。APP用java写,Frameworks大部分用java,Librarys有C/C++,Linux Kernel毫无疑问用C。JNI其实质就是一种Java代码和C/C++代码的桥梁。
用到JNI的java代码的基本结构如下图4-2 和4-3所示(这里以MediaPlayer.java为例)
图4-2 加载库
图4-3 MediaPlayer.java start()函数
首先需要使用System.loacLibrary(“library name”);来加载该java文件用到的库;然后使用native关键字定义本地方法(在C/C++中实现的方法);
图4-4 JNI 函数注册
在这里,读者需要注意的是,这里C/C++函数的函数名好像很简单,并不像上面介绍的那么复杂。这是因为,上面介绍的使用JNI的静态注册方法,来注册native函数的;而这里要是用显示的注册,很明显不方便。因此这里用到了动态注册的办法。稍微留意一下图2-1 JNINativeMethod数据结构。
PS:更加详细的资料请查阅
http://developer.android.com/training/articles/perf-jni.html
五 、 参考资料
《Android框架揭秘》 人民邮电出版社 ,棒子的那本书!