关于android中JNI层的理解

本文详细介绍了Android中JNI的工作原理,包括如何使用JNI连接Java层和本地层,本地库的命名规则,以及JNI的静态和动态注册方法。此外,还讲解了JNIEnv接口的作用、数据类型转换、字符串和数组处理、访问Java字段和方法,以及JNI的异常处理策略。通过实例展示了JNI在实际开发中的应用。

 

关于JNI的理解

 

android的平台上,JNIJava native interface)是连接Java层和native层的一座桥梁。其实就是说JNIJava调用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_methodjava_android_****_****_method建立一个关联关系,其实就是保存JNI层函数的指针。以后再调用native_method函数的时候,直接使用这个函数指针就可以了,当然这项工作是由虚拟机完成的。

2JNI的动态注册

动态注册的方式主要是实现这个JNINativeMethod的结构。在这个结构中,可以把java native函数和JNI函数一一对应起来。

描述这个结构体:typedef struct{

                 //Javanative的函数名

                 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. 数据类型:

 

JavaJNI的数据类型之间是一一对应的关系。

 

4 JNIEnv的解释:

JNIEnv 是一个指向线程局部数据的接口指针,这个指针里面包含了一个指向函数表的指针。在

这个表中,每一个函数位于一个预定义的位置上面。 JNIEnv很像一个 C++ 虚函数表或者

Microsoft COM 接口。

演示了这种关系。

         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

最前面的四个函数都是用来做异常处理的 。 剩下的都是用来释放资源的 , 通常异常发生后都需要释放资源。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值