当前位置: 代码迷 >> Android >> Android-简略的JNI实例
  详细解决方案

Android-简略的JNI实例

热度:36   发布时间:2016-04-28 06:18:06.0
Android---简单的JNI实例
一、JNI概述
JNI 是Java Native Interface的缩写,中文翻译为“Java本地调用”,JNI 是本地编程接口。它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程语言(如 C、C++ 和汇编语言)编写的应用程序和库进行互操作。就是说,JNI是一种技术,通过这种技术可以做到两点:
1)Java程序中的函数可以调用Native语言写的函数,Native一般指的是C/C++编写的函数。

2)Native程序中的函数可以调用Java层的函数,也就是说C/C++程序可以调用Java函数。


二、JNI应用

1、JNI本地调用:(以例子介绍)

        java:(HelloWorld.java)

class HelloWorld {	private native void print();	public static void main(String[] args){		new HelloWorld().print();	}	static {		System.loadLibrary("HelloWorld");	}}

新建一个java文件,可以看出第二行有个native,说明这个函数是java和其他语言(c/c++)协作时用的,并用其他语言实现。在文件中有 System.loadLibrary,它是对本地方法的加载,原则上是在调用native函数前,任何时候,任何地方加载都可以,它的参数是动态库的名字。
然后需要用命令javac HelloWorld.java生成HelloWorld.class文件,
下一步是利用javah -jni -classpath . HelloWorld生成对应C/C++的HelloWorld.h文件,设置classpath的目的在于告诉java环境,在哪些目录下可以找到所要执行的java程序所需要的类或者包,此处不用声明.class文件名,在对应目录下编译器会自动找到。查看.h文件:

*******/*  * Class:     HelloWorld  * Method:    print  * Signature: ()V  */ JNIEXPORT void JNICALL Java_HelloWorld_print   (JNIEnv *, jobject);*******

注释部分是对应文件的类、native方法和方法签名。方法的签名是由方法的参数和返回值类型共同构成的,如.h文件中,Signature:()V,其中()中代表的是方法参数,V代表的是方法无返回值。java程序中参数类型和Signature有不同的对应值,可网上查阅,此处不赘述。
此处对应java中方法的jni函数,JNIEXPORT void JNICALL Java_HelloWorld_print (JNIEnv *, jobject);这里的JNIEXPORT和JNICALL都是JNI关键字,表示此函数是要被JNI调用的。
JNIEnv是一个与线程相关的代表JNI环境的结构体,用来调用JAVA的函数或操作Jobject对象等


第二个参数的意义取决于该方法是静态还是实例方法,当本地方法作为一个实例方法时,第二个参数相当于对象本身,即this,当本地方法作为一个静态方法时,指向所在类。
C:(HelloWorld.c)

#include <jni.h> #include <stdio.h> #include "HelloWorld.h" JNIEXPORT void JNICALL Java_HelloWorld_print(JNIEnv *env, jobject obj) { 	printf("Hello World!\n"); 	return; }
此时.c文件中的函数要跟.H文件中一样,这样在JAVA调用时才能根据库找到对应的JNI函数,进而调用执行该函数。
下面是生成动态库。
$:gcc -I/usr/lib/jvm/java-6-sun/include/linux/ -I/usr/lib/jvm/java-6-sun/include/ -fPIC -shared -o libHelloWorld.so HelloWorld.c
gcc是c程序的编译命令,-I用来指定路径下的相关头文件,最后执行java HelloWorld就可以完成调用。
2、android下jni调用(eclipse)
(1)ubuntu下Android NDK开发环境搭建(仅参考我本地搭建方法)
①下载NDK,http://dl.google.com/android/ndk/android-ndk-r4b-linux-x86.zip
②解压到当前目录
$pwd <--查看当前目录   ~/android-ndk-r4b
③配置环境变量
$gedit ~/.bashrc   :在打开的文件末尾添加以下内容
NDK=~/android-ndk-r4d
export NDK
④在当前环境下读取并执行~/.bashrc中的命令:
$source ~/.bashrc
⑤查看是否生效
$echo $NDK
~/android-ndk-r4b/
(2)注册JNI函数
①静态注册
java:
package com.example.hellojni; import android.app.Activity; import android.widget.TextView; import android.os.Bundle; public class HelloJni extends Activity {     @Override     public void onCreate(Bundle savedInstanceState)     {         super.onCreate(savedInstanceState);         TextView  tv = new TextView(this);         tv.setText( stringFromJNI() );         setContentView(tv);     }     public native String  stringFromJNI();     static {         System.loadLibrary("hello-jni");     } }
此时会在工程目录中/bin/classes/com/example/hellojni中会自动生成.class文件,
此时可以利用命令javah -jni -classpath bin/classes/ com.example.hellojni.HelloJni生成.h文件,可见当前文件夹下生成了一个有包名类名组成的com_example_hellojni_HelloJni.h文件,
新建一个jni文件夹,并新建一个hello-jni.c和Android.mk。
Android.mk

LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE    := hello-jni LOCAL_SRC_FILES := hello-jni.c include $(BUILD_SHARED_LIBRARY)
代码如上,android.mk是android提供的一种makefile文件,用来指定编译生成的so库名、引用的头文件等。LOCAL_PATH 用于给出当前文件的路径;LOCAL_MODULE是模块的名字,必须唯一切不能包含空格;LOCAL_SRC_FILES指定要编译的源文件列表;LOCAL_SHARED_LIBRARIES则表示模块在运行时要以来的共享库,在链接时需要。
hello-jni.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 !"); }
这个文件中的函数名必须和生成的.h文件中的函数名一致,否则可能会无法找到,也就是Java、对应的包名类名和函数名组成了新函数名。
执行ndk:/home/wesley/android-ndk-r4b/ndk-build,在obj文件中生成库,在eclipse中全部加载运行就可以在模拟器中显示。
总结:当Java层调用 stringFromJNI函数时,它会从对应的JNI库中寻找Java_com_example_hellojni_HelloJni_stringFromJNI函数,如果没有,便回报错,如果找到,则会为这个stringFromJNI和Java_com_example_hellojni_HelloJni_stringFromJNI建立一个关联关系,其实就是保存JNI层函数的函数指针,以后调用stringFromJNI函数时,直接使用这个函数指针就可以了,当然这个工作是由虚拟机来完成的。
②动态注册
从静态注册中可以看出来,它有几个弊端,首先就是需要编译所有声明了native函数的Java类,每个所生成的class文件都得用javah生成头文件;其次javah生成的JNI层函数名特别长,书写不便;最后初次调用native函数时要根据函数名搜索对应的JNI层函数来建立关联关系,这样很影响效率。故动态注册,直接让native函数知道JNI层对应函数的函数指针。
上面的工程改成动态注册只要改C文件:
HelloWorld.c:
#include <string.h>#include <jni.h>#include <stdlib.h>#include <assert.h>#include <stdio.h>jstring native_hello(JNIEnv* env, jobject thiz){	return (*env)->NewStringUTF(env, "动态注册JNI");}/**方法对应表*/static JNINativeMethod gMethods[] = {	{"stringFromJNI","()Ljava/lang/String;", (void*)native_hello},};/**为某一个类注册本地方法*/static int registerNativeMethods(JNIEnv* env, const char* className, 				 JNINativeMethod* gMethods, int numMethods){	jclass clazz; 	/*	env指向一个JNIEnv结构体,classname对应为Java类名,由于JNINativeMethod中	使用的函数不是全路径名,所以要指明是哪个类。	*/	clazz = (*env)->FindClass(env, className);	if(clazz == NULL){		return JNI_FALSE;	}	//实际上是调用JNIEnv的RegisterNatives函数完成注册	if((*env)->RegisterNatives(env, clazz, gMethods, numMethods) < 0){		return JNI_FALSE;	}	return JNI_TRUE;}/**为所有类注册本地方法*/static int registerNatives(JNIEnv* env){	const char* kClassName = "com/example/hellojni/HelloJni";	return registerNativeMethods(env, kClassName, gMethods, 				sizeof(gMethods)/sizeof(gMethods[0]));}/**System.loadLibrary("lib")时调用,如果成功返回JNI版本,失败返回-1*该函书的第一个参数类型为JavaVM,是虚拟机在JNI层的代表*每个Java进程只有一个这样的JavaVM*/JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved){	JNIEnv* env = NULL;	jint result = -1;		if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_4)!= JNI_OK){		return -1;	}	assert(env != NULL);		//动态注册JNI函数	if(!registerNatives(env)){		return -1;	}	result = JNI_VERSION_1_4;		return result;}
在jni.h中定义了JNINativeMethod结构体,利用这个结构体就可以把java中native函数和此处的函数名对应。那为什么需要这个签名信息呢?因为Java支持函数重载,也就是说,可以定义同名但不同参数的函数。但仅仅根据函数名是没法找到具体函数的,JNI技术中就将参数类型和返回值类型的组合成为了一个函数的签名信息,有了签名信息和函数名,就能顺利地找到Java中的函数了。
typedef struct {	//java中native函数的名字,不用带包路径char *name; //Java函数的签名信息。char *signature; //JNI层对应的函数指针void *fnPtr;} JNINativeMethod;
此结构体中的signature可以通过命令来取得,在class文件所在的目录执行javap -p-s  HelloWorld。
然后加载库就可以完成动态注册。
总结:当Java层通过System.loadLibrary加载万JNI动态库后,紧接着会查找该库中一个叫JNI_OnLoad的函数,如果有,就调用它,而动态注册的工作就是从这里完成的。

(3)通过JNIEnv操作jobject:
java对象是由成员变量和成员函数组成的,操作jobject的本质就应当是操作这些对象的成员变量和成员函数。它们是类的属性,所以在JNI规则中,用jfieldID和jmethodID来表示Java累的成员变量和成员函数,可通过JNIEnv的下面两个函数得到:
jfieldID GetFieldID(jclass clazz, const char *name, const char *sig)
jmethodID GetMethodID(jclass clazz, const char *name, const char *sig)
其中,jclass代表Java类,name表示成员函数或成员变量的名字,sig为这个函数和变量的签名信息。将获取的ID可以保存以下,这样可以提高效率。
(4)垃圾回收
①Local Reference:本地引用。在JNI层函数中使用的非全局引用对象都是ocal Reference,它包括函数调用时传入的jobject和JNI层函数中创建的jobject。ocal Reference最大的特点就是一旦JNI层函数返回,这些jobject就可能被垃圾回收。
②Global Reference:全局引用,这种对象如不住动释放,它永远不会被垃圾回收。
③Weak Global Reference:弱全局引用,一种特殊的Global Reference,在运行过程中可能会被垃圾回收,所以在使用之前,需JNIEnv的IsSameObject判断是否被回收了。



  相关解决方案