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= 的共享库。