Android NDK开发篇:Java与原生代码通信(数据操作)

来源:互联网 发布:淘宝客服绩效考核方法 编辑:程序博客网 时间:2024/05/11 15:35

 虽然说使用NDK可以提高Android程序的执行效率,但是调用起来还是稍微有点麻烦。NDK可以直接使用Java的原生数据类型,而引用类型,因为Java的引用类型的实现在NDK被屏蔽了,所以在NDK使用Java的引用类型则要做相应的处理。


  一、对引用数据类型的操作

  虽然Java的引用类型的实现在NDK被屏蔽了,JNI还是提供了一组API,通过JNIEnv接口指针提供原生方法修改和使用Java的引用类型。

    1、字符串操作

    JNI把Java的字符串当作引用来处理,在NDK中使用Java的字符串,需要相关的API进行转换。JNI支持Unicode编码和UTF-8编码的字符串,有两组函数通过JNIEnv接口指针处理这些字符串编码:

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jstring javaString;  
  2. javaString = (*env)->NewStringUTF(env, "Hello World");  

    该方法生成一个的UTF-8编码字符串。


    2、Java字符串转C字符串

    要在原生方法中使用Java字符串,需要将Java字符串转成C字符串。可以调用GetStringChars函数:

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. const jbyte *str;  
  2. jboolean isCopy;  
  3.   
  4. str = (*env)->GetStringUTFChars(env, javaString, &isCopy);  

    第三个参数isCopy,可以用作判断该函数返回的字符串是否是Java字符串的副本,还是直接指向Java字符串的内存。


    3、释放字符串

    通过JNIEnv调用的GetStringChars和GetStringUTFChars函数获得的C字符串在使用完后要正确释放,否则就会造成内存泄漏。可通过ReleaseStringUTFChars函数(用于释放Unicode)和ReleaseStringUTFChars函数(用于释放UTF-8)释放字符串。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. (*env)->ReleaseStringUTFChars(env, javaString, str);  
  2. (*env)->ReleaseStringChars(env, javaString, str);  


  二、数组操作

  JNI把Java的数组也是当作引用类型处理的,不过JNI还是提供了函数操作Java数组的。

    1、创建数组

    直接用New<Type>Array函数可以创建数组实例。Type可以是原生数据类型,也可以是Object,使用相应的API传递参数确定大小。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jintArray array;  
  2. array = (*env)->NewIntArray(env, 10);  
  3. if (0 == array) {  
  4.     // do it  
  5. }  

    2、访问数组

    JNI有两种方式可以访问Java数组,可以将数组的代码复制成C数组,然后再操作C数组,完成后提交修改,这样效率有点低。另外一办法是让JNI直接提供指向数组元素的指针。

    方法一:使用副本,调用Get<Type>ArrayRegion函数复制,Set<Type>ArrayRegion函数提交修改

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. // 将Java数组复制到C数组  
  2. jintArray javaArray;  
  3. jint array[10];  
  4.   
  5. // ...  
  6.   
  7. // 复制数组  
  8. (*env)->GetIntArrayRegion(env, javaArray, 010, array);  
  9.   
  10. // do it  
  11.   
  12. // 提交修改  
  13. (*env)->SetIntArrayRegion(env, javaArray, 010, array);  

    当数组很大的时候,这个方法的效率就很低。


    方法二:直接操作指针,调用Get<Type>ArrayElements函数获取数组的指针


[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jint *array;  
  2. jboolean isCopy;  
  3. jintArray javaArray;  
  4.   
  5. // ...  
  6.   
  7. array = (*env)->GetIntArrayElements(env, javaArray, &isCopy);  

    第三个参数isCopy的作用同Java字符串转C字符串,是否为Java数组的副本。

    使用完之后,就要马上释放,否则会造成内存泄漏,释放函数是Release<Type>ArrayElements

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. (*env)->ReleaseIntArrayElements(env, javaArray, array, 0);  

    第三个参数0代表将内容复制回来并释放原生数组。如果是JNI_COMMIT,则复制回来,但不释放。JNI_ABORT,释放但不复制回来。


  三、NIO操作

  JNI提供NIO操作函数,使Java可以使用的原生代码创建的缓冲区。相比数组操作,NIO缓冲区的数据传输性能更好,适合在原生代码和Java应用之间传输大量数据。

    1、创建字节缓冲区,使用NewDirectByteBuffer方法

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. unsigned char *buff = (unsigned char *) malloc(1024);  
  2.   
  3. // ...  
  4.   
  5. jobject directBuff;  
  6. directBuff = (*env)->NewDirectByteBuffer(env, buffer, 1024);  

    需要注意的是,原生方法的内存分配不在虚拟机的管理范围,所以需要手动管理内存避免内存泄漏。


    2、获取Java字节缓冲区

    Java也可以创建字节缓冲区,在原生方法中调用GetDirectBufferAddress函数获取原生字节数组的内存地址

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. unsigned char *buff;  
  2. jbyteArray directBuffer;  
  3.   
  4. // ...  
  5.   
  6. buffer = (unsigned char *) (*env)->GetDirectBufferAddress(env, directBuffer);  

  四、访问域

  原生方法想要获取Java的成员变量和调用Java的成员函数,就要通过JNI提供的访问域方法。

  Java有两种域,分别是实例域和静态域。类的对象有个自己实例域的副本,而类一个的所有对象共用同一个静态域。有一下Java类,JavaClass:

[java] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. public class JavaClass {  
  2.       
  3.     private String instanceField = "instance filed";  
  4.       
  5.     private static String staticField = "static filed";  
  6.       
  7.     private String getInstanceField() {  
  8.         return instanceField;  
  9.     }  
  10.       
  11.     private static String getStaticField() {  
  12.         return staticField;  
  13.     }  
  14. }  


    1、获取域ID

    JNI通过域ID来访问两种域,可以通过实例的获取class对象,然后获取域ID,使用GetObjectClass函数可以获得class对象

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jclass clazz;  
  2. jobject instance;  
  3.   
  4. // ...  
  5.   
  6. clazz = (*env)->GetObjectClass(env, instance);  

    根据域的类型不同,使用GetFieldId函数获取实例域ID,GetStaticFieldId获取静态域ID,返回类型均为jfieldID;

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jfieldID fieldId;  
  2.   
  3. // 获取实例域ID  
  4. fieldId = (*env)->GetFieldID(env, clazz, "instanceField""Ljava/lang/String;");  
  5.   
  6. // 获取静态域ID  
  7. fieldId = (*env)->GetStaticFieldID(env, clazz, "staticField""Ljava/lang/String;");  

    两个函数的最后一个参数是Java中表示域类型的域描述符。

    可以缓存最频繁使用的域ID,这样可以提高性能。


    2、获取域

    获取域ID之后,就可以通过Get<Type>Field函数来获取实例域,通过GetStatic<Type>Field获取静态域

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jstring field;  
  2.   
  3. // 获取实例域  
  4. field = (*env)->GetObjectField(env, instance, fieldId);  
  5.   
  6. // 获取静态域  
  7. field = (*env)->GetStaticObjectField(env, clazz, fieldId);  

    获取一个Java域的值就要调用两到三个JNI函数,非常麻烦,而且效率也比较低,建议把需要的参数传递给原生方法,这样可以提高性能。


  四、调用方法

    1、获取方法ID

    和域一样,Java的方法有两类,访问这两类方法,先要获取方法的ID,使用GetMethodID和GetStaticMethodID,返回值类型为jmethodID。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jmethodID methodId;  
  2.   
  3. // 获取实例方法ID  
  4. methodId = (*env)->GetMethodID(env, clazz, "getInstanceField""()Ljava/lang/String;");  
  5.   
  6. // 获取静态方法ID  
  7. methodId = (*env)->GetStaticMethodID(env, clazz, "getStaticField""()Ljava/lang/String;");  

    两个方法的最后一个参数表示的方法描述符,在Java中表示方法签名。


    2、调用方法

    以方法ID为参数通过调用Call<ReturnType>Method和CallStatic<ReturnType>Method函数调用方法。

[cpp] view plain copy
 在CODE上查看代码片派生到我的代码片
  1. jstring result;  
  2.   
  3. // 调用实例方法  
  4. result = (*env)->CallStringMethod(env, instance, methodId);  
  5.   
  6. // 调用静态方法  
  7. result = (*env)->CallStaticStringMethod(env, clazz, methodId);  

    Java方法和原生代码的转换代价比较大,建议在类设计的时候要规划好,这样才能提高性能。


  五、域和方法描述符

  使用Java的成员变量和方法,都必须通过域描述符号和方法描述符来获取域和方法的ID。Java类型的签名映射关系如下:

Boolean -> Z

Byte -> B

Char -> C

Short -> S

Int -> I

Long -> J

Float -> F

Double -> D

其它类 -> L + 类名(包名用’\‘分隔)

type[] -> [type

方法 -> (参数类型签名) + 返回类型签名


  可见使用起来相当麻烦。


  关于Java与原生代码之间的通信,如果发生了内存泄漏,API就会返回NULL,崩溃的时候如果没有抛出异常,那么原生代码就会停止运行,应用程序会发生闪退。

0 0
原创粉丝点击