用JNI实现与原生代码通信
1. JNI概述
JNI是Java程序设计语言功能最强的特征,允许Java类的一些方法原生实现,同时让他们能够像普通Java方法一样被调用和使用.
原生方法也可以使用Java对象, 使用方法和Java本身一样.
原生方法可以创建新的Java对象或者使用Java应用程序创建的对象, 这些Java应用程序可以检查,修改和调用这些对象的方法执行相应的任务.
2. JNI示例
1. 原生方法的声明
原生方法使用 native 关键字描述, native 会通知编译器,该方法使用别的语言提供的方法具体实现.
原生方法没有方法体, 方法以 ; 结束.
public native <ReturnType> <NativeMethodName> (<ParamsList>);
public native String jniSayHello (String hello);
2. 加载共享库
原生方法会被编译成一个 共享库.
Java应用需要加载共享库以便虚拟机能够找到原生方法的实现.
java.lang.System 类提供两个静态的方法 load(), loadLibrary(), 用于在运行时加载共享库.
static {
System.loadLibrary("module");
}
module和Android.mk片段中的 LOCAL_MODULE 构建系统变量定义的模块名称相同.
loadLibrary的参数不包含共享库的位置,Java库路径即系统属性 java.library.path 保存loadLibrary方法在共享库搜索的目录列表
Android系统中的Java库路径有: /vendor/lib 和 /system/lib
loadLibrary 在扫描Java库路径的时候, 一旦发现同名的库, 立即加载共享库. 因为Java库路径的第一组目录就是Android系统目录, 为了避免与系统库命名冲突, 强烈建议Android开发者为每一个共享库选择唯一的名称.
3. 实现原生方法
原生方法jniSayHello 使用 Java_com_rygz_jni_HelloJNI_jniSayHello 的完全限定的函数声明,这种显示的函数命名方虚拟机在加载的共享库中自动查找原生函数.
1. C/C++头文件生成器: javah命令
javah 工具可以为原生方法解析Java类文件并生成由原生方法声明组成的头文件.
- 用javac命令将.java源文件编译成.class字节码文件
javac src/com/rygz/jni/HelloJNI.java -d ./bin
//-d 表示将编译后的class文件放到指定的目录下,这里我把它放到和src同级的bin目录下
- 用javah -jni命令,根据class字节码文件生成.h头文件(-jni参数是可选的)
javah -classpath bin/classes com.rygz.jni.HelloJNI
javah -jni -classpath ./bin -d ./jni com.rygz.jni.HelloJNI
//默认生成的.h头文件名为:com_rygz_jni_HelloJNI.h(包名+类名.h),也可以通过-o参数指定生成头文件名称:
javah -jni -classpath ./bin -o HelloJNI.h com_rygz_jni.HelloJNI
参数说明:
-classpath :类搜索路径,这里表示从当前的bin目录下查找
-d :将生成的头文件放到当前的jni目录下
-o : 指定生成的头文件名称,默认以类全路径名生成(包名+类名.h)
注意:-d和-o只能使用其中一个参数。
- 用本地代码实现.h头文件中的函数
#include <jni.h>
/* Header for class com_rygz_jni_HelloJNI */
#ifndef _Included_com_rygz_jni_HelloJNII
#define _Included_com_rygz_jni_HelloJNII
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_rygz_jni_HelloJNI_jniSayHello(JNIEnv *, jclass, jstring);
#ifdef __cplusplus
}
#endif
#endif
// HelloJNI.c
#include "com_rygz_jni_HelloJNI.h"
#ifdef __cplusplus
extern "C"{
#endif
JNIEXPORT jstring JNICALL Java_com_rygz_jni_HelloJNI_jniSayHello( JNIEnv *env, jclass cls, jstring jhello){
const char *charString = NULL;
char buff[128] = { 0 };
charString = (*env)->GetStringUTFChars(env, jhello, NULL);
if (charString == NULL){
printf("hello is null.\n");
return NULL;
}
printf("Java String:%s\n", charString);
sprintf(buff, "hello %s", charString);
(*env)->ReleaseStringUTFChars(env, jhello, charString);
return (*env)->NewStringUTF(env, buff);
}
#ifdef __cplusplus
}
#endif
- 将C/C++代码编译成本地动态库文件
动态库文件名命名规则:lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:
Mac OS X : libHelloJNI.jnilib
Windows :HelloJNI.dll(不需要lib前缀)
Linux/Unix:libHelloJNI.so
- 在Eclipse IDE中
(省略)
2. 方法声明
由上知, Java 中jniSayHello是无参数的, 但原生的方法带有两个参数.
JNIEXPORT jstring JNICALL Java_com_rygz_jni_jniSayHello(JNIEnv *, jobject, jstring);
// 第一个参数JNIEnv 是指向可用JNI函数表的接口指针;
// 第二个参数jobject 是HelloJNI类示例的Java对象引用;
函数的命名规则:
用javah工具生成函数原型的头文件,函数命名规则为:Java_类全路径方法名。如Java_com_rygz_jni_HelloJNI_jniSayHello,其中Java是函数的前缀,com_rygz_jni_HelloJNI是类名,jniSayHello是方法名,它们之间用 _(下划线) 连接。
函数参数:
JNIEXPORT jstring JNICALL Java_com_rygz_jni_HelloJNI_jniSayHello(JNIEnv *, jclass, jstring);
第一个参数:JNIEnv*是定义任意native函数的第一个参数(包括调用JNI的RegisterNatives函数注册的函数),指向JVM函数表的指针,函数表中的每一个入口指向一个JNI函数,每个函数用于访问JVM中特定的数据结构。
第二个参数:调用java中native方法的实例或Class对象,如果这个native方法是实例方法,则该参数是jobject,如果是静态方法,则是jclass
第三个参数:Java对应JNI中的数据类型,Java中String类型对应JNI的jstring类型。(后面会详细介绍JAVA与JNI数据类型的映射关系)
函数返回值类型:夹在JNIEXPORT和JNICALL宏中间的jstring,表示函数的返回值类型,对应Java的String类型
- JNIEnv 接口指针
原生代码通过JNIEnv 接口指针提供的各种函数来使用虚拟机的功能.
JNIEnv 是一个指向线程-局部数据的指针, 而线程-局部数据中包含指向函数表的指针
实现原生方法的函数将JNIEnv 接口指针作为它们的第一个参数.
传递给每一个原生方法调用的JNIEvn 接口指针在与方法调用相关的线程中也有效, 但它不能被缓存以及被其他线程使用.
原生代码中 C 和C++ 调用JNI 函数的语法不同:
A. C代码中, JNIEnv 是指向JNINativeInterface结构的指针, 为了访问任何一个JNI函数, 该指针需要首先被解引用.因为C代码中JNI函数不了解当前的JNI环境, JNIEnv 实例应该作为第一个参数传递给每一个JNI函数的调用者.
return (* env)->NewStringUTF(env, "Hello JNI!");
B. C++ 代码中JNIEnv 实际上是C++类的实例, JNI 函数以成员函数的形式存在, 因为JNI方法已经访问了当前的JNI环境, 因此JNI方法调用不要求JNIEnv 实例作为参数.
return env->NewStringUTF("Hello jni!")