关于android中JNI层的理解
2012-11-07 19:30
357 查看
关于JNI的理解
在android的平台上,JNI(Java native interface)是连接Java层和native层的一座桥梁。其实就是说JNI是Java调用native方法的一个“接口”。
JNI 可以这样与本地程序进行交互:
(1) 你可以使用 JNI来实现“本地方法”( native methods
),并在 JAVA 程序中调用它们。
(2 ) JNI 支持一个“调用接口”( invocation interface),它允许你把一个 JVM
嵌入到本地程序中。本地程序可以链接一个实现了 JVM的本地库,然后使用“调用接口”执行 JAVA
语言编写的软件模块。例如,一个用 C语言写的浏览器可以在一个嵌入式 JVM
上面执行从网上下载下来的 applets。
图一 JNI的工作流程
JNI层对应的库是lib****_jin.so, native层对应的库命名为lib****.so,其中两者的****是保持一致的。Java层通过lib****_jin.so实现跟lib****.so的交互。
Java层使用JNI很方便,可以直接加载JNI库,即在程序中添加一条加载语句即可。
例如:
Static{
System.LoadLibrary(“media_jni”);
}
同时,在Java程序中使用关键字native声明函数,此时表示调用的是native的方法。
2. JNI的注册问题。
一般情况下JNI的注册有两种:静态的方法和动态注册。
(1)静态方法如下图所示:
图 2 JNI静态注册方法
本质是:当Java层调用一个native_method函数的时候,它会首先从对应的JNI库中寻找这个java_android_****_****_method函数,如果没有就会报错。如果找到了就会为这个native_method和java_android_****_****_method建立一个关联关系,其实就是保存JNI层函数的指针。以后再调用native_method函数的时候,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
(2)JNI的动态注册
动态注册的方式主要是实现这个JNINativeMethod的结构。在这个结构中,可以把java native函数和JNI函数一一对应起来。
描述这个结构体:typedef struct{
//Java中native的函数名
Const char* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型组合
Const char* signature;
//JNI层对应函数的函数指针,都是(void*)类型。
Void* fnPtr;
}JNINativeMethod;
其中,这个参数2,就是写法问题:
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
对于这个结构体的使用是定义了一个JNINativeMethod数组,其中的成员就是所有native函数的一一对应。
例如:
Static JNINativeMethod gMethod[] = {
{“method1” , (Ljava/lang/String;Ljava/lang/String;)V , (void*)android_****_****_ method1},
{“method2” , (Ljava/lang/)V , (void*)android_****_****_ method2},
{“method3” , ()V , (void*)android_****_****_ method3},
{“method4” , (Ljava/lang/String;Ljava/lang/String;)V , (void*)android_****_****_ method4},
{“method1” , (Ljava/lang/String)V , (void*)android_****_****_ method5},
:
:
};
这个数组在android_****_****.ccp中,就是在JNI层的文件中。同时,在android_****_****.ccp中使用注册语句完成。
例如:Int register_android_media_mediarecorder(JNIEnv *env);
在这里需要主要的是,这个register是由JNI_OnLoad()函数完成。这个函数是Java层通过System.LoadLibrary加载完JNI动态库之后,紧接着就会在该库中查找的。如果有,就调用它,而且动态注册的工作就是在这里完成的。
3. 数据类型:
Java和JNI的数据类型之间是一一对应的关系。
4. JNIEnv的解释:
JNIEnv 是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在
这个表中,每一个函数位于一个预定义的位置上面。 JNIEnv很像一个 C++
虚函数表或者
Microsoft COM
接口。
图3 演示了这种关系。
图3 JNIEnv接口指针
如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个 JNIEnv接口指针。从同一个线程中调用的本地方法,传入的 JNIEnv
指针是相同的。本地方法可能被不同的线程调用,这时,传入的 JNIEnv
指针是不同的。但 JNIEnv 间接指向的函数表在多个线程间是共享的。
JNIEnv 指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。因为 JNIEnv指针是线程局部的,本地代码决不能跨线程使用 JNIEnv
。
JNIEnv实际的工作是提供了一些JNI的系统函数,通过这些函数可以完成两项工作:
(1)调用Java函数。
(2)操作jobject对象等诸多事情。
通过JNIEnv操作jobject。操作jobject的本质就相当于操作这些对象的成员变量和成员函数。成员变量和成员函数是由类定义的。它们是类的属性,所以在JNI中,用jfieldID,
jmethodID来表示java成员变量和成员函数。
JNIEnv的一些方法介绍:
获取字符串和释放空间
1.
对字符串的处理函数:
GetStringChars( );
GetStringUTFChars( );
Get/SetStringRegion( );
Get/SetString-UTFRegion( );
GetStringCritical( );
需要注意的是,这些函数都是会占用资源的,所以最后都是要释放掉ReleaseStringChars( )。
例如:
本地代码中,必须使用合适的 JNI函数把 jstring
转化为 C/C++字符串。 JNI
支持字符串在Unicode和 UTF-8
两种编码之间转换。 Unicode字符串代表了 16-bit
的字符集合。 UTF-8字符串使用一种向上兼容 7-bit ASCII
字符串的编码协议。 UTF-8字符串很像 NULL
结尾的字符串,在包含非 ASCII字符的时候依然如此。所有的 7-bitASCII
字符的值都在 1~127 之间,这些值在 UTF-8编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符( 16-bitUnicode值)。函数
Java_Prompt_getLine 通过调用 JNI
函数 GetStringUTFChars来读取字符串的内容。
GetStringUTFChars
可以把一个 jstring 指针(指向 JVM内部的 Unicode
字符序列)转化成一个 UTF-8格式的 C
字符串。如果确信原始字符串数据只包含 7-bit ASCII字符,你可以把转化后的字符串传递给常规的 C
库函数使用,如 printf。
JNIEXPORT jstring JNICALL
Java_ Prompt _getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than* 127 characters */
scanf("%s", b);
从 GetStringUTFChars中获取的 UTF-8
字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉 JVM
这个 UTF-8字符串不会被使用了,因为这个
UTF-8 字符串占用的内存会被回收。
2.
访问基本类型数组
的程序调用了一个本地方法,这个方法对一个数
class IntArray {
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
对应的JNI:
合适的 JNI函数来访问基本数组元素:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
3.访问对象数组
下面的例子调用了一个本地方法来创建一个二维的 int数组,然后打印这个数组的内容:
class ObjectArrayTest {
private static native int[][] initInt2DArray(int size);
public static void main(String[] args) {
int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(" " + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.loadLibrary("ObjectArrayTest");
}
}
JNI本地方法可以是下面这样子的:
JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env,
jclass cls,
int size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = (*env)->NewObjectArray(env, size, intArrCls,
NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}
2. JNI访问Java的字段。
利用field可以使JNI来访问Java中的数据。就是说可以修改Java中数据的内容,可以理解为是callback。
经常使用的函数:
// 获取类引用。
GetObjectClass( );
GetFieldID( );
// 获取字符串和数组对象。
GetObjectField( );
// 重新设置对象内容。
SetObjectField( );
JNI访问Java的方法:
(1)
实例方法必须在类的某个类对象实现上调用。
(2)
静态方法在任何一个对象实例上都可以调用。
(3)
调用父类的实例方法
(4)
调用构造函数。
GetObjectClass( );
GetMethodID( );
Call<Type>Method( );// JNI使用Call<Type>来调用返回值为< Type>的实例方法。
6. JNI中的异常处理函数:
一旦 JNI
发生错误,必须依赖于 JVM 来处理异常。通过调用 Throw或者 ThrowNew
来向 JV M抛出一个异常。一个未被处理的异常会记录在当前线程中。和 JAVA中的异常不同,本地代码中的异常不会立即中断当前的程序执行。本地代码中没有标准的异常处理机制,因此, JNI程序最好在每一步可能会产生异常的操作后面都检查和处理异常。
JNI程序员处理异常通常有两种方式:
1 、本地方法可以选择立即返回。让代码中抛出的异常向调用者抛出。
2 、本地代码可以通过调用 ExceptionClear清理异常并运行自己的异常处理代码。
异常发生后,一定要先进行处理或者清除后再进行后续的 JNI函数调用。大部分情况下,调用一个未被处理的异常都可能会一个未定义的结果。下面列表中的 JNI函数可以在发生异常后安全地调用:
调用:
• ExceptionOccurred
• ExceptionDescribe
• ExceptionClear
• ExceptionCheck
•
• ReleaseStringChars
• ReleaseStringUTFchars
• ReleaseStringCritical
• Release<Type>ArrayElements
• ReleasePrimitiveArrayCritical
• DeleteLocalRef
• DeleteGlobalRef
• DeleteWeakGlobalRef
• MonitorExit
最前面的四个函数都是用来做异常处理的 。 剩下的都是用来释放资源的 , 通常异常发生后都需要释放资源。
在android的平台上,JNI(Java native interface)是连接Java层和native层的一座桥梁。其实就是说JNI是Java调用native方法的一个“接口”。
JNI 可以这样与本地程序进行交互:
(1) 你可以使用 JNI来实现“本地方法”( native methods
),并在 JAVA 程序中调用它们。
(2 ) JNI 支持一个“调用接口”( invocation interface),它允许你把一个 JVM
嵌入到本地程序中。本地程序可以链接一个实现了 JVM的本地库,然后使用“调用接口”执行 JAVA
语言编写的软件模块。例如,一个用 C语言写的浏览器可以在一个嵌入式 JVM
上面执行从网上下载下来的 applets。
图一 JNI的工作流程
JNI层对应的库是lib****_jin.so, native层对应的库命名为lib****.so,其中两者的****是保持一致的。Java层通过lib****_jin.so实现跟lib****.so的交互。
Java层使用JNI很方便,可以直接加载JNI库,即在程序中添加一条加载语句即可。
例如:
Static{
System.LoadLibrary(“media_jni”);
}
同时,在Java程序中使用关键字native声明函数,此时表示调用的是native的方法。
2. JNI的注册问题。
一般情况下JNI的注册有两种:静态的方法和动态注册。
(1)静态方法如下图所示:
图 2 JNI静态注册方法
本质是:当Java层调用一个native_method函数的时候,它会首先从对应的JNI库中寻找这个java_android_****_****_method函数,如果没有就会报错。如果找到了就会为这个native_method和java_android_****_****_method建立一个关联关系,其实就是保存JNI层函数的指针。以后再调用native_method函数的时候,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。
(2)JNI的动态注册
动态注册的方式主要是实现这个JNINativeMethod的结构。在这个结构中,可以把java native函数和JNI函数一一对应起来。
描述这个结构体:typedef struct{
//Java中native的函数名
Const char* name;
//Java函数的签名信息,用字符串表示,是参数类型和返回值类型组合
Const char* signature;
//JNI层对应函数的函数指针,都是(void*)类型。
Void* fnPtr;
}JNINativeMethod;
其中,这个参数2,就是写法问题:
其中比较难以理解的是第二个参数,例如
"()V"
"(II)V"
"(Ljava/lang/String;Ljava/lang/String;)V"
实际上这些字符是与函数的参数类型一一对应的。
对于这个结构体的使用是定义了一个JNINativeMethod数组,其中的成员就是所有native函数的一一对应。
例如:
Static JNINativeMethod gMethod[] = {
{“method1” , (Ljava/lang/String;Ljava/lang/String;)V , (void*)android_****_****_ method1},
{“method2” , (Ljava/lang/)V , (void*)android_****_****_ method2},
{“method3” , ()V , (void*)android_****_****_ method3},
{“method4” , (Ljava/lang/String;Ljava/lang/String;)V , (void*)android_****_****_ method4},
{“method1” , (Ljava/lang/String)V , (void*)android_****_****_ method5},
:
:
};
这个数组在android_****_****.ccp中,就是在JNI层的文件中。同时,在android_****_****.ccp中使用注册语句完成。
例如:Int register_android_media_mediarecorder(JNIEnv *env);
在这里需要主要的是,这个register是由JNI_OnLoad()函数完成。这个函数是Java层通过System.LoadLibrary加载完JNI动态库之后,紧接着就会在该库中查找的。如果有,就调用它,而且动态注册的工作就是在这里完成的。
3. 数据类型:
Java和JNI的数据类型之间是一一对应的关系。
4. JNIEnv的解释:
JNIEnv 是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在
这个表中,每一个函数位于一个预定义的位置上面。 JNIEnv很像一个 C++
虚函数表或者
Microsoft COM
接口。
图3 演示了这种关系。
图3 JNIEnv接口指针
如果一个函数实现了一个本地方法,那么这个函数的第一个参数就是一个 JNIEnv接口指针。从同一个线程中调用的本地方法,传入的 JNIEnv
指针是相同的。本地方法可能被不同的线程调用,这时,传入的 JNIEnv
指针是不同的。但 JNIEnv 间接指向的函数表在多个线程间是共享的。
JNIEnv 指针指向一个线程内的局部数据结构是因为一些平台上面没有对线程局部存储访问的有效支持。因为 JNIEnv指针是线程局部的,本地代码决不能跨线程使用 JNIEnv
。
JNIEnv实际的工作是提供了一些JNI的系统函数,通过这些函数可以完成两项工作:
(1)调用Java函数。
(2)操作jobject对象等诸多事情。
通过JNIEnv操作jobject。操作jobject的本质就相当于操作这些对象的成员变量和成员函数。成员变量和成员函数是由类定义的。它们是类的属性,所以在JNI中,用jfieldID,
jmethodID来表示java成员变量和成员函数。
JNIEnv的一些方法介绍:
获取字符串和释放空间
1.
对字符串的处理函数:
GetStringChars( );
GetStringUTFChars( );
Get/SetStringRegion( );
Get/SetString-UTFRegion( );
GetStringCritical( );
需要注意的是,这些函数都是会占用资源的,所以最后都是要释放掉ReleaseStringChars( )。
例如:
本地代码中,必须使用合适的 JNI函数把 jstring
转化为 C/C++字符串。 JNI
支持字符串在Unicode和 UTF-8
两种编码之间转换。 Unicode字符串代表了 16-bit
的字符集合。 UTF-8字符串使用一种向上兼容 7-bit ASCII
字符串的编码协议。 UTF-8字符串很像 NULL
结尾的字符串,在包含非 ASCII字符的时候依然如此。所有的 7-bitASCII
字符的值都在 1~127 之间,这些值在 UTF-8编码中保持原样。一个字节如果最高位被设置了,意味着这是一个多字节字符( 16-bitUnicode值)。函数
Java_Prompt_getLine 通过调用 JNI
函数 GetStringUTFChars来读取字符串的内容。
GetStringUTFChars
可以把一个 jstring 指针(指向 JVM内部的 Unicode
字符序列)转化成一个 UTF-8格式的 C
字符串。如果确信原始字符串数据只包含 7-bit ASCII字符,你可以把转化后的字符串传递给常规的 C
库函数使用,如 printf。
JNIEXPORT jstring JNICALL
Java_ Prompt _getLine(JNIEnv *env, jobject obj, jstring prompt)
{
char buf[128];
const jbyte *str;
str = (*env)->GetStringUTFChars(env, prompt, NULL);
if (str == NULL) {
return NULL; /* OutOfMemoryError already thrown */
}
printf("%s", str);
(*env)->ReleaseStringUTFChars(env, prompt, str);
/* We assume here that the user does not type more than* 127 characters */
scanf("%s", b);
从 GetStringUTFChars中获取的 UTF-8
字符串在本地代码中使用完毕后,要使用ReleaseStringUTFChars告诉 JVM
这个 UTF-8字符串不会被使用了,因为这个
UTF-8 字符串占用的内存会被回收。
2.
访问基本类型数组
的程序调用了一个本地方法,这个方法对一个数
class IntArray {
private native int sumArray(int[] arr);
public static void main(String[] args) {
IntArray p = new IntArray();
int arr[] = new int[10];
for (int i = 0; i < 10; i++) {
arr[i] = i;
}
int sum = p.sumArray(arr);
System.out.println("sum = " + sum);
}
static {
System.loadLibrary("IntArray");
}
}
对应的JNI:
合适的 JNI函数来访问基本数组元素:
JNIEXPORT jint JNICALL
Java_IntArray_sumArray(JNIEnv *env, jobject obj, jintArray arr)
{
jint buf[10];
jint i, sum = 0;
(*env)->GetIntArrayRegion(env, arr, 0, 10, buf);
for (i = 0; i < 10; i++) {
sum += buf[i];
}
return sum;
}
3.访问对象数组
下面的例子调用了一个本地方法来创建一个二维的 int数组,然后打印这个数组的内容:
class ObjectArrayTest {
private static native int[][] initInt2DArray(int size);
public static void main(String[] args) {
int[][] i2arr = initInt2DArray(3);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
System.out.print(" " + i2arr[i][j]);
}
System.out.println();
}
}
static {
System.loadLibrary("ObjectArrayTest");
}
}
JNI本地方法可以是下面这样子的:
JNIEXPORT jobjectArray JNICALL
Java_ObjectArrayTest_initInt2DArray(JNIEnv *env,
jclass cls,
int size)
{
jobjectArray result;
int i;
jclass intArrCls = (*env)->FindClass(env, "[I");
if (intArrCls == NULL) {
return NULL; /* exception thrown */
}
result = (*env)->NewObjectArray(env, size, intArrCls,
NULL);
if (result == NULL) {
return NULL; /* out of memory error thrown */
}
for (i = 0; i < size; i++) {
jint tmp[256]; /* make sure it is large enough! */
int j;
jintArray iarr = (*env)->NewIntArray(env, size);
if (iarr == NULL) {
return NULL; /* out of memory error thrown */
}
for (j = 0; j < size; j++) {
tmp[j] = i + j;
}
(*env)->SetIntArrayRegion(env, iarr, 0, size, tmp);
(*env)->SetObjectArrayElement(env, result, i, iarr);
(*env)->DeleteLocalRef(env, iarr);
}
return result;
}
2. JNI访问Java的字段。
利用field可以使JNI来访问Java中的数据。就是说可以修改Java中数据的内容,可以理解为是callback。
经常使用的函数:
// 获取类引用。
GetObjectClass( );
GetFieldID( );
// 获取字符串和数组对象。
GetObjectField( );
// 重新设置对象内容。
SetObjectField( );
JNI访问Java的方法:
(1)
实例方法必须在类的某个类对象实现上调用。
(2)
静态方法在任何一个对象实例上都可以调用。
(3)
调用父类的实例方法
(4)
调用构造函数。
GetObjectClass( );
GetMethodID( );
Call<Type>Method( );// JNI使用Call<Type>来调用返回值为< Type>的实例方法。
6. JNI中的异常处理函数:
一旦 JNI
发生错误,必须依赖于 JVM 来处理异常。通过调用 Throw或者 ThrowNew
来向 JV M抛出一个异常。一个未被处理的异常会记录在当前线程中。和 JAVA中的异常不同,本地代码中的异常不会立即中断当前的程序执行。本地代码中没有标准的异常处理机制,因此, JNI程序最好在每一步可能会产生异常的操作后面都检查和处理异常。
JNI程序员处理异常通常有两种方式:
1 、本地方法可以选择立即返回。让代码中抛出的异常向调用者抛出。
2 、本地代码可以通过调用 ExceptionClear清理异常并运行自己的异常处理代码。
异常发生后,一定要先进行处理或者清除后再进行后续的 JNI函数调用。大部分情况下,调用一个未被处理的异常都可能会一个未定义的结果。下面列表中的 JNI函数可以在发生异常后安全地调用:
调用:
• ExceptionOccurred
• ExceptionDescribe
• ExceptionClear
• ExceptionCheck
•
• ReleaseStringChars
• ReleaseStringUTFchars
• ReleaseStringCritical
• Release<Type>ArrayElements
• ReleasePrimitiveArrayCritical
• DeleteLocalRef
• DeleteGlobalRef
• DeleteWeakGlobalRef
• MonitorExit
最前面的四个函数都是用来做异常处理的 。 剩下的都是用来释放资源的 , 通常异常发生后都需要释放资源。
相关文章推荐
- 关于Android的一些理解
- Android关于Service的几点理解
- Android关于TextureView理解及基本使用
- 关于android中Context参数的理解
- android关于多dex打包的理解
- 关于 android 权重 weight在布局中的理解和使用
- 关于Android.mk和build/envsetup.sh的一些小小理解
- 关于 Android 中 Bitmap 的 ARGB_8888、ALPHA_8、ARGB_4444、RGB_565 的理解
- 关于对Android Activity 生命周期的7个方法的理解
- Android中关于JNI 的学习(三)在JNI层訪问Java端对象
- android 关于触摸事件的理解
- 关于android中线程,进程,组件,app的理解
- 为自己记----android中关于actionbar的一些简单理解
- (转载)关于android:id="@+id/xx"的理解
- 一篇文章理解所有android关于存储的方法
- Android中关于“UI只能在主线程中更新”说法的理解
- Android开发中关于Layout_weight的理解
- 关于Android系统四大组件的理解分析
- 关于Android事件传递机制自己的理解
- Android开发---关于回调函数的理解