JNI的基本原理
** 在Java中调用C库函数
开发流程
------
在Java代码中通过JNI调用C函数的步骤如下:
第一步: 编写Java代码
第二步: 编译Java代码
第三步: 生成C语言头文件
第四步: 编写C代码
第五步: 生成C共享库
第六步: 运行Java程序
*** 第一步 编写Java代码
JNI方法是在Java代码中声明的。
在Java类中,使用"native"关键字,声明本地方法该方法与用C/C++编写的JNI本地函数相对应。"native"关键字告知Java编译器,在Java代码中带有该关键字的方法只是声明,具体由C/C++等其他语言编写实现。
如果起吊方法前的native关键字,编译代码时,Java编译器就会报错,抛出编译错误,告知该方法没有实现。
调用System.loadLibrary()方法加载具体的实现本地方法的C运行库。System.loadLibrary()方法加载由字符串参数指定的本地库,在不同操作系统平台下,加载的C运行库不同。
*** 第二步 编译Java代码
#+BEGIN_SRC java
javac xxx.java
#+END_SRC
生成 xxx.class
*** 第三步 生成C语言头文件
#+BEGIN_SRC java
javah -classpath path classname
#+END_SRC
生成classname.h
| Java类型 | Java本地类型 |
|----------+--------------|
| / | < |
|----------+--------------|
| byte | jbyte |
| short | jshort |
| int | jint |
| long | jlong |
| float | jfloat |
| double | jdouble |
| char | jchar |
| boolean | jboolean |
| void | void |
Java本地类型也提供了另外三种类型
| java引用类型 | java本地类型 |
|--------------+--------------|
| / | < |
|--------------+--------------|
| 对象 | Jobject |
| String | Jstring |
*** 第四步 编写C/C++代码
编写xxx.c文件
*** 第五步 生成C共享库
#+BEGIN_SRC sh
cc -I/usr/lib/jvm/java-6-sun/include/linux
-I/usr/lib/jvm/java-6-sun/include/
-fPIC -shared -o libxxx.so xxx.c
#+END_SRC
*** 第六步 运行Java程序
#+BEGIN_SRC java
java -cp path -o java.library.path='path' classname
#+END_SRC
** 小结
(1)在java类中声明本地方法
(2)使用javah命令,生成包含JNI本地函数原型的头文件
(3)实现JNI本地函数
(4)生成C共享库
(5)通过JNI,调用JNI本地函数
* 调用JNI函数
在由C语言编写的JNI本地函数中如何控制Java端的代码
- 创建Java对象
- 访问静态成员域
- 调用类的静态方法
- 访问Java对象的成员变量
- 访问Java对象的方法
** 调用JNI函数的示例程序结构
** Java层代码 (JniFuncMain.java)
1.JniFuncMain类
#+BEGIN_SRC java
public class JniFuncMain
{
print static int staticIntField = 300;
// 加载本地库
static { System.loadLibrary("jnifunc"); }
// 本地方法声明
public static native JniTest createJniObject();
public static void main(String[] args)
{
// 从本地代码生成JniTest对象
System.out.println("[Java] createJniObject() 调用本地方法");
JniTest jniObj = createJniObject();
// 调用JniTest对象的方法
jniObj.callTest();
}
}
#+END_SRC
JniFuncMain.java中的JniFuncMain类
+ 通过java静态块,在调用本地方法前,加载jnifunc运行库
+ 使用static关键字声明本地方法createJniObject()在调研那个此方法时不需要创建对象,直接通过JniFuncMain类调用即可
+ 不使用Java语言的new运算符,调用与createJniObject()本地方法相对应的C函数生成JniTest类的对象,在将对象的引用保存在jniObj引用变量中
+ 调用jniObj对象的callTest()方法
2.JniTest类
#+BEGIN_SRC java
class JniTest
{
private int intField;
//构造方法
public JniTest(int num)
{
intField = num;
System.out.println("[Java] 调用JniTest对象的构造方法:intField = " + intField);
}
// 此方法由JNI本地函数调用
public int callByNative(int num)
{
System.out.println("[Java] JniTest 对象的 callByNative("+ num +")调用");
return num;
}
public void callTest()
{
System.out.println("[Java] JniTest 对象的 callTest() 方法调用:intField="intField");
}
}
#+END_SRC
** 分析JNI本地函数代码
**** JniFuncMain.h头文件
使用javah命令,生成本地方法的函数原型
#+BEGIN_SRC java
javah JniFuncMain
#+END_SRC
JniFuncMain.h
#+BEGIN_SRC c
/* 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
#+END_SRC
createJniObject()本地方法对应的JNI本地函数原型,形式如下
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *, jclass)
**** jnifunc.cpp 文件
#+BEGIN_SRC C++
JNIEXPORT jobject JNICALL Java_JniFuncMain_createJniObject(JNIEnv *env, jclass clazz)
{
jclass targetClass;
jmethodID mid;
jobject newObject;
jstring helloStr;
jfieldID fid;
jint staticIntField;
jint result;
// 获取JniFuncMain类的staticIntField变量值
fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
staticIntField = env->GetStaticIntField(clazz, fid);
printf("[CPP] 获取JniFuncMain类的staticIntField值\n");
printf(" JniFuncMain.staticIntField = %d\n", staticIntField);
// 查找生成对象的类
targetClass = new->NewObject(targetClass, mid , 100);
// 查找构造方法
mid = env->GetMethodID(targetClass, "<init>", "(I)V");
// 生成JniTest对象(返回对象的引用)
printf("[CPP]JniTest对象生成\n");
newObject = env->NewObject(targetClass, mid, 100);
// 调用对象的方法
mid = env->GetMethodID(targetClass,"callByNative", "(I)I");
result = env->CallIntMethod(newObject, mid , 200);
//设置JniObject对象的intField值
fid = env->GetFieldID(targetClass, "intField", "I");
printf("[CPP] 设置JniTest对象的intField值为200\n");
env->SetIntField(newObject, fid, result);
//返回对象的引用
return newObject;
}
#+END_SRC
**** 通过JNI,获取成员变量值
下面代码用于获取JniFuncMaind类的staticIntField成员变量的值
#+BEGIN_SRC c
// 1. 查找含有待放文成员变量的JniFuncMain类的jclass值
// 2. 获取staticField变量的ID值
fid = env->GetStaticFieldID(clazz, "staticIntField", "I");
// 3. 读取jclass与fieldid指定的成员变量值
staticIntField = env->GetStaticIntField(clazz, fid);
#+END_SRC
程序通过JNI访问java类/对象的成员变量安如下顺序进行:
(1) 查找含待放文的成员变量的Java类的jclass值
(2) 获取此类成员变量的jfieldID值。若成员变量为静态变量,则调用名称为GetStaticFieldID()的JNI函数;若待访问的成员变量是普通对象,则调用名称为GetFieldID()的JNI函数。
(3) 使用12中获得的jclass与jfieldID值,获取或设置成员变量值。
依据以上顺序,待读取树脂的staticIntField成员变量在JniFuncMain类被声明。JniFuncMain类的jclass值被传递给JNI本地函数 =java_JniFuncMain_createJniObject()= 的第二个参数中,若想获取指定类的jclass值,调用JNI函数FindClass()即可。
若想在本地代码中访问Java的成员变量,必须获取相应成员变量的ID值。例子中成员变量的ID保存在jfieldID类型的变量中。由于待读取数值的staticIntField成员变量时JniFUncMain类的静态变量,在获取staticIntField的ID时,影调用名称为GetStaticFieldID()的JNI函数。
在例子中的GetStaticFieldID()函数,与下表中的GetStaticFieldID()函数原型有些不同,函数原型中带有四个参数,而代码中仅有三个,缺少了env参数,这不是错误,而是与所用的编程语言相关。具体请参考后面Tip中关于JNI函数编码风格的说明。
| JNI函数 - GetStaticFieldID() | |
|------------------------------+--------------------------------------------------------------------------------------------|
| / | < |
| 形式 | jfield GetStaticFieldID(JNIEnv *env, jclass clazz, const char*name, const char *signature) |
|------------------------------+--------------------------------------------------------------------------------------------|
| 说明 | 返回指定类的指定的静态变量的jfieldID的值 |
|------------------------------+--------------------------------------------------------------------------------------------|
| 参数 | env-JNI接口指针 clazz-包含成员变量的类的jclass name-成员变量名 signature-成员变量签名 |
| JNI函数 - GetFieldID() | |
|------------------------+---------------------------------------------------------------------------------------|
| / | < |
| 形式 | jfield GetFieldID(JNIEnv *env, jclass clazz, const char *name, const char *signature) |
|------------------------+---------------------------------------------------------------------------------------|
| 说明 | 返回对象中指定的成员变量的jfieldID的值 |
|------------------------+---------------------------------------------------------------------------------------|
| 参数 | env-JNI接口指针 clazz-包含成员变量的类的jclass name-成员变量名 signatuer-成员变量签名 |
以上两个函数都要去提供成员变量的签名。成员变量与成员方法都拥有签名,使用 =<JDK_HOME>/bin= 目录下的javap命令(java反编译器),可以获取成员变量活成员方法签名。
Tip: 在JNI中获取成员变量活成员方法签名
形式: javap =[选项]= '类名'
选项: -s 输出java签名
-p 输出所有类及成员
在获取成员变量所在的类与ID后,根据各个成员变量的类型与存储区块(static或non-static),调用相应的JNI函数读取成员变量值即可。在JNI中有两种函数用来获取成员便令的值,分别为Get<type>Field函数与GetStatic<type> Field函数。<type>指Int, Char, Double等基本数据类型,具体参考JNI文档。
| JNI函数 GetStatic<type>Field | |
|------------------------------+------------------------------------------------------------------------------------------------------|
| / | < |
| 形式 | <jnitype>GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID) |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 说明 | 返回clazz类中ID为fieldID的静态变量的值 |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 参数 | env-JNI接口指针 clazz-包含成员变量的类 fieldID-成员变量的ID |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 参考 | <type>指Object、Boolean、Byte、Char、Short、Int、Long、Float、Double九种基本类型 |
| | 返回类型<jnitype>指jobject、jboolean、jbyte、jchar、jshort、jint、jlong、jfloat、jdouble九种基本类型 |
|------------------------------+------------------------------------------------------------------------------------------------------|
| 返回值 | 返回静态成员变量的值 |
| JNI函数 Get<type>Field | |
| / | < |
|------------------------+---------------------------------------------------------------------|
| 形式 | <jnitype>Get<type>Field(JNIEnv *env, Jobject obj, jfieldID fieldID) |
|------------------------+---------------------------------------------------------------------|
| 说明 | 返回obj对象中ID为fieldID的成员变量的值 |
|------------------------+---------------------------------------------------------------------|
| 参数 | env-JNI接口指针 |
| | obj-包含成员变量的对象 |
| | fieldID-成员变量的ID |
|------------------------+---------------------------------------------------------------------|
| 返回值 | 返回成员变量的值 |
由于staticIntField是Int类型的静态成员变量,所以调用GetStaticFieldID()函数即可获取StaticIntField的值.
生成对象
在JNI本地函数中如何生成Java类对象呢?
-----
// 1. 查找生成对象的类
targetClass = env->FindClass("JniTest");
// 2. 查找类的构造方法
mid = env->GetMethodID(targetClass, "<init>", "(I)V");
// 3. 生成JniTest类对象(返回对象引用)
newObject = env->NewObject(targetClass, mid, 100);
-----
通过JNI函数,生成Java对象的顺序如下:
1. 查找指定的类,并将查找到的类赋值给jclass类型的变量。
2. 查找java类构造方法的ID值,类型为jmethodID。
3. 生成java类对象
首先调用JNI函数FindClass(),查找生成对象的类。将类名作为FindClass()函数参数,查找并获得jclass值
| JNI函数 FindClass | |
|-------------------+-------------------------------------------------|
| 形式 | jclass FindClass(JNIEnv *env, const char *name) |
|-------------------+-------------------------------------------------|
| 说明 | 查找name指定的Java类 |
|-------------------+-------------------------------------------------------|
| 参数 | env-JNI接口指针 |
| | name-待查找的类名 |
|-------------------+-------------------------------------------------|
| 返回值 | 返回jclass的值 |
获取类的构造方法的ID并保存在jmethodID变量中。在JNI函数中有一个GetMethodID()函数用来获取指定类的指定方法ID。此函数除了可以用来获取指定类的构造方法的ID外,还可以获取类的其他的方法的ID。若指定的是静态方法,则可以调用JNI函数中的GetStaticMethodID()函数,获得指定静态方法的ID。
| JNI函数 GetMethodID | |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 形式 | jmethodID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *signature) |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 说明 | 获取clazz类对象的指定方法ID。注意,方法名(name)与签名应当保持一致。若获取类构造方法的ID,方法名应为<init> |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | clazz:Java类 |
| | name:方法名 |
| | signature:方法签名 |
|---------------------+-----------------------------------------------------------------------------------------------------------|
| 返回值 | 若方法ID错误,则返回NULL |
以类的jclass与构造方法ID为参数,调用函数NewObject()函数生成JniTest类的对象。JniTest类的构造方法JniTest(int num)带有一个int类型的参数,在调用NewObject()时,同时传入100这一int数据。在生成类对象后,将对象的引用保存在jobject变量中。
| JNI函数 NewObject | |
|-------------------+-----------------------------------------------------------------------|
| 形式 | jobject NewObject(JNIEnv *env, jclass clazz, jmethodID methodID, ...) |
|-------------------+-----------------------------------------------------------------------|
| 说明 | 生成指定类的对象。methodID指类的构造方法的ID |
|-------------------+-----------------------------------------------------------------------|
| 参数 | env:JNI接口指针 |
| | clazz: Java类 |
| | methodID:类的构造方法的ID |
| | ...:传递给类构造方法的参数 |
|-------------------+-----------------------------------------------------------------------|
| 返回值 | 返回类对象的引用。若发生错误,返回NULl |
Tip: 局部引用与全局引用
在实现JNI本地函数时,由GetObjectClass()、FindClass()等JNI函数返回的jclass\jobject等引用都是局部引用(Local Reference)
局部引用是JNI默认的,它仅在JNI本地函数内部有效,即当JNI本地函数返回后,其内部的引用就会失效。
在JNI编程中,实现JNI本地函数时,必须准确地理解局部引用的含义。
下面再举一个例子进一步详细的说明一下。
#+BEGIN_SRC java
class RefTest
{
public static int intField;
public static void setField(int num) {
int Field = num;
}
}
public class RefTestMain
{
// 加载本地库
static { System.loadLibrary("reftest"e); }
// 声明本地方法
public static native int getMember();
public static void main(String[] args) {
RefTest.setField(100);
System.out.println("intField = " + getMember());
RefTest.setField(200);
System.out.println("intField = " + getMember());
}
}
#+END_SRC
其中,本地方法getMember()的具体实现在reftest.cpp中。为了说明局部引用问题,声明了一个静态jclass变量targetClass,准备保存类的引用。
#+BEGIN_SRC c
static jclass targetClass = 0;
JNIEXPORT jint JNICALL Java_RefTestMain_getMember(JNIEnv *env, jclass clazz)
{
jfieldID fid;
jint intField;
jclass targetClass;
if(targetClass == 0) {
targetClass = env->FindClass(RefTest");
}
fid = env->GetStaticFieldID(targetClass, "intField", "I");
intField = env->GetStaticIntFIeld(targetClass, fid);
return intField;
}
#+END_SRC
运行程序会报错,原因在于JNI函数中的if (targetClass == 0)的判断,在java中的两次调用,第一次调用时targetClass还为0,第二次就不为0了。第二次没有调用FindClass造成出现错误。
为了解决这一问题,JNI提供了一个名为NewGlobalRef()的JNI函数,用来为指定的类或对象生成全局引用(Global Reference),以便在JNI本地函数中在全局范围内使用该引用。
| JNI函数 NewGlobalRef | |
|----------------------+------------------------------------------------|
| 形式 | jobject NewGlobalRef(JNIEnv *env, jobject obj) |
|----------------------+------------------------------------------------|
| 说明 | 为obj指定的类或对象,生成全局引用 |
|----------------------+------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | obj: 待生成全局引用的引用值 |
|----------------------+------------------------------------------------|
| 返回值 | 返回生成的全局引用,所发生错误,返回NULL |
当全局引用使用完后,应当调用名称为DeleteGlobalRef()的JNI函数,显性的将全局引用销毁。
#+BEGIN_SRC c
#include "RefTestMain.h"
static jclass globalTargetClass = 0;
JNIEXPORT jint JNICALL Java_RefTestMain_getMember (JNIEnv *env, jclass jclazz)
{
jfieldID fid;
jint intField;
jclass targetClass;
if(globalTargetClass == 0) {
targetClass = env->FindClass("RefTest");
globalTargetClass = (jclass)env->NewGlobalRef(targetClass);
}
fid = env->GetStaticFieldID(globalTargetClass, "initField", "I");
intField = env->GetStaticIntField(globalTargetClass, fid);
return intField;
}
#+END_SRC
上面代码调用了NewGlobalRef()函数,将targetClass中保存的RefTest类的局部引用(由FindClass()函数返回)转换成全局引用。并且将生成的全局引用保存在globalTargetClass静态变量中。
局部引用在函数执行完程后即无效。而全局引用除非调用DeleteGlobalRef()明确将其销毁,不然这个全局引用总是有效的,可以在运行库的其他函数中使用该引用。
**** 调用Java方法
下面描述了如何使用JNI函数调用Java方法,并将返回值保存至JNI本地函数的变量中的过程
-----
// 1. 获取含待调用方法的Java类的jclass
targetClass = env->GetObjectClass(newObject);
// 2. 获取待调用方法的ID
mid = env->GetMethodID(targetClass, "callByNative", "(I)I");
// 3. 调用Java方法 保存返回值
result = env->CallIntMethod(newObject, mid, 200);
-----
通过JNI调用Java方法的顺序如下
1. 获取含待调用方法的Java类的jclass。若待调用方法属于某个Java类对象,则该方法用来获取Java类对象的jobject。
2. 调用GetMethodID()函数,获取待调用方法的ID(jMethodID)。使用jclass与GetMethodID()函数
3. 根据返回值类型,调用相应的JNI函数,实现对Java方法的调用。若待调用的Java方法是静态方法,则调用函数的形式应为CallStatic<type>Method();若待调用的方法属于某个类对象,则调用函数的形式应为Call<type>Method()。
程序首先获取含callByNative()方法的JniTest类的jclass。在获取JniTest类的jclass时,可以直接调用FindClass()函数,将类引用保存在targetClass中。但是为了向各位介绍GetObjectClass()这个JNI函数,因而在此调用了GetObjectClass()函数。
| JNI函数 CallStatic<type>Method() | |
|----------------------------------+------------------------------------------------------------------------------------|
| 形式 | <jnitype>CallStatic<type>Method(JNIEnv *env, jcalss clazz,jmethodID methodID, ...) |
|----------------------------------+------------------------------------------------------------------------------------|
| 说明 |调用methodID指定的类的静态方法 |
|----------------------------------+------------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | clazz: 含待调方法的类 |
| | methodID:待调方法的ID 由GetStaticMethodID()函数获取 |
| | ...:传递给待调方法的参数 |
|----------------------------------+------------------------------------------------------------------------------------|
| 返回值 | 被调方法的返回值 |
|----------------------------------+------------------------------------------------------------------------------------|
| 参考 | <type>除了前面说<Get<type>FieldID()时列出的九种外又添加了void类型,返回值<jnitype>也增加了void类型。 待调方法的返回值不同,<type>也不同。若待调方法的返回值类型为int, 则调用函数为CallStaticIntMethod() |
| JNI函数 Call<type>Method() | |
|----------------------------+-------------------------------------------------------------------------------|
| 形式 | <jnitype>Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...) |
|----------------------------+-------------------------------------------------------------------------------|
| 说明 | 调用methodID指定的java对象的方法 |
|----------------------------+-------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | obj: 含待调方法的Java对象的引用 |
| | methodID: 待调用方法的ID,由GetMethodID()函数来获取 |
| | ...: 传递给待调用方法的参数 |
|----------------------------+-------------------------------------------------------------------------------|
| 返回值 | 被调用方法的返回值 |
**** 通过JNI设置成员变量的值
-----
// 1. 获取含IntField成员变量的JniTest类的jclass值
// 类引用已经被保存到targetClass中
// 2. 获取JniTest对象的IntField变量值
fid = env->GetFieldID(targetClass, "intField", "I");
// 3. 将result值设置为IntField值
env->SetIntField(newObject, fid, resutl);
-----
| JNI函数 SetStatic<type>Field | |
|------------------------------+-------------------------------------------------------------------------------------|
| 形式 | void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, <type>value) |
|------------------------------+-------------------------------------------------------------------------------------|
| 说明 | 设置fieldID指定的Java类静态成员变量的值 |
|------------------------------+-------------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | clazz: 含待设置成员变量的类的引用 |
| | fieldID: 待设成员变量的ID,由GetStaticFieldID()函数获取 |
| | value: 指定设置值 |
| JNI函数 Set<type>Field | |
|------------------------+--------------------------------------------------------------------------------|
| 形式 | void Set<type>Field (JNIEnv *env, jobject obj, jfieldID fieldID, <type> value) |
|------------------------+--------------------------------------------------------------------------------|
| 说明 | 设置fieldID指定的Java对象的成员变量 |
|------------------------+--------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | obj: 包含待设成员变量的Java对象的引用 |
| | fieldID: 待设成员变量的ID,由GetFieldID()函数获取 |
| | value:指定设置值 |
* 在C程序中运行Java类
本节中学习在由C/C++编写的主程序中如何运行Java类,这也是使用JNI的重要方式。
在C/C++程序中运行Java类也必须使用Java虚拟机。为此JNI提供了一套Invocation API,它允许本地代码在自身内存区域内加载Java虚拟机。
下面列出的可能使你决定使用Invocation API在C/C++代码中调用Java代码的集中典型情况:
+ 需要在C/C++编写的本地应用程序中访问用Java语言编写的代码或代码库
+ 希望在C/C++编写的本地应用程序中使用标准的Java库
+ 当需要把自己已有的C/C++程序与Java程序组织链接在一起时,使用Invocation API可以将它们组织成一个完整的程序
** Invocaton API 应用示例
实例程序由InvokeJava.cpp与InvocationTest.java两个文件构成
示例程序将按如下顺序执行:
(1) 主程序InvokeJava.cpp使用Invocation API加载Java虚拟机。
(2) 通过JNI函数加载InvocationTest类至内存中
(3) 执行被加载的InvocatonTest类main()方法
*** 分析Java代码 InvocationApiTest.java
#+BEGIN_SRC java
public class InvocationAPiTest
{
public static void main(String[] args)
{
System.out.println(args[0]);
}
}
#+END_SRC
=仅含有一个main()方法,该main()方法是一个静态方法,带有一个字符串对象数组,在方法体中仅有一条输出语句,用来降低一个数组元素args[0]中的字符串输出到控制台上。=
*** 分析C代码 invocationApi.c
#+BEGIN_SRC c
#include <jni.h>
int main()
{
JNIEnv *env;
JavaVM *vm;
JavaVMInitArgs vm_args;
JavaVMOptions options[1];
jint res;
jclass cls;
jmethodID mid;
jstring jstr;
jclass stringClass;
jobjectArray args;
// 1. 生成Java虚拟机选项
options[0].optionString = "-Djava.class.path=."
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
// 2. 生成Java虚拟机
res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
// 3. 查找并加载类
cls = (*env)->FindClass(env, "InvocationApiTest");
// 4. 获取main()方法的ID
mid = (*env)->GetStaticMethodID(env, cls, "main", ([Ljava/lang/String;)V);
// 5. 生成字符串对象,用作main()方法的参数
jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");
stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
// 6. 调用main()方法
(*env)->CallStaticVoidMethod(env, cls, mid, args);
// 7. 销毁Java虚拟机
(*vm)->DestroyJavaVM(vm);
}
#+END_SRC
下面开始分析代码的主要部分
#include命令用来将jni.h头文件包含到本文件中。jni.h头文件包含C代码使用JNI必须的各种变量类型或JNI函数的定义,在本地代码中使用JNI时,必须将此头文件包含到本地代码中。
#+BEGIN_SRC java
// 1. 生成Java虚拟机选项
options[0].optionString = "-Djava.class.path=."
vm_args.version = 0x00010002;
vm_args.options = options;
vm_args.nOptions = 1;
vm_args.ignoreUnrecognized = JNI_TRUE;
#+END_SRC
生成一些参数或选项值,这些值在加载Java虚拟机时被引用,用来设置Java虚拟机的运行环境或控制Java虚拟机的运行,如设置CLASSPATH或输出调试信息等。
在生成Java虚拟机选项时,使用JavaVMInitArgs与JavaVMOption结构体,它们定义在jni.h头文件中
#+BEGIN_SRC c
typedef struct JavaVMInitArgs {
jint version;
jint nOptions;
JavaVMOption *options;
jboolean ignoreUnrecognized;
} JavaVMInitArgs;
typedef struct JavaVMOption {
char *optionString;
void *extraInfo;
} JavaVMOption;
#+END_SRC
观察JavaVMInitArgs结构体定义代码,可以发现JavaVMInitArgs结构体内包含JavaVMOption结构体的指针。JavaVMOption结构体包含Java虚拟机的各个参数,JavaVMInitArgs结构体用来将这些参数选项传递给Java虚拟机。
接下来,看一下结构体中各个成员的含义。
JavaVMInitArgs结构体的versino成员用来指定传递诶虚拟机的选项的变量的形式,设定在jni.h头文件中定义的 =JNI_Version_1_2= 的值。nOptions与options用来指定JavaVMInitArgs所指的JavaVMOption结构体数组值。nOptions指定JavaVMOption结构体数组元素的个数,options用来指向JavaVMOption结构体的地址。示例中只设置了一个Java虚拟机选项,即JavaVMOption结构体数组仅有一个元素,声明如下
#+BEGIN_SRC c
JavaVMOption options[1];
#+END_SRC
为了指定以上JavaVMOption结构体数组,需要指定JavaVMInitArgs的options与nOptions
#+BEGIN_SRC c
vm_args.options = options; // JavaVMOption 结构体的地址
vm_args.nOptions = 1; // JavaVMOption 结构体数组元素个数
#+END_SRC
ignoreUnrecognized是JavaVMInitArgs结构体jboolean类型的成员,当Java虚拟机独到设置错误的选项值时,该成员用来决定Java虚拟机是忽略错误后继续执行,还是返回错误后终止执行。若ignoreUnrecognized被设置为 =JNI_TRUE= ,Java虚拟机遇到错误选项时,忽略错误后继续执行;若被设置为 =JNI_FALSE= ,当遇到错误选项,Java虚拟机将错误返回后终止执行。
接下来分析JavaVMOption结构体,它用来指定Java虚拟机的选项值。若想创建选项值,只要向结构体的optionString成员指定一个字符串,用作Java虚拟机选项的形式。比如示例中的"-Djava.class.path=.",用来设置标准选项,即将Java虚拟机要加载的类的默认目录设置为当前目录(.),其形式为-Dproperty=value。
#+BEGIN_SRC c
res = JNI_CreateJavaVM(&vm, (void**)&env, &vm_args);
#+END_SRC
本行代码是整个程序的核心部分,即C应用程序调用 =JNI_CreateJavaVM()= 函数,生成并装载Java虚拟机。 =JNI_CreateJavaVM()= 函数的第一个参数类型为JavaVM,它表示Java虚拟机接口,用来生成或销毁Java虚拟机。DestroyJavaVM()是接口函数之一,该函数用来销毁Java虚拟机。
在 =JNI_CreateJavaVM()= 函数的第二个参数env中,保存着JNI接口的指针的地址。通过env所指的JNI接口指针,可以使用各种JNI函数,即在C/C++中,通过env,可以生成Java对象,调用相应方法等。
| JNI Invocation API- =JNI_CreateJavaVM= | |
|-------------------------------------+-----------------------------------------------------------------|
| 形式 | jint =JNI_CreateJavaVM= (javaVM **vm, JNIEnv **env, void *vm_args) |
|-------------------------------------+-----------------------------------------------------------------|
| 说明 | 装载并初始化Java虚拟机 |
|-------------------------------------+-----------------------------------------------------------------|
| 参数 | vm: JavaVM指针的地址 |
| | env: JNI接口指针的地址 |
| | =vm_args:= 传递给Java虚拟机的参数 |
|-------------------------------------+-----------------------------------------------------------------|
| 返回值 | 成功,返回0;失败,返回负值 |
为了加载InvocationTest类和执行方法(向main方法传递字符串参数"Hello"),首先调用FindClass()函数,装载InvocationApiTest类。而后调用GetStaticMethodID()函数,获取main()方法的ID,准备调用main()方法。
在使用CallStaticVoidMethod()函数调用main()方法之前,首先构造出传递给main()方法的参数。Java的main()方法的参数是String[]数组
#+BEGIN_SRC java
public static void main(String[] args)
#+END_SRC
示例中将"Hello Invocation API!!"字符串传递给main()方法。首先调用NewStringUTF()函数,将UTF-8形式的字符串,转换成Java字符串对象String。然后调用NewObjectArray()函数,创建String对象数组,使用创建的String对象将其初始化。先创建一个含有一个元素的String[]数组,而后将"Hello Invocation API!!"字符串赋值给数组的第一个元素。
#+BEGIN_SRC c
jstr = (*env)->NewStringUTF(env, "Hello Invacation API!!");
stringClass = (*env)->NewObjectArray(env, 1, stringClass, jstr);
args = (*env)->NewObjectArray(env, 1, stringClass, jstr);
#+END_SRC
调用JNI本地函数处理String对象的方法有些复杂。如果你对此仍迷惑不解,我们不妨将这部分代码转换成与其等价的Java代码。
如下所示,首先创建包含一个元素的字符串数组,而后将"Hello Invocation API!!"字符串赋值给数组的首个元素
#+BEGIN_SRC java
String[] args = new String[1];
args[0] = "Hello Invocation API!"
#+END_SRC
| JNI函数 NeStringUTF | |
|---------------------+------------------------------------------------------|
| 形式 | jstring NewStringUTF(JNIEnv *env, const char *bytes) |
|---------------------+------------------------------------------------------|
| 说明 | 将UTF-8形式的C字符串转换成java.lang.String对象 |
|---------------------+------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | bytes: 待生成String对象的C字符串的地址 |
|---------------------+------------------------------------------------------|
| 返回值 | 成功,返回String对象的jstring类型的引用;失败,返回NULL |
| JNI函数 NewObjectArray | |
|------------------------+----------------------------------------------------------------------------------------------|
| 形式 | jarray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initalElement) |
|------------------------+----------------------------------------------------------------------------------------------|
| 说明 | 生成由elementClass对象组成的数组。数组元素个数由length指定,initalElement参数用来初始化对象数组 |
|------------------------+----------------------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | length: 数组元素个数 |
| | elementClass:数组元素对象的类型 |
| | initialElement: 数组初始化值 |
|------------------------+----------------------------------------------------------------------------------------------|
| 返回值 | 若成功,则返回数组引用;失败,则返回NULL |
#+BEGIN_SRC c
(*env)->CallStaticVoidMethod(env, cls, mid, args);
#+END_SRC
=本行代码通过CallStaticVoidMethod()函数调用InvocationApiTest类的main()方法。在上面创建的Stringp[]数组是CallStaticVoidMethod()函数的第四个参数,该参数会被传递给InvocationApiTest类的main()方法。当InvocationApiTest类的main()方法被调用执行时,它会向控制台输出args字符串数组的 args[0]元素中的字符串。=
* 直接注册JNI本地函数
Java虚拟机在运行包含本地方法的Java应用程序时,要经过以下两个步骤。
1. 调用System.loadLibrary()方法,将包含本地方法具体实现的C/C++运行库加载到内存中。
2. Java虚拟机检索加载进来的库函数符号,在其中查找与Java本地方法拥有相同签名的JNI本地函数符号。若找到一致的,则将本地方法映射到具体的JNI本地函数。
在Android Framework这类复杂的系统下,拥有大量的包含本地方法的java类,Java虚拟机加载相应的运行库,再逐一检索,将各个本地方法与相应的函数映射起来,这显然会增加运行时间,降低运行的效率。
为此,JNI机制提供了名称为RegisterNatives()的JNI函数,该函数允许C/C++开发者将JNI本地函数与Java类的本地方法直接映射在一起。当不调用RegisterNative()函数时,Java虚拟机会自动检索并将JNI本地函数与相应的Java本地方法链接在一起。但当开发者直接调用RegisterNatives()函数进行映射时,Java虚拟机就不必进行映射处理,这会极大提高运行速度,提升运行效率。
由于程序员直接将JNI本地函数与Java本地方法链接在一起,在加载运行库时,Java虚拟机不必为了识别JNI本地函数而将JNI本地函数的名称与JNI支持的命名规则进行对比,即任何名称的函数都能直接链接到Java本地方法上。
** 加载本地库时,注册JNI本地函数
#+BEGIN_SRC java
#include "jni.h"
#include <stdio.h>
// JNI本地函数原型
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);
JNIEXPORT jint JNICALL JNI_Onoad(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;
}
// 实现JNI本地函数
void printHelloNative(JNIEnv *env, jobject obj)
{
printf("Hello World!\n");
return;
}
void printStringNative(JNIEnv *env, jobject obj, jstring string)
{
const char *str = env->GetStringUTFChars(string, 0);
printf("%s!\n, str);
return;
}
#+END_SRC
#+BEGIN_SRC java
void printHelloNative(JNIEnv *env, jobject obj);
void printStringNative(JNIEnv *env, jobject obj, jstring string);
#+END_SRC
此两行代码用来声明JNI本地函数原型。如前所述,在使用RegisterNatives()函数机型映射时,不需要将JNI本地函数原型与JNI命名规则进行比对,所以使用的函数名比较简单。但函数中的两个公共参数必须指定为"JNIEnv *env, jobject obj"。
#+BEGIN_SRC java
if(vm->GetEnv((void**)&env, JNI_VERSION_1_4) != JNI_OK) {
printf("Error");
return JNI_ERR;
#+END_SRC
在 =JNI_OnLoad()= 函数中首先判断JNI的版本,即调用GetEnv()函数,判断Java虚拟机是否支持JNI1.4。若java虚拟机支持JNI1.4, =JNI_OnLoad()= 函数就会返回 =JNI_VERSION_1_4= ;若不支持, =JNI_OnLoad()= 函数就会返回 =JNI_ERR= ,并终止装载库的行为。
当GetEnv()函数调用完毕后,JNI接口指针被保存到env变量中,在调用FindClass()、RegisterNatives()等JNI函数时,可以使用该变量。
| JNI Invocation API - GetEnv | |
|-----------------------------+---------------------------------------------------|
| 形式 | jint GetEnv(JavaVM *vm, void **env, jint version) |
|-----------------------------+---------------------------------------------------|
| 说明 | 判断Java虚拟机是否支持version指定的JNI版本,而后将JNI接口指针设置到*env中 |
|-----------------------------+---------------------------------------------------|
| 参数 | vm: JavaVM接口指针的地址 |
| | env: JNI接口指针地址 |
| | version: JNI版本 |
|-----------------------------+---------------------------------------------------|
| 返回值 | 若执行成功,返回0;失败,返回负值 |
#+BEGIN_SRC java
cls = env->FindClass("HelloJNI");
#+END_SRC
为了把声明的JNI本地函数与JNI本地函数映射在一起,本行先调用FindClass()函数加载HelloJNI类,并将类引用保存到jclass变量cls中。
#+BEGIN_SRC java
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
#+END_SRC
该部分代码用来将Java类的本地方法与JNI本地函数映射在一起。首先使用JNINativeMethod结构体数组,将待映射的本地方法与JNI本地函数的相关信息保存在数组中,而后调用RegisterNatives()函数进行映射。JNINativeMethod结构体定义如下
#+BEGIN_SRC c
typedef struct {
char *name; // 本地方法名称
char *signature; // 本地方法签名
void *fnPtr; // 与本地方法相对应的JNI本地函数指针
} JNINativeMethod
#+END_SRC
如代码所示,nm是JNINativeMethod结构体数组,它保存着printHello()、printString()与printHelloNative()、printStringNative()函数的链接信息。
保存好映射信息后,将它们传递给RegisterNatives()函数,最后由RegisterNatives()函数完成映射。
| JNI函数 RegisterNatives | |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 形式 | jarray RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methdos, jint nMethods) |
|-------------------------+--------------------------------------------------------------------------------------------------|
| 说明 | 将clazz指定类中的本地方法与JNI本地函数链接在一起,链接信息保存在JNINativeMethod结构体数组中 |
|-------------------------+------------------------------------------------------------------------------------------------------------------------------------|
| 参数 | env: JNI接口指针 |
| | clazz: Java类 |
| | methods: 包含本地方法与JNI本地函数的链接信息 |
| | nMethods: methods数组元素的个数 |
|-------------------------+---------------------------------------------------------------------------------------------------------|
| 返回值 | 若执行成功,返回数组引用;否则返回NULL |
=总结一下,本节中通过JNI_OnLoad()函数将Java本地方法与JNI本地函数映射起来。=
** Android中的应用举例
* 使用Android NDK开发
Andoird NDK ( Native Development Kit )
+ 包含将C/C++源代码编译成本地库的工具(编译器、连接器等)
+ 提供将编译好的本地库插入Android包文件(.apk)中的功能
+ 在生成本地库时,Android平台可支持的系统头文件与库
+ NDK开发相关的文档、示例、规范
** 安装Androdi NDK
网站: http://developer.android.com/sdk/ndk/index.html
** 使用Android NDK 开发步骤
=设置好NDK环境变量后,在<NDK_HOME>/apps目录下,会看到一些NDK使用示例程序=
+ hello-jni: 调用本地库,接收"Hello from JNI"字符串,并通过TextView将其输出
+ two-libs: 调用本地库,返回两数之和,并通过TextView输出
+ san-angeles: 调用本地OpenGL ES API, 渲染3D图片
+ hello-gl2: 调用OpenGL ES 2.0, 渲染三角形
+ bitmap-plasma: 一个使用本地代码访问Android Bitmap对象的像素缓存区的示例程序
** hello-jni
内容:
AndroidManifest.xml default.properties /jni /res /src /tests
ndk-build后:
AndroidManifest.xml default.properties /jni /libs /obj /res /src /tests
#+BEGIN_SRC sh
hello-jni$ tree
.
├── AndroidManifest.xml
├── default.properties
├── jni
│ ├── Android.mk
│ └── hello-jni.c
├── libs
│ └── armeabi
│ ├── gdbserver
│ ├── gdb.setup
│ └── libhello-jni.so
├── obj
│ └── local
│ └── armeabi
│ ├── libhello-jni.so
│ └── objs-debug
│ └── hello-jni
│ ├── hello-jni.o
│ └── hello-jni.o.d
├── res
│ └── values
│ └── strings.xml
├── src
│ └── com
│ └── example
│ └── hellojni
│ └── HelloJni.java
└── tests
├── AndroidManifest.xml
├── default.properties
└── src
└── com
└── example
└── hellojni
└── HelloJniTest.java
19 directories, 15 files
#+END_SRC
关键的文件:
java层: HelloJni.java HelloJniTest.java 资源文件:strings.xml
jni层: hello-jni.c Android.mk
下面主要需要分析 HelloJni.java, hello-jni.c, Android.mk 三个文件
HelloJni.java
#+BEGIN_SRC java
package com.example.hellojni;
import android.app.Activity;
import android.widget.TextView;
import android.os.Bundle;
public class HelloJni extends Activity
{
/** onCreate函数在activity第一次被创建的时候调用 */
@Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
/* 创建一个TextView并且设置它的内容.
* 文本的内容是通过本地方法获取的
*/
TextView tv = new TextView(this);
tv.setText( stringFromJNI() ); // 这里调用了本地方法
setContentView(tv);
}
/* 本地方法通过本地库hello-jni实现
* 本地库与这个应用程序已经打包在了一起
*/
public native String stringFromJNI();
/* 下面是另外一个方法的声明,这个方法没有通过hello-jni实现
* 这是为了说明,你可以声明任意的本地方法,在java代码中
* 它们的实现会在装载的本地库里寻找,当你首次调用它们的时候
* 尝试调用这个方法会引发java.lang.UnsatisfiedLinkError exception!
*/
public native String unimplementedStringFromJNI();
/* 下面的代码用来在应用程序开始的时候装载hello-jni库
* 这个库在安装的时候由包管理器已经安装好了
*/
static {
System.loadLibrary("hello-jni");
}
}
#+END_SRC
hello-jni.c
#+BEGIN_SRC c
#include <string.h>
#include <jni.h>
jstring java_com_example_hellojni_HelloJni_stringFromJNI(JNIEnv *env, jobject thiz)
{
return (*env)->NewStringUTF(env, "Hello from JNI !");
}
#+END_SRC
Android.mk
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := hello-jni
LOCALSRC_FILES := hello-jni.c
include $(BUILD_SHARED_LIBRARY)
#+END_SRC
一个Android.mk文件首先必须定义好 =LOCAL_PATH= 变量,用于在开发树中查找源文件。 =LOCAL_PATH= 变量在Android.mk文件的最开始被定义,若无特殊情况,一般采用如下编写形式:
#+BEGIN_SRC sh
LOCAL_PATH := $(call my-dir)
#+END_SRC
$(call my-dir)用来保存my-dir宏函数的返回值。my-dir是一个宏函数,由编译系统提供,用于返回包含Android.mk文件的目录,即将Android.mk文件所在的目录设置为基本目录。
一般来说,本地库的源代码与Android.mk文件在同一目录下,即在 =<PROJECT_HOME>/jni= 目录下。若将$(call my-dir)返回值保存到 =LOCAL_PATH= 变量中,即可准确指定NDK编译的基本文件目录。
=include $(CLEAR_VARS)= 用来初始化Android.mk文件中" =LOCAL_XXX= "即以 =LOCAL_= 开头的变量,如 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等变量,但在一开始的 =LOCAL_PATH= 变量除外。由于Android编译系统将会 =LOCAL_XXX= 变量用作全局变量,所以需要使用该命令初始化这些变量。
=LOCAL_MODULE= 变量必须被定义,以标识在Android.mk文件中描述的每个模块,即要生成的库的名称。该名称必须唯一,且不含空格,编译系统会自动产生合适的前缀和后缀,比如设置 =LOCAL_MODULE= 变量为ndk-exam, 编译后缀会生成名为libndk-exam.so的共享库。
=LOCAL_SRC_FILES= 变量必须包含将要编译打包进模块中的各个源文件。这些源文件所在目录即是 =LOCAL_PATH= 变量指定的目录,即 =<PROJECT_HOME>/jni= 目录。
=include $(BUILD_SHARED_LIBRARY)= 使用 =LOCAL_MODULE= 、 =LOCAL_SRC_FILES= 等变量值,创建名称为 =lib$(LOCAL_MODULE).so= 的共享库。