JNI编程

来源:互联网 发布:轩辕剑昊天塔进阶数据 编辑:程序博客网 时间:2024/06/02 06:56

    • 一 综述
    • 二 重要数据结构
    • 三 简单例子
      • 1 Java 访问CC
      • 2 Java代码CC代码互相访问
        • 21 整体结构
        • 22 Java代码
        • 23 CC代码
    • 四 Android 上的JNI使用
    • 五 参考资料

一、 综述

JNIJava 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函数动态注册的时候会用到。
图2-1

  • *name 在java文件中用native关键字声明的方法名
  • *signature 方法的签名(稍后会介绍)
  • fnPtr 一个void类型指针,“指向”本地的方法实现。

图2-2 JVM相关数据结构这里涉及到两个数据结构,和创建JVM相关,没有做深入研究。
图2-2
在图中列出的还有一个重要的函数 JNI_OnLoad(JavaVM *vm, void *reserved);用于将在java中定义的native函数和在C/C++中实现的函数联系起来。这样,有了函数的声明、定义、实现,我们就可以使用了。
图2-3 JNI_OnLoad函数
2-3

三、 简单例子

这里会通过两个具体的实例来简单的说明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);其中JNIEXPORTJNICALL 都是Jni的关键字在jni_md.h中有定义。从java代码到C/C++代码的过程中,JVM扮演者重要的角色,如下图所示
图3-1
图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
图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代码结构
如上图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
图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
图3-5运行结果
那么我们可以从上面的代码中得出从JNI调用Java函数的基本步骤

  1. 获取java中某个变量的域(field):
    fid = env->GetStaticFieldID(clazz,"staticIntField","I");
    staticIntField = env->GetStaticIntField(clazz,fid);

  2. 获取java中某个变量的域(field):
    targetClass = env->FindClass("JniTest");
    mid = env->GetMethodID(targetClass,"<init>","(I)V");
    newObject = env->NewObject(targetClass,mid,100);

  3. 用生成的对象来调用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
图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-2 加载库
4-3
图4-3 MediaPlayer.java start()函数
首先需要使用System.loacLibrary(“library name”);来加载该java文件用到的库;然后使用native关键字定义本地方法(在C/C++中实现的方法);
4-4
图4-4 JNI 函数注册
在这里,读者需要注意的是,这里C/C++函数的函数名好像很简单,并不像上面介绍的那么复杂。这是因为,上面介绍的使用JNI的静态注册方法,来注册native函数的;而这里要是用显示的注册,很明显不方便。因此这里用到了动态注册的办法。稍微留意一下图2-1 JNINativeMethod数据结构。
PS:更加详细的资料请查阅
http://developer.android.com/training/articles/perf-jni.html

五 、 参考资料

《Android框架揭秘》 人民邮电出版社 ,棒子的那本书!

0 0
原创粉丝点击