JNI完全解析

1.Android与JNI

从编程语言年地,Android Framework由基于Java语言的Java层与基于C/C++语言的C/C++层组成,每个层中的功能模块都是使用相应的语言编写的,并且两个层中的大部分模块之间保持着千丝成缕的联系。以Android GPS子系统为例,如下图所示,它提供终端的地理位置信息。在Android应用程序获取GPS信息时,需要先调用应用程序框架中Location Manager提供的Java API,而后通过调用框架内的GPS库,连接到GPS设备驱动上,应用程序即可获取当前地理位置信息。在此过程中,C/C++层与Java层相互作用、相互配合,共同完成任务。

GPS_framework

在Android Framework中,需要提供一种媒介或桥梁,将Java层(上层)与C/C++层(底层)有机地联系起来,使得它们相互协调,共同完成某些任务。在两层之间充当桥梁的就是Java本地接口(JNI,Java Native Interface),它允许Java代码与基于C/C++编写的应用程序和库进行交互操作。

Java提供了一系列接口,允许Java类与使用C/C++等其他编程语言(在JNI中,这些语言被称为本地语言)编写的应用程序、模块、库进行交互操作。比如,在Java类中使用C语言库中的特定函数,或在C语言程序中使用Java类库,都需要借助JNI来完成。

通常在以下几种情况下会使用JNI:

1)注重处理速度

与本地代码相比,Java代码的执行速度要慢一些。如果对某段程序的执行速度有较高的要求,建议使用C/C++编写代码。而后在Java中通过JNI调用基于C/C++编写的部分,常常能够获得更快的运行速度。

2)硬件控制

为了更好地控制硬件,硬件控制代码通常都使用C语言编写。而后借助JNI将其与Java层连接起来,从而实现对硬件的控制。

3)既有C/C++代码的复用

在程序编写过程中,常常会使用一些已经编写好的C/C++代码,既提高了编程效率,又确保了程序的安全性与健壮性。在复用这些C/C++代码时,就要通过JNI来实现。

2.简单的JNI示例

1)第一步:编写Java代码

HelloJni.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HelloJni{
static {
System.loadLibrary("hellojni");
}
native void printHello();
native void printString(String str);
public static void main(String[]args){
HelloJni helloJni=new HelloJni();
helloJni.printHello();
helloJni.printString("Hello world!\n");
}
}

2)第二步:编译Java代码,非常简单。而编译的目的其实是为了生成相应的函数签名。

编译
1
$javac HelloJni.java

3)第三步:生成相应的头文件,利用javah即可

生成头文件
1
$javah HelloJni

生成的头文件如下:

HelloJni.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJni */
#ifndef _Included_HelloJni
#define _Included_HelloJni
#ifdef __cplusplus
extern "C" {
#endif
/*
+ Class: HelloJni
+ Method: printHello
+ Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJni_printHello
(JNIEnv *, jobject);
/*
+ Class: HelloJni
+ Method: printString
+ Signature: (Ljava/lang/String;)V
*/
JNIEXPORT void JNICALL Java_HelloJni_printString
(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif

可以看到,里面包含了jni.h这个头文件以及sayHello这个Java方法对应的本地方法声明。

其中JNIEXPORT,JNICALL都是JNI关键字,表示此函数要被JNI调用,函数原型中必须有这两个关键字,JNI才能正常调用函数。其实,JNIEXPORT,JNICALL两个关键字都是宏定义,它们被定义在JDK_HOME/include/linux/jni_md.h文件中。

JNIEnv*是JNI接口的指针,用来调用JNI表中的各种JNI函数,如下图所示:

JNI

函数原型中的第二个默认参数类型jobject也是JNI提供的Java本地类型,用来在C代码中访问Java对象,此参数中保存着调用本地方法的对象的一个引用。例如上面的helloJni.sayHello();中helloJni对象调用了本地方法,上面JNI函数的第二个参数jobject中保存着对helloJni对象的引用。

Java类型与Java本地类型的对应列表如下:

Java类型 Java本地类型 占用内存大小
byte jbyte 1
short jshort 2
int jint 4
long jlong 8
float jfloat 4
double jdouble 8
char jchar 2
boolean jboolean 1
void void -

以上Java本地类型定义都可以在下面的头文件中找到:

  • 1)JDK_HOME/include/jni.h
  • 2)JDK_HOME/include/linux/jni_md.h

注意,如果是Windows平台,则jni_md.h的位置则不是这个路径。

  • 4)根据头文件编写相应的C/C++代码
    C语言代码如下:

    HelloJni.c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <jni.h>
    #include "HelloJni.h"
    /*
    + Class: HelloJni
    + Method: printHello
    + Signature: ()V
    */
    JNIEXPORT void JNICALL Java_HelloJni_printHello
    (JNIEnv *env, jobject obj)
    {
    printf("Hello JNI!\n");
    }
    /*
    + Class: HelloJni
    + Method: printString
    + Signature: (Ljava/lang/String;)V
    */
    JNIEXPORT void JNICALL Java_HelloJni_printString
    (JNIEnv *env, jobject obj, jstring str)
    {
    const char*content=(*env)->GetStringUTFChars(env,str,0);
    printf("%s\n",content);
    return;
    }
  • 5)生成so库
    Linux下gcc的使用一文中对于Linux下gcc的使用进行了详细的阐述,如果不有熟悉的小伙伴,建议先看一下那篇博客。

由于包含的头文件在JDK_HOME/include和JDK_HOME/include/linux下,所以编译成so库的命令如下:

$gcc -shared -fPIC hellojni.c -I /usr/lib/jvm/jdk7/include -I /usr/lib/jvm/jdk7/include/linux -o libhellojni.so
  • 6)运行
    Linux下gcc的使用一文中讲解过库路径、文件路径与环境变量的问题,这里存在同样的问题,因而在运行之前先要将当前的路径添加进去:
    link
    1
    2
    3
    $export LD_LIBRARY_PATH=LD_LIBRARY_PATH:$PWD
    $export CLASSPATH=$CLASSPATH:$PWD
    $java HelloJni

之后可看到正确的输出结果。

值得注意的是,上面是以C语言为例子,如果采用C++的话,除了在编译so库时将gcc换成g++外,其它均一样,所以此处不再示例。

3.Java和C相互调用示例

上面举了Java调用C的例子,下面的例子中则包含了C调用Java中的对象及方法等。
首先是Java代码:

JniFuncMain.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class JniFuncMain
{
private static int num=300;
static {System.loadLibrary("jnifunc");}
public static native JniTest createJniObject();
public static void main(String[]args)
{
System.out.println("[Java] createJniObject() call native method");
JniTest jniObj=createJniObject();
jniObj.callTest();
}
}
class JniTest
{
private int count;
public JniTest(int num)
{
this.count=num;
System.out.println("[Java] constructor: count="+count);
}
public int callByNative(int num)
{
System.out.println("[Java] JniTest method callByNative("+num+")");
return num;
}
public void callTest()
{
System.out.println("[Java] JniTest callTest() method,count="+count);
}
}

编译后通过javah生成相应的头文件,头文件如下所示:

JniFuncMain.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JniFuncMain */
#ifndef _Included_JniFuncMain
#define _Included_JniFuncMain
#ifdef __cplusplus
extern "C" {
#endif
/*
+ Class: JniFuncMain
+ Method: createJniObject
+ Signature: ()LJniTest;
*/
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif

此处采用C++来实现,jnifunc.cpp的代码如下:

jnifunc.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include "JniFuncMain.h"
#include <stdio.h>
#include <jni.h>
/*
+ Class: JniFuncMain
+ Method: createJniObject
+ Signature: ()LJniTest;
*/
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject
(JNIEnv *env, jclass clazz)
{
jclass targetClass;
jmethodID mid;
jobject newObject;
jstring helloStr;
jfieldID fid;
jint num;
jint result;
fid=env->GetStaticFieldID(clazz,"num","I");
num=env->GetStaticIntField(clazz,fid);
printf("[CPP] get the value of field num%d\n",num);
targetClass=env->FindClass("JniTest");
mid=env->GetMethodID(targetClass,"<init>","(I)V");
printf("[CPP] JniTest object\n");
newObject=env->NewObject(targetClass,mid,100);
mid=env->GetMethodID(targetClass,"callByNative","(I)I");
result=env->CallIntMethod(newObject,mid,200);
fid=env->GetFieldID(targetClass,"count","I");
printf("[CPP] set the value of count in JniTest Object\n");
env->SetIntField(newObject,fid,result);
return newObject;
}

程序通过JNI访问Java类/对象的成员变量按如下顺序进行:

  • 1)查找含待访问的成员变量的Java类的jclass值;
  • 2)获取此类成员变量的jfieldID值。若成员变量为静态变量,则调用名称为GetStaticFieldID()的JNI函数;若待访问的成员变量是普通对象,则调用名称为GetFieldID()的JNI函数;
  • 3)使用1,2中获得的jclass与jfieldID值。

下面重点讲解一下GetStaticFieldID()和GetFieldID()函数。

jfield GetStaticFieldID(JNIEnvenv,jclass clazz,const charname,const char*signature);

jfield GetFieldID(JNIEnvenv,jclass clazz,const charname,const char*signature);

其中env-JNI接口指针
clazz-包含成员变量的类的jclass
name-成员变量名
signature-成员方法签名

那么如何获取成员方法签名呢?
在jdk中有一个javap命令(Java反编译器),利用它可轻松获取指定的成员变量或成员方法的签名。

形式: javap [选项] 类名  
选项:-s 输出Java签名  
      -p 输出所有类及成员  

比如这里使用javap -s JniFuncMain命令,输出如下:

输出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Compiled from "JniFuncMain.java"
public class JniFuncMain{
public JniFuncMain();
Signature: ()V
public static native JniTest createJniObject();
Signature: ()LJniTest;
public static void main(java.lang.String[]);
Signature:([Ljava/lang/String;])V
static {};
Signature: ()V
}

然后,编译获取so库的命令为:

编译so库
1
$g++ -shared -fPIC jnifunc.cpp -I /usr/lib/jvm/jdk7/include -I /usr/lib/jvm/jdk7/include/linux -o libjnifunc.so

后面的步骤与前一个示例相似,此处不再赘述。输出结果如下:

output
1
2
3
4
5
6
7
[Java] createJniObject() call native method
[CPP] get the value of field num300
[CPP] JniTest object
[Java] constructor: count=100
[Java] JniTest method callByNative(200)
[CPP] set the value of count in JniTest Object
[Java] JniTest callTest() method,count=200

从输出结果可看出,Java通过JNI调用了native方法,C++中通过JNI调用了Java中的成员变量及方法。

实际上在Android系统中,也有大量的代码使用了JNI函数,这些代码存在于Android源码的相关目录中。比如在Android 2.2.1的如下目录下就有:

dir
1
2
3
frameworks/base/core/jni
frameworks/base/services/jni
frameworks/base/media/jni

关于这些,后面还会详细讲解,此处先提一下。

4.在C程序中运行Java类

前面示例代码中的主程序都是使用Java语言编写的,通过这些示例,我们学习了在Java代码中如何通过本地方法调用C函数,即使用JNI的方式。在这个小节中,我们将一起学习在由C/C++编写的主程序中如何运行Java类,这也是使用JNI的重要方式。

我们知道,由Java类编译生成的字节码必须运行在Java虚拟机上,那么在C/C++编写的程序中运行Java类或对象还需要Java虚拟机吗?

答案是肯定的。为此,JNI提供了一套Invocation API,它允许本地代码在自身内存区域内加载Java虚拟机。在C代码中,如何使用Invocation API,装载Java类,并运行指定的方法呢?这就是本节要讲解的主要内容。

下面列出的情况就是需要使用Invocation API在C/C++代码中调用Java代码的几种典型情况。

  • 1)需要在C/C++编写的本地应用程序中访问用Java语言编写的代码或代码库;
  • 2)希望在C/C++编写的本地应用程序中使用标准Java类库;
  • 3)当需要把已有的C/C++程序与Java程序组织链接在一起时,使用Invocation API,可以将它们组织成一个完整的程序。

首先是Java程序,非常简单:

InvocationApiTest.java
1
2
3
4
5
6
7
public class InvocationApiTest
{
public static void main(String[]args)
{
System.out.println(args[0]);
}
}

然后是C代码(invocationApi.c):

invocationApi.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <jni.h>
int main()
{
JNIEnv*env;
JavaVM*vm;
JavaVMInitArgs vm_args;
JavaVMOption options[1];
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
//生成Java虚拟机选项
options[0].optionString="-Djava.class.path=.";
vm_args.version=0x00010002;
vm_args.options=options;
vm_args.nOptions=1;
vm_args.ignoreUnrecognized=JNI_TRUE;
//创建Java虚拟机
res=JNI_CreateJavaVM(&vm,(void**)&env,&vm_args);
//查找并加载类
cls=(*env)->FindClass(env,"InvocationApiTest");
//根据main方法的签名获取该方法的ID
mid=(*env)->GetStaticMethodID(env,cls,"main","([Ljava/lang/String;)V");
//生成字符串对象,用作main()方法的参数
jstr=(*env)->NewStringUTF(env,"Hello Invocation API!");
stringClass=(*env)->FindClass(env,"java/lang/String");
args=(*env)->NewObjectArray(env,1,stringClass,jstr);
//调用main()方法
(*env)->CallStaticVoidMethod(env,cls,mid,args);
//销毁Java虚拟机
(*vm)->DestroyJavaVM(vm);
}

在生成Java虚拟机选项时,使用JavaVMInitArgs和JavaVMOption结构体,它们定义在jni.h文件中,如下所示:

jni.h
1
2
3
4
5
6
7
8
9
10
11
typedef struct JavaVMInitArgs{
jint version;
jint nOptions;
JavaVMOption*options;
jboolean ignoreUnrecognized;
} JavaVMInitArgs;
typedef struct JavaVMOptions{
char*optionString;
void*extraInfo;
} JavaVMOption;

此处由于包含与jvm相关的库,所以编译命令稍微复杂一点:

compile
1
$gcc -g -I /usr/lib/jvm/jdk7/include -I /usr/lib/jvm/jdk7/include/linux -L/usr/lib/jvm/jdk7/jre/lib/amd64/server invocationApi.c -ljvm -o main

另外,由于使用到了libjvm.so,所以如果直接./main的话会出现找不到libjvm.so的错误。解决方法也很简单,只要将/usr/lib/jvm/jdk7/jre/lib/amd64/server加入到链接路径即可:

link
1
$export LD_LIBRARY_PATH=LD_LIBRARY_PATH:/usr/lib/jvm/jdk7/jre/lib/amd64/server/

之后运行即可得到”Hello Invocation API!”的输出。

5.直接注册JNI本地函数

通过前面的简单示例学习,我们已经知道Java虚拟机在运行包含本地方法的Java应用程序时,要经过以下两个步骤:

  • 1).调用System.loadLibrary()方法,将包含本地方法具体实现的C/C++运行库加载到内存中;
  • 2).Java虚拟机检索加载进来的库函数符号,在其中查找与Java本地方法拥有相同签名的JNI本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数。
    像简单示例中那样,若JNI支持的功能函数只有一个,Java虚拟机在将本地方法与C运行库中的JNI本地函数映射在一起时,不会耗费很长时间。但在Android Framework这类复杂的系统下,拥有大量的包含本地方法的Java类,Java虚拟机加载相应运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率。

为了解决这一问题,JNI机制提供了名为RegisterNatives()的JNI函数,该函数允许C/C++开发者将JNI本地函数与Java类的本地方法直接映射在一起。当不调用RegisterNatives()函数时,Java虚拟机会自动检索并将JNI本地函数与相应的Java本地方法链接在一起。但当开发者直接调用RegisterNatives()函数进行映射时,Java虚拟机就不必进行映射处理,这会极大提高运行速度,提升运行效率。

由于程序员直接将JNI本地函数与Java本地方法链接在一起,在加载运行库时,Java虚拟机不必为了识别JNI本地函数而将JNI本地函数的名称与JNI支持的命名规则进行比对,即任何名称的函数都能直接链接到Java本地方法上。

首先分析System.loadLibrary()方法的执行过程。在Java代码中,调用System.loadLibrary()方法时,Java虚拟机会加载其参数指定的共享库。然后,Java虚拟机检索共享库内的函数符号,检查JNI_OnLoad()函数是否被实现,若共享库中含有相关函数,则JNI_OnLoad()函数就会被自动调用。否则,像前面的libhellojni.so一样,库中的JNI_OnLoad()函数未被实现,则Java虚拟机会自动将本地方法与库内的JNI本地函数符号进行比较匹配。

在加载指定的库文件时,JNI_OnLoad()函数会被自动调用执行,程序开发者若想手工映射本地方法与JNI本地函数,需要在JNI_OnLoad()函数内调用RegisterNatives()函数进行映射匹配。

jint JNI_OnLoad(JavaVMvm,voidreserved) 其中vm:JavaVM接口指针 reserved:预定参数

若执行成员,则返回所生成的数组引用,否则返回NULL.

仍以HelloJni那个例子为例,HelloJni.java的代码不需要改变,但是用hellojni.cpp代替hellojni.c,其中hellojni.cpp的代码如下:

hellojni.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include "jni.h"
#include <stdio.h>
void printHelloNative(JNIEnv*env,jobject obj);
void printStringNative(JNIEnv*env,jobject obj,jstring string);
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM*vm,void *reserved)
{
JNIEnv*env=NULL;
JNINativeMethod nm[2];
jclass cls;
jint result=-1;
if(vm->GetEnv((void**)&env,JNI_VERSION_1_4)!=JNI_OK){
printf("Error");
return JNI_ERR;
}
cls=env->FindClass("HelloJni");
nm[0].name="printHello";
nm[0].signature="()V";
nm[0].fnPtr=(void*)printHelloNative;
nm[1].name="printString";
nm[1].signature="(Ljava/lang/String;)V";
nm[1].fnPtr=(void*)printStringNative;
env->RegisterNatives(cls,nm,2);
return JNI_VERSION_1_4;
}
void printHelloNative(JNIEnv*env,jobject obj)
{
printf("Hello Jni!\n");
return;
}
void printStringNative(JNIEnv*env,jobject obj,jstring string)
{
const char*str=env->GetStringUTFChars(string,0);
printf("%s",str);
return;
}

由于是采用C++,所以编译要采用g++,编译命令如下:

compile
1
$g++ -shared -fPIC hellojni.cpp -I /usr/lib/jvm/jdk7/include -I /usr/lib/jvm/jdk7/include/linux -o libhellojni.so

编译时会有一些Warning,可忽略。
在运行之前,需要利用export将当前目录加入到链接库路径中:

link
1
$export LD_LIBRARY_PATH=LD_LIBRARY_PATH:$PWD

然后就可以直接运行了:

run
1
$java HelloJni

可看到正确的输出,说明本地方法调用成功。