最近关注于android的framework层,都是一堆堆的native,jni搞不定就没法看下去,就小研究了一下.

为什么要使用jni?有如下理由:

  • java标准库不支持与特定平台相关的一些功能,需要c/c++层给予支持
  • 可能已存在一个功能良好的库,并不想再用java重写,可以用jni调用它
  • 在某个功能点效率至关重要,用底层的语言实现更好(如c内嵌汇编)

废话不多说,直接上个能跑的例子胜过千言万语:

在一个目录下新建一个java文件Hello.java:

package com.mypack;
public class Hello{
	static{ 
		try{ 
			System.loadLibrary("hello"); 
		}catch(UnsatisfiedLinkError e){ 
			System.err.println( "Cannot load hello library:\n " + e.toString() ); 
		} 
	} 


public native void greet(); 


public static void main(String[] args){
	new Hello().greet();
}
}

(1)编译这个java文件

$ javac -d . Hello.java ($为linux系统提示符)

(2)然后使用javah命令生成一个对应于native方法的c头文件(这个步骤不是必须的,后面会说到自己编写映射方法)

$ javah -o hello.h com.mypack.Hello (-o参数指定输入头文件名称)

(3)再编写一个cpp文件定义c头文件中的方法:

#include<iostream>
#include"hello.h"
using namespace std;

JNIEXPORT void JNICALL Java_com_mypack_Hello_greet(JNIEnv *env, jobject obj){
	cout<<"Hello world!"<<endl;
}
(4)编译这个cpp文件:

$ g++ -I/usr/lib/jvm/java-6-sun-1.6.0.26/include -I/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux -fPIC -c Hello.cpp (待会解释为什么这个命令会这么长)

(5)生成so库:

$ g++ -shared -o libhello.so Hello.o

(6)告诉动态链接器上面生成的libhello.so的路径

$ export LD_LIBRARY_PATH=`pwd`:$LD_LIBRARY_PATH

这样一切就大功告成了,直接运行命令$ java com.mypack.Hello,你就会看到可爱的"Hello world"出现的屏幕上.


现在解释一下上面的过程,我们先看一下用javah命令为我们生成的hello.h里边的内容:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_mypack_Hello */

#ifndef _Included_com_mypack_Hello
#define _Included_com_mypack_Hello
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_mypack_Hello
 * Method:    greet
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_mypack_Hello_greet
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

有必要解释一下这个文件,这是系统为我们生成的头文件,#ifndef  _Included_com_mypack_Hello不用说,头文件中常用的条件编译手段,防止重复包含的.

在extern "C"语句块中含有Java_com_mypack_Hello_greet方法的声明,为什么这个方法的名字这么长?因为JNI中规定c/c++方法只要是Java_包名_类名_方法名这种形式存在(也要注意方法的参数和方法修饰),那么会对应上java中相应的native方法,因此类com.mypack.Hello中的greet方法会对应这里的Java_com_mypack_Hello_greet方法.

那么为什么要使用extern "C"来修饰这里的方法声明呢?我们知道extern "C"是链接指示,它不能出现在类定义或者函数定义的内部,它必须出现在函数的第一次声明上,它的作用是:

指示Java_com_mypack_Hello_greet方法在用g++编译器编译出来的目标文件中的符号表名称按照gcc编译器的标准.

我们知道c语言编译器比较单纯,它不会偷偷背着你做一些事情,而c++的编译器则复杂而不可靠,用g++编译出来的目标文件中的符号表完全对应不上源文件中的名称(它有自己的规则),我们可以把hello.h中的extern "C" 语句去掉,看看符号表中的函数名称到底是什么.

去掉以后重新编译Hello.cpp,然后使用readelf命令查看符号表:

$ readelf -s -W Hello.o

可以看到有一行记录:

17: 00000000    68 FUNC    GLOBAL DEFAULT    3 _Z27Java_com_mypack_Hello_greetP7JNIEnv_P8_jobject

一长串的_Z27Java_com_mypack_Hello_greetP7JNIEnv_P8_jobject就是Java_com_mypack_Hello_greet函数在目标文件中的符号表.(可以用c++filt命令查看原始函数信息)

JNI根据java中的greet方法寻找Java_com_mypack_Hello_greet方法,但是目标文件中符号表的内容却是_Z27Java_com_mypack_Hello_greetP7JNIEnv_P8_jobject这么长串玩意,自然对应不上了,就会出错.

我们再恢复extern "C"指示,重新编译出一个目标文件,再用readelf命令查看符号表:

17: 00000000    68 FUNC    GLOBAL DEFAULT    3 Java_com_mypack_Hello_greet

这才是原原本本的函数名,这才能对应上,这也就是为什么要加extern "C'的原因.下面会说到,如果自己定义java方法和c/c++方法的映射关系,那么就不用加extern "C"链接指示.


下面解释一下上面编译的第四步:

$ g++ -I/usr/lib/jvm/java-6-sun-1.6.0.26/include -I/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux -fPIC -c Hello.cpp

由于hello.h包含了jni.h文件(它又包含了jni_md.h)文件,所以需要找到这两个头文件的路径,在我的机器上路径是/usr/lib/jvm/java-6-sun-1.6.0.26/include和/usr/lib/jvm/java-6-sun-1.6.0.26/include/linux,你可以使用find命令在/usr/lib目录下搜索自己机器上的头文件路径.

-fPIC参数的作用是产生与位置无关的代码,使得不需要链接器修改库代码就可以在任何地址加载和执行这些代码

上面的第五步:

$ g++ -shared -o libhello.so Hello.o

作用是生成动态链接库以libhello.so为名字,这里去掉lib和.so剩下的hello就是System.loadLibrary方法使用的名称,这种将库名映射到实际库的方法是linux定义的.

注意Java_com_mypack_Hello_greet(JNIEnv *env, jobject obj)方法中的JNIEnv指针和jobject类型,

JNI提供了两个关键的数据结构"JavaVM"和"JNIEnv",它们本质是都是函数指针的指针,"JavaVM"提供了调用接口的功能,可以用来创建和销毁一个JavaVM,而JNIEnv提供了大部分JNI的功能,c/c++相应的jni的函数都接受JNIEnv做为第一个参数.



你可能会想,如果我在c/c++中定义的名字不按Java_包名_类名_方法名这个规则来,如何让java方法和c/c++的方法映射上呢?这就需要一个jni的方法来帮忙了,这个方法叫

jint RegisterNatives(jclass clazz, 
const JNINativeMethod *methods, jint nMethods);
是JNIEnv对象中的方法,jclass对应于java类的Class对象,JNINativeMethod是一个结构体类型,它的结构如下:

typedef struct { 

    char *name; 

    char *signature; 

    void *fnPtr; 

} JNINativeMethod;

name为java中native方法的名称,signature为方法的签名信息,fnPtr是一个指针类型,传递c/c++中的函数指针,这样java方法和c/c++方法就对应上了.


直接用一例子说明,在Hello.java文件中添加一个新的native方法:

public native String getName(String name);//传递一个name给c++,c++什么都不做直接返回,然后java打印出这个字符串

然后Hello.cpp文件改动就比较大了,源文件如下:

#include<iostream>
#include"hello.h"
using namespace std;

JNIEXPORT void JNICALL Java_com_mypack_Hello_greet(JNIEnv *env, jobject obj){
	cout<<"Hello world!"<<endl;
}
JNIEXPORT jstring JNICALL cpp_getName(JNIEnv *env, jobject obj , jstring name){
	return name;	
}
static JNINativeMethod gMethods[] ={{"getName","(Ljava/lang/String;)Ljava/lang/String;",(void*)cpp_getName}};
static jint registerM(JavaVM* vm, void* reserved){
	JNIEnv* env = NULL;
	jclass clazz = NULL;
	const char* className = "com/mypack/Hello";

	if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
			cout<<"GetEnv error!"<<endl;
        	return -1;
	}
	clazz = env->FindClass(className);
	if (clazz == NULL) {
			cout<<"Find Class error!"<<endl;
        	return -1;
	}
	if (env->RegisterNatives(clazz, gMethods,sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
			cout<<"RegisterNatives error!"<<endl;
        	return -1;
    }
    return JNI_VERSION_1_4;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved){
	cout<<"JNI_Onload was called!"<<endl;
	if (registerM(vm, reserved) != JNI_VERSION_1_4){
		return -1;
	}
		return JNI_VERSION_1_4;
}

按照上面的步骤再来一次,传递一个"张三"到getName()方法,就会打印出:

JNI_Onload was called!
Hello world!
张三


可以看到我多写了一个JNI_OnLoad方法,这个方法属于回调函数,即程序员自己写实现由系统调用,它在jni.h文件中声明,而且加上了extern "C"指示(不加的话找不到此方法),它在库被加载的时候被调用(还有一个对应方法叫JNI_onUnLoad),这个方法需要返回使用的JNI版本,默认使用的是JNI_VERSION_1_1.

我在这个JNI_OnLoad方法中调用了registerM方法,此方法又会调用RegisterNatives实现java方法到c/c++方法的映射,

前面说过了RegisterNatives方法需要的三个参数:

1.第一个参数clazz通过JNIEnv的findClass方法获得,此方法需要传递一个表示类的全限定名称的字符串,这里传递的是"com/mypack/Hello"

2.第二个参数传递一个结构体数组名(gMethods),会被转换为第一个结构体元素的指针,结构体的第一个和第三个成员的含义一看便知,关键在于第二个成员

"(Ljava/lang/String;)Ljava/lang/String;"的含义,它代表着java中native方法的签名信息,包括了参数和返回类型,具体定义对应关系如下:

Z | boolean

B | byte

C | char

S | short

I | int

J | long

F | float

D | double

L全限定类名; | java全限定类名

[ type | type[]

(参数类型)返回类型 | java方法类型

举例:

long f(int n, String s,int[] arr);

有如下的签名:

(ILjava/lang/String;[I)J

那么我们这里的getName方法就有签名"(Ljava/lang/String;)Ljava/lang/String;"了

其实不用记忆这些签名信息的对应关系,使用javap -s -p YourClass就可以自动生成YourClass中所有方法的签名信息,将“Signature:”后边的内容填上去就行了

3.第三个参数为结构体数组中元素的数量,这里只映射了一个方法,值就为1了


java类型和本地类型的对应关系:


java类型本地类型说明
booleanjboolean无符号,8位
bytejbyte无符号,8位
charjchar无符号,16位
shortjshort有符号,16位
intjint有符号32位
longjlong有符号,64位
floatjfloat32位
doublejdouble64位
voidvoidN/A

可以看到本地类型前边有一个字母j,对于java中的类,在本地类型中的类型都为jobject(由于String是个比较特殊的类,而且比较常用,它在本地类型中有个对应的类型jstring,而且有一系列相应的函数与之对应)

我们可以打开jni.h和jni_md.h查看这些类型的定义是否是上表符合:

typedef unsigned char   jboolean;
  typedef unsigned short  jchar;
  typedef short           jshort;
  typedef float           jfloat;
  typedef double          jdouble;

 typedef int jint;
 #ifdef _LP64 /* 64-bit Solaris */
 typedef long jlong;
 #else
 typedef long long jlong;
 #endif
 typedef signed char jbyte;


下面再给出一个例子示范一下如何通过jni来调用java中的方法.

现在我在Hello.java中新建一个native方法:

public native void addMessage(List<String> list);
这个方法需要一个List<String>对象传递给底层,然后底层会往这个List里边添加一个字符串,返回上来以后在java中打印List的内容以检验是否添加成功.

添加完以后,编译这个Class类,同时用javah命令更新Hello.h头文件,相应的在Hello.cpp中添加如下代码:

JNIEXPORT void JNICALL Java_com_mypack_Hello_addMessage(JNIEnv *env, jobject obj, jobject list){
	jclass clazz = env->FindClass("java/util/List");
	if (clazz == NULL) {
			cout<<"Find Class error!"<<endl;
        	return;
	}
	jmethodID methodId = env->GetMethodID(clazz, "add", "(Ljava/lang/Object;)Z");
	if(methodId == NULL){
		cout<<"GetMethod ID error!"<<endl;
		return;
	}
	const char* str = "message from cpp";
	jstring s = env->NewStringUTF(str);
	env->CallBooleanMethod(list,methodId,(jobject)s);
}
首先调用FindClass方法获取List类的Class实例,再调用GetMethodId方法获取add方法的id信息,由于add方法的形参是Object(泛型参数T)返回值是boolean,所以方法签名信息应该为(Ljava/lang/Object;)Z.

获取了jmethodID以后,就可以通过CallBooleanMethod这个方法来调用List中的add方法了.

Call<type>Method有三种形态,这里的type为上面表格中的类型,NativeType与之对应,例如调用CallVoidMethod那么返回类型就为void,调用CallIntMethod那么返回类型就为jint.

NativeType Call<type>Method(jobject obj, jmethodID methodID, ...);

NativeType Call<type>MethodA(jobject obj, jmethodID methodID, jvalue *args);

NativeType Call<type>MethodV(jobject obj, jmethodID methodID, va_list args);

我在这里使用的是第一种形式,传递了一个jobject对象给add方法.

事实证明,"message from cpp"打印出来了,说明添加成功了.


最后想说明的是,Java的对象传递到了本地方法中,成为jobject类型,这是种强引用类型,会使原始对象便为强可触及状态,垃圾回收器不会回收这样的对象,传递到本地方法中的jobject为局部引用,它在本地方法中有效,本地方法返回后会被释放掉.也可以使用这种局部引用创建一个全局的引用,全局引用需要显式的释放,否则对象一直不会被回收了.

举个例子,我想把一个上面Hello.java类的jclass对象变为全局引用保存在本地层的一个全局变量中:

static jclass jcls;

由于我在registerM方法中获取到了这个jclass对象,那么可不可以在这里执行如下代码直接保存呢:

jcls = env->FindClass(className);

很遗憾不能这样做,因为FindClass方法返回的是局部引用,在本地方法返回以后会失效,就会出现无法预料的错误,在我的机器上直接虚拟机挂了.

如果想成功,就得这样调用NewGlobalRef函数:

clazz = env->FindClass(className);
jcls = (jclass)env->NewGlobalRef(clazz);
在释放的时候调用 DeleteGlobalRef函数.

更多jni api的用法请看:

http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/jniTOC.html




Logo

更多推荐