JNI编程

3 篇文章 0 订阅

一、 综述

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框架揭秘》 人民邮电出版社 ,棒子的那本书!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值