JNI入门学习
------jiangguohu
1,JNI简介
JNI是Java Native Interface的缩写,中文为“JAVA本地调用”。从Java1.1开始,Java Native Interface(JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。
2,JAVA层处理
Java程序员在java层使用JNI,只需要完成两项工作:
• 加入对应的JNI库(System.loadLibrary(“JNI库的名字”);一般放在static中);
• 声明由关键字native修饰的函数;
3,JNI注册
在java层用native修饰的函数系统是怎么判定它与JNI层中的某个函数相对应的呢?这就需要注册JNI函数了。注册JNI函数的方法有两种:
• 静态方法
这种方法是根据java层的函数名来找对应的JNI函数。它需要java的工具程序javah的参与。整体流程如下:
• 先编写java代码,然后编译生成.class文件
• 使用java工具程序javah,如javah -o output packagename.classname,这样就会生成一个output.h的JNI层头文件。 packagename.classname是java代码编译后的class文件。在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
这个头文件一般会使用packagename_class.h的样式,比如android_media_MediaScanner.h等。
这个方法有几个弊端:
• 需要编译所有含有native函数的java类,每个所生成的class文件都得用javah生成一个头文件。
• 初次调用native函数时,要根据函数名字搜索对应的JNI层函数来建立关联关系(会建立一个函数指针,之后再调用的时候直接使用函数指针),这样会影响运行效率。
• 动态方法
用JNINativeMethod结构来记录native函数于JNI函数一一对应的关系。例如在android_media_MediaScanner.cpp中的static JNINativeMethod gMethods[]数组,数组中的每一个成员均含有三条属性值,分别表示“Java中native函数的函数名”,“函数的签名信息”,“JNI层对应的函数指针”。再然后写一个注册该数组的方法,在方法中调用并返回AndroidRunTime类的registerNativeMethods函数来注册该 JNINativeMethod数组。
当java层通过System.loadLibrary加载完JNI动态库之后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有就调用它,从而完成动态注册。所以当我们使用动态注册方法的时候,就必须实现JNI_OnLoad函数,在这个函数中调用上一段讲到的注册JNINativeMethod数组的方法,从而动态注册了相对应的JNI函数。
(这部分参考代码来理解更加容易)
4,Java与Native之间的数据类型转换
与java一样native中的数据类型也分为基本数据类型和引用数据类型。比较容易记住的是java中的boolean对应native中的jboolean(只是在前面加了个j),其他的依此类推。但是需要注意的是它所占用的字节情况有所变化。这里不一一介绍了。
例如:
在MediaScanner.java中:
private native void processFile(String path, String mimeType, MediaScannerClient client);
在其对应的JNI层函数:
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz,
jstring path, jstring mimeType, jobject client)
从上可以看到,java中面向对象类型MediaScannerClient 在JNI层对应 jobject。还需要注意的是:在java层中只有三个参数,到了JNI层后却变成了五个。 JNIEnv *env接下来一节会介绍。 jobject thiz代表了java层的MediaScanner对象,它表示在哪个MediaScanner对象上调用 processFile。如果java层是static函数,那么这个参数将是jclass,表示在调用哪个java class的静态函数。
5,JNIEnv介绍
JNIEnv是一个与线程相关的,代表JNI环境的结构体。简单来说,它实际上就是提供了一些JNI系统函数。通过这些函数可以调用java函数,操作jobject对象等很多事情。
与线程相关,也就是说线程A中有一个JNIEnv,线程B中也有一个JNIEnv,不能在线程B中使用线程A里面的JNIEnv。
JavaVM是虚拟机在JNI层的代表,全进程只有一个JavaVM对象,所以可以保存,并且可以在任何地方使用。调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体,也就可以调用java层中的函数,属性等了。此外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应资源。
通过JNIEnv获得jfieldID(java类成员变量)以及jmethodID(java类成员函数):
android_media_MediaScanner.cpp中MyMediaScannerClient构造函数为例:
// 先找到android/media/MediaScannerClient类在JNI层中对应的 jclass实例
jclass mediaScannerClientInterface =
env->FindClass(“android/media/MediaScannerClient”);
// 取出 MediaScannerClient类中函数 scanFile的jmethodID
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
// 取出 MediaScannerClient类中函数 handleStringTag 的jmethodID
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
在得到jmethodID之后 CallVoidMethod调用java对象的scanFile 函数:
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
…...
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia);
}
通过上述类似于 CallVoidMethod的 CallIntMethod, CallStringMethod等方法来调用java对象对应的函数。如果想调用java中的static函数,则需要CallStatic<Type>Method系列函数。对应的,如果需要调用java对象对应的属性值,则需要调用Get<type>Field系列方法,也可以通过Set<type>Field系列方法来设置对应的java对象的属性值。
6,JNI类型签名
由于java支持函数重载,也就是说可以定义同名但是不同参数的函数,这就造成了在JNI中仅仅只是根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术中就将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能够顺利的找到我们想要的java中的函数了。
例如:
在MediaScanner.java中:
private native void processFile(String path, String mimeType, MediaScannerClient client);
它在JNI中的签名:
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
从中可以看出,括号内的是参数类型标识,括号外最右边的是返回值类型标识。当参数类型是引用类型时,其格式是“L包名;”,其中包名中的“.”换成“/”。Ljava/lang/String;表示的是一个java String类型。具体的可拜读JNI签名规则表。
此外java提供了一个叫javap的工具,帮组我们生成函数或者变量的签名信息,用法如下:
javap -s -p xxx
其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,默认只会打印public成员和函数的签名信息。
7,HelloWord
在eclipse中新建项目TestMyJNI,MainActivity.java,TestMyJNI.java
接下来就需要注册JNI。
• 首先将TestMyJNI.java复制到项目的bin文件目录下,用终端进入到bin目录。在终端输入javac TestMyJNI.java,得到TestMyJNI.class文件。
• 在bin下新建目录com/example/testmyjni,然后将编译好的TestMyJNI.class文件复制到该目录下。再在终端中javah -jni com.example.testmyjni.TestJNI,在bin目录下就生成了com_example_testmyjni_TestMyJNI.h文件。
这也就完成了JNI的注册。再接下来,需要编写com_example_testmyjni_TestMyJNI.cpp和Android.mk文件。
com_example_testmyjni_TestMyJNI.cpp:
Android.mk:
在编写好之后,在终端中进入该项目根目录,$NDK/ndk-build (这需要实现安装好NDK),生成libTestMyJNI.so文件。这样之后,就可以在eclipse中编译该项目了。
8,ubuntu下NDK安装
• 从官网下载NDK http://developer.android.com/tools/sdk/ndk/index.html#Reqs ,并解压文件。(在此目录为/home/jiangguohu/android-ndk-r10b)
• 配置NDK的环境变量
执行:gedit ~/.bashrc,在打开的文件末尾添加如下内容:
NDK=/home/jiangguohu/android-ndk-r10b
export NDK
• 在当前 bash 环境下读取并执行 ~/.bashrc 中的命令:
source ~/.bashrc
或 (source命令也称为“点命令”,也就是一个点符号(.)。)
. ~/.bashrc
• 然后查看是否生效:
执行:echo $NDK
结果:/home/jiangguohu/android-ndk-r10b
• 在目标工程目录下编译:
cd进入项目文件根目录,然后在终端输入:
$NDK/ndk-build
------jiangguohu
1,JNI简介
JNI是Java Native Interface的缩写,中文为“JAVA本地调用”。从Java1.1开始,Java Native Interface(JNI)标准成为java平台的一部分,它允许Java代码和其他语言写的代码进行交互。JNI一开始是为了本地已编译语言,尤其是C和C++而设计的,但是它并不妨碍你使用其他语言,只要调用约定受支持就可以了。
2,JAVA层处理
Java程序员在java层使用JNI,只需要完成两项工作:
• 加入对应的JNI库(System.loadLibrary(“JNI库的名字”);一般放在static中);
• 声明由关键字native修饰的函数;
3,JNI注册
在java层用native修饰的函数系统是怎么判定它与JNI层中的某个函数相对应的呢?这就需要注册JNI函数了。注册JNI函数的方法有两种:
• 静态方法
这种方法是根据java层的函数名来找对应的JNI函数。它需要java的工具程序javah的参与。整体流程如下:
• 先编写java代码,然后编译生成.class文件
• 使用java工具程序javah,如javah -o output packagename.classname,这样就会生成一个output.h的JNI层头文件。 packagename.classname是java代码编译后的class文件。在生成的output.h文件里,声明了对应的JNI层函数,只要实现里面的函数即可。
这个头文件一般会使用packagename_class.h的样式,比如android_media_MediaScanner.h等。
这个方法有几个弊端:
• 需要编译所有含有native函数的java类,每个所生成的class文件都得用javah生成一个头文件。
• 初次调用native函数时,要根据函数名字搜索对应的JNI层函数来建立关联关系(会建立一个函数指针,之后再调用的时候直接使用函数指针),这样会影响运行效率。
• 动态方法
用JNINativeMethod结构来记录native函数于JNI函数一一对应的关系。例如在android_media_MediaScanner.cpp中的static JNINativeMethod gMethods[]数组,数组中的每一个成员均含有三条属性值,分别表示“Java中native函数的函数名”,“函数的签名信息”,“JNI层对应的函数指针”。再然后写一个注册该数组的方法,在方法中调用并返回AndroidRunTime类的registerNativeMethods函数来注册该 JNINativeMethod数组。
当java层通过System.loadLibrary加载完JNI动态库之后,紧接着会查找该库中一个叫JNI_OnLoad的函数。如果有就调用它,从而完成动态注册。所以当我们使用动态注册方法的时候,就必须实现JNI_OnLoad函数,在这个函数中调用上一段讲到的注册JNINativeMethod数组的方法,从而动态注册了相对应的JNI函数。
(这部分参考代码来理解更加容易)
4,Java与Native之间的数据类型转换
与java一样native中的数据类型也分为基本数据类型和引用数据类型。比较容易记住的是java中的boolean对应native中的jboolean(只是在前面加了个j),其他的依此类推。但是需要注意的是它所占用的字节情况有所变化。这里不一一介绍了。
例如:
在MediaScanner.java中:
private native void processFile(String path, String mimeType, MediaScannerClient client);
在其对应的JNI层函数:
static void android_media_MediaScanner_processFile(JNIEnv *env, jobject thiz,
jstring path, jstring mimeType, jobject client)
从上可以看到,java中面向对象类型MediaScannerClient 在JNI层对应 jobject。还需要注意的是:在java层中只有三个参数,到了JNI层后却变成了五个。 JNIEnv *env接下来一节会介绍。 jobject thiz代表了java层的MediaScanner对象,它表示在哪个MediaScanner对象上调用 processFile。如果java层是static函数,那么这个参数将是jclass,表示在调用哪个java class的静态函数。
5,JNIEnv介绍
JNIEnv是一个与线程相关的,代表JNI环境的结构体。简单来说,它实际上就是提供了一些JNI系统函数。通过这些函数可以调用java函数,操作jobject对象等很多事情。
与线程相关,也就是说线程A中有一个JNIEnv,线程B中也有一个JNIEnv,不能在线程B中使用线程A里面的JNIEnv。
JavaVM是虚拟机在JNI层的代表,全进程只有一个JavaVM对象,所以可以保存,并且可以在任何地方使用。调用JavaVM的AttachCurrentThread函数,就可以得到这个线程的JNIEnv结构体,也就可以调用java层中的函数,属性等了。此外,在后台线程退出前,需要调用JavaVM的DetachCurrentThread函数来释放对应资源。
通过JNIEnv获得jfieldID(java类成员变量)以及jmethodID(java类成员函数):
android_media_MediaScanner.cpp中MyMediaScannerClient构造函数为例:
// 先找到android/media/MediaScannerClient类在JNI层中对应的 jclass实例
jclass mediaScannerClientInterface =
env->FindClass(“android/media/MediaScannerClient”);
// 取出 MediaScannerClient类中函数 scanFile的jmethodID
mScanFileMethodID = env->GetMethodID(
mediaScannerClientInterface,
"scanFile",
"(Ljava/lang/String;JJZZ)V");
// 取出 MediaScannerClient类中函数 handleStringTag 的jmethodID
mHandleStringTagMethodID = env->GetMethodID(
mediaScannerClientInterface,
"handleStringTag",
"(Ljava/lang/String;Ljava/lang/String;)V");
在得到jmethodID之后 CallVoidMethod调用java对象的scanFile 函数:
virtual status_t scanFile(const char* path, long long lastModified,
long long fileSize, bool isDirectory, bool noMedia)
{
…...
mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified, fileSize, isDirectory, noMedia);
}
通过上述类似于 CallVoidMethod的 CallIntMethod, CallStringMethod等方法来调用java对象对应的函数。如果想调用java中的static函数,则需要CallStatic<Type>Method系列函数。对应的,如果需要调用java对象对应的属性值,则需要调用Get<type>Field系列方法,也可以通过Set<type>Field系列方法来设置对应的java对象的属性值。
6,JNI类型签名
由于java支持函数重载,也就是说可以定义同名但是不同参数的函数,这就造成了在JNI中仅仅只是根据函数名是没法找到具体函数的。为了解决这个问题,JNI技术中就将参数类型和返回值类型的组合作为一个函数的签名信息,有了签名信息和函数名,就能够顺利的找到我们想要的java中的函数了。
例如:
在MediaScanner.java中:
private native void processFile(String path, String mimeType, MediaScannerClient client);
它在JNI中的签名:
"(Ljava/lang/String;Ljava/lang/String;Landroid/media/MediaScannerClient;)V",
从中可以看出,括号内的是参数类型标识,括号外最右边的是返回值类型标识。当参数类型是引用类型时,其格式是“L包名;”,其中包名中的“.”换成“/”。Ljava/lang/String;表示的是一个java String类型。具体的可拜读JNI签名规则表。
此外java提供了一个叫javap的工具,帮组我们生成函数或者变量的签名信息,用法如下:
javap -s -p xxx
其中xxx为编译后的class文件,s表示输出内部数据类型的签名信息,p表示打印所有函数和成员的签名信息,默认只会打印public成员和函数的签名信息。
7,HelloWord
在eclipse中新建项目TestMyJNI,MainActivity.java,TestMyJNI.java
接下来就需要注册JNI。
• 首先将TestMyJNI.java复制到项目的bin文件目录下,用终端进入到bin目录。在终端输入javac TestMyJNI.java,得到TestMyJNI.class文件。
• 在bin下新建目录com/example/testmyjni,然后将编译好的TestMyJNI.class文件复制到该目录下。再在终端中javah -jni com.example.testmyjni.TestJNI,在bin目录下就生成了com_example_testmyjni_TestMyJNI.h文件。
这也就完成了JNI的注册。再接下来,需要编写com_example_testmyjni_TestMyJNI.cpp和Android.mk文件。
com_example_testmyjni_TestMyJNI.cpp:
Android.mk:
在编写好之后,在终端中进入该项目根目录,$NDK/ndk-build (这需要实现安装好NDK),生成libTestMyJNI.so文件。这样之后,就可以在eclipse中编译该项目了。
8,ubuntu下NDK安装
• 从官网下载NDK http://developer.android.com/tools/sdk/ndk/index.html#Reqs ,并解压文件。(在此目录为/home/jiangguohu/android-ndk-r10b)
• 配置NDK的环境变量
执行:gedit ~/.bashrc,在打开的文件末尾添加如下内容:
NDK=/home/jiangguohu/android-ndk-r10b
export NDK
• 在当前 bash 环境下读取并执行 ~/.bashrc 中的命令:
source ~/.bashrc
或 (source命令也称为“点命令”,也就是一个点符号(.)。)
. ~/.bashrc
• 然后查看是否生效:
执行:echo $NDK
结果:/home/jiangguohu/android-ndk-r10b
• 在目标工程目录下编译:
cd进入项目文件根目录,然后在终端输入:
$NDK/ndk-build