JNI/NDK开发指南(十一)——JNI异常处理

最后更新于:2022-04-01 07:39:12

### 异常简介 异常,显而意见就是程序在运行期间没有按照正常的程序逻辑执行,在执行过程当中出现了某种错误,导致程序崩溃。在Java中异常分为运行时异常(RuntimeException)和编译时异常,在程序中有可能运行期间发生异常的逻辑我们会用try…catch…来处理,如果没有处理的话,在运行期间发生异常就会导致程序奔溃。而编译时异常是在编译期间就必须处理的。本章主要介绍运行时异常。 **示例1:** ~~~ // 运行时异常 public static void exceptionCallback() { int a = 20 / 0; System.out.println("--->" + a); } ~~~ **示例2:** ~~~ // 编译期间异常 public static void testException() throws Exception { // ... System.out.println("testException() invoked!"); } public static void main(String[] args) { exceptionCallback(); try { testException(); } catch (Exception e) { e.printStackTrace(); } // .... } ~~~ ~~~ 在示例2中,testException方法声明时显示抛出了一个java.lang.Exception异常,所以在程序调用的地方必须用try...catch处理。 ~~~ 大家都知道,如果示例2中main方法执行到调用exceptionCallback方法时,方法第一行有一个除0的操作,因此该方法会抛出`java.lang.ArithmeticException`数学异常,而在main方法调用的时候并没有处理这个函数在运行时可能会发生的异常,所以会导致程序立即结束,而后面的代码`try{testException();}catch(Exception e) {e.printStackTrace();}`都不会被执行。运行示例2程序的你会看到下面的结果: ~~~ Exception in thread "main" java.lang.ArithmeticException: / by zero at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8) at com.study.jnilearn.JNIException.main(JNIException.java:22) ~~~ 我们改进一下上面这个程序: ~~~ public static void main(String[] args) { try { exceptionCallback(); } catch (Exception e) { e.printStackTrace(); } try { testException(); } catch (Exception e) { e.printStackTrace(); } } ~~~ 这时我们运行程序,调用`exceptionCallback`方法时会引发`java.lang.ArithmeticException: / by zero`异常,由于我们用try…catch块显示处理了异常,所以程序会继续往下执行,调用testException()函数,打印`testException() invoked!`。运行结果如下所示: ~~~ java.lang.ArithmeticException: / by zero at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8) at com.study.jnilearn.JNIException.main(JNIException.java:24) testException() invoked! ~~~ ### Java与JNI处理异常的区别 下面来小结一下: 1、在Java中如果觉得某段逻辑可能会引发异常,用try…catch机制来捕获并处理异常即可 2、如果在Java中发生运行时异常,没有使用try…catch来捕获,会导致程序直接奔溃退出,**后续的代码都不会被执行** 3、编译时异常,是在方法声明时显示用throw声明了某一个异常,编译器要求在调用的时候必须显示捕获处理 `public static void testException() throws Exception {}` 上面这几点,写过Java的朋友都知道,而且很简单,但我为什么还要拿出来说呢,其实我想重点说明的是,在JNI中发生的异常和Java完全不一样。***我们在写JNI程序的时候,JNI没有像Java一样有try…catch…final这样的异常处理机制,面且在本地代码中调用某个JNI接口时如果发生了异常,后续的本地代码不会立即停止执行,而会继续往下执行后面的代码。*** ### 异常处理示例 **示例3:** 这个例子在main中调用了doit本地方法,在本地方法中会回调`exceptionCallback`方法,该方法中会引发一个除0的运行时异常`java.lang.ArithmeticException`,我们通过这个示例来学习在JNI中如何来正确处理这种异常。 ~~~ package com.study.jnilearn; public class JNIException { public static native void doit(); public static void exceptionCallback() { int a = 20 / 0; System.out.println("--->" + a); } public static void normalCallback() { System.out.println("In Java: invoke normalCallback."); } public static void main(String[] args) { doit(); } static { System.loadLibrary("JNIException"); } } ~~~ ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_JNIException */ #ifndef _Included_com_study_jnilearn_JNIException #define _Included_com_study_jnilearn_JNIException #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_JNIException * Method: doit * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_JNIException_doit (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif // JNIException.c #include "com_study_jnilearn_JNIException.h" #include <stdio.h> JNIEXPORT void JNICALL Java_com_study_jnilearn_JNIException_doit(JNIEnv *env, jclass cls) { jthrowable exc = NULL; jmethodID mid = (*env)->GetStaticMethodID(env,cls,"exceptionCallback","()V"); if (mid != NULL) { (*env)->CallStaticVoidMethod(env,cls,mid); } printf("In C: Java_com_study_jnilearn_JNIException_doit-->called!!!!"); if ((*env)->ExceptionCheck(env)) { // 检查JNI调用是否有引发异常 (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); // 清除引发的异常,在Java层不会打印异常的堆栈信息 (*env)->ThrowNew(env,(*env)->FindClass(env,"java/lang/Exception"),"JNI抛出的异常!"); //return; } mid = (*env)->GetStaticMethodID(env,cls,"normalCallback","()V"); if (mid != NULL) { (*env)->CallStaticVoidMethod(env,cls,mid); } } ~~~ 程序运行结果如下: ~~~ Exception in thread "main" java.lang.ArithmeticException: / by zero at com.study.jnilearn.JNIException.exceptionCallback(JNIException.java:8) at com.study.jnilearn.JNIException.doit(Native Method) at com.study.jnilearn.JNIException.main(JNIException.java:17) Exception in thread "main" java.lang.Exception: JNI抛出的异常! at com.study.jnilearn.JNIException.doit(Native Method) In Java: invoke normalCallback. at com.study.jnilearn.JNIException.main(JNIException.java:17) In C: Java_com_study_jnilearn_JNIException_doit-->called!!!! ~~~ 在Main方法中调用doit本地方法后,程序的控制权即交给了JNI,在doit的本地方法中回调exceptionCallback方法,引发了一个`java.lang.ArithmeticException`异常,但本地接口并不会马上退出,而是会继续执行后面的代码,所以我们在调用完一个任何一个JNI接口之后,必须要做的一件事情就是检查这次JNI调用是否发生了异常,如果发生了异常不处理,而继续让程序执行后面的逻辑,将会产生不可预知的后果。在本例中,我们调用了JNI的`ExceptionCheck`函数检查最近一次JNi调用是否发生了异常,如果有异常这个函数返回JNI_TRUE,否则返回JNI_FALSE。当检测到异常时,我们调用`ExceptionDescribe`函数打印这个异常的堆栈信息,然后再调用`ExceptionClear`函数清除异常堆栈信息的缓冲区(如果不清除,后面调用ThrowNew抛出的异常堆栈信息会覆盖前面的异常信息),最后调用`ThrowNew`函数手动抛出一个java.lang.Exception异常。但在JNI中抛出未捕获的异常与Java的异常处理机制不一样,在JNI中并不会立即终止本地方法的执行,而是继续执行后面的代码。这种情况需要我们手动来处理。在例中的38行,如果你不用return马上退出方法的话,37行ThrowNew后面的代码依然会继续执行,如程序运行的结果一样,仍然会回调`normalCallback`方法,打印出:invoke normalCallback. 异常检查JNI还提供了另外一个接口,`ExceptionOccurred`,如果检测有异常发生时,该函数会返回一个指向当前异常的引用。作用和`ExceptionCheck`一样,两者的区别在于返回值不一样。我们改造一下上面的例子: ~~~ // .... jthrowable exc = NULL; exc = (*env)->ExceptionOccurred(env); // 返回一个指向当前异常对象的引用 if (exc) { (*env)->ExceptionDescribe(env); // 打印Java层抛出的异常堆栈信息 (*env)->ExceptionClear(env); // 清除异常信息 // 抛出我们自己的异常处理 jclass newExcCls; newExcCls = (*env)->FindClass(env,"java/lang/Exception"); if (newExcCls == NULL) { return; } (*env)->ThrowNew(env, newExcCls, "throw from C Code."); } // .... ~~~ ### 写一个抛出异常的工具类 当需要抛出自己的异常处理逻辑时,需要二步,调用FindClass找到异常处理类,然后调用ThrowNew抛出一个异常。为了简化操作步聚,我们写一个工具函数,根据一个异常类名专门用来生成一个指定名字的异常: ~~~ void JNU_ThrowByName(JNIEnv *env, const char *name, const char *msg) { // 查找异常类 jclass cls = (*env)->FindClass(env, name); /* 如果这个异常类没有找到,VM会抛出一个NowClassDefFoundError异常 */ if (cls != NULL) { (*env)->ThrowNew(env, cls, msg); // 抛出指定名字的异常 } /* 释放局部引用 */ (*env)->DeleteLocalRef(env, cls); } ~~~ ### 异常发生后释放资源 在异常发生后,释放资源是一件很重要的事情。下面的例子中,调用 GetStringChars 函数后,如果后面的代码发生异常,要记得调用 ReleaseStringChars 释放资源。 ~~~ JNIEXPORT void JNICALL Java_pkg_Cls_f(JNIEnv *env, jclass cls, jstring jstr) { const jchar *cstr = (*env)->GetStringChars(env, jstr); if (c_str == NULL) { return; } ... if ((*env)->ExceptionCheck(env)) { /* 异常检查 */ (*env)->ReleaseStringChars(env, jstr, cstr); // 发生异常后释放前面所分配的内存 return; } ... /* 正常返回 */ (*env)->ReleaseStringChars(env, jstr, cstr); } ~~~ ### 总结 1、当调用一个JNI函数后,必须先检查、处理、清除异常后再做其它 JNI 函数调用,否则会产生不可预知的结果。 2、一旦发生异常,立即返回,让调用者处理这个异常。或 调用 ExceptionClear 清除异常,然后执行自己的异常处理代码。 3、异常处理的相关JNI函数总结: 1> ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE 2> ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL 3> ExceptionDescribe:打印异常的堆栈信息 4> ExceptionClear:清除异常堆栈信息 5> ThrowNew:在当前线程触发一个异常,并自定义输出异常信息 `jint (JNICALL *ThrowNew) (JNIEnv *env, jclass clazz, const char *msg);` 6> Throw:丢弃一个现有的异常对象,在当前线程触发一个新的异常 `jint (JNICALL *Throw) (JNIEnv *env, jthrowable obj);` 7> FatalError:致命异常,用于输出一个异常信息,并终止当前VM实例(即退出程序) `void (JNICALL *FatalError) (JNIEnv *env, const char *msg);`
';

Android JNI局部引用表溢出:local reference table overflow (max=512)

最后更新于:2022-04-01 07:39:10

在[《JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用》](http://blog.csdn.net/xyang81/article/details/44657385)这篇文章中详细介绍了在JNI中三种引用的使用方式,区别、应用场景和开发注意事项。由于都是理论,看完之后可能印象不够深刻,由其是在开发当中容易出错的地方。所以这篇文章用一个例子说明引用使用不当会造成的问题,以引起大家对这个知识点的重视。 首先创建一个Android工程,在主界面放一个文本框和一个按钮,文本框用于接收创建局部引用的数量N,点击按钮后会获取文本框中的数量,然后调用native方法在本地代码中创建一个长度为N的字符串数组,再返回到Java层,并输出到控制台中。 ### 界面如下: ![界面](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c947c1c52.jpg "") activity_main.xml如下所示: ~~~ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal" android:padding="5dip" > <EditText android:id="@+id/str_count" android:layout_width="0dip" android:layout_height="wrap_content" android:layout_weight="1" android:inputType="numberDecimal" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_toRightOf="@id/str_count" android:onClick="onTestLocalRefOverflow" android:text="局部引用表溢出测试" /> </LinearLayout> ~~~ ### 在MainActivity中声明native方法和初始化View ~~~ package com.example.jni; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.EditText; public class MainActivity extends Activity { // 返回count个sample相同的字符串数组,并用编号标识,如:sample1,sample2... public native String[] getStrings(int count, String sample); EditText mEditText; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mEditText = (EditText) findViewById(R.id.str_count); } public void onTestLocalRefOverflow(View view) { String[] strings = getStrings(Integer.parseInt(mEditText.getText().toString()),"I Love You %d Year!!!"); for (String string : strings) { System.out.println(string); } } static { System.loadLibrary("local_ref_overflow_test"); } } ~~~ Java中的代码比较简单,MainActivity中声明了一个native方法getStrings,用于调用到本地函数,onTestLocalRefOverflow方法是主界面中按钮的点击事件,点击按钮后调用getStrings方法,传入字符串的数量和字符串内容,然后返回N个相同字符串长度的数组。 接下来,在工程下面创建一个jni目录,并分别创建Android.mk、Application.mk和local_ref_overflow_test.c文件,其中Android.mk是NDK编译系统自动编译和打包C/C++源代码的描述文件。Application.mk用于描述NDK编译时的一些参数选项,如:C/C++预编译宏、CPU架构等。(后续会开文章详细介绍)local_ref_overflow_test.c是实现MainActivity中getStrings本地方法的C代码。 Android.mk文件内容如下所示: ![JNI目录结构](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c947dc848.jpg "") ~~~ LOCAL_PATH := $(call my-dir) include $(CLEAR_VARS) #清除环境变量 LOCAL_MODULE := local_ref_overflow_test #so文件名称,不用加lib前缀和.so后缀 LOCAL_SRC_FILES := local_ref_overflow_test.c #C源文件 LOCAL_LDLIBS := -llog #链接日志模块 include $(BUILD_SHARED_LIBRARY) #将源文件编译成共享库 ~~~ Application.mk文件内容如下所示: ~~~ APP_ABI := armeabi armeabi-v7a #指定编译CPU架构类型 ~~~ **local_ref_overflow_test.c**文件内容如下所示: ~~~ #include <jni.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <android/log.h> #define LOG_TAG "MainActivity" #define LOG_I(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG, __VA_ARGS__) #define LOG_E(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #ifdef __cplusplus extern "C" { #endif jobjectArray getStrings(JNIEnv *env, jobject obj, jint count, jstring sample) { jobjectArray str_array = NULL; jclass cls_string = NULL; jmethodID mid_string_init; jobject obj_str = NULL; const char *c_str_sample = NULL; char buff[256]; int i; // 保证至少可以创建3个局部引用(str_array,cls_string,obj_str) if ((*env)->EnsureLocalCapacity(env, 3) != JNI_OK) { return NULL; } c_str_sample = (*env)->GetStringUTFChars(env, sample, NULL); if (c_str_sample == NULL) { return NULL; } cls_string = (*env)->FindClass(env, "java/lang/String"); if (cls_string == NULL) { return NULL; } // 获取String的构造方法 mid_string_init = (*env)->GetMethodID(env, cls_string, "<init>", "()V"); if (mid_string_init == NULL) { (*env)->DeleteLocalRef(env,cls_string); return NULL; } obj_str = (*env)->NewObject(env, cls_string, mid_string_init); if (obj_str == NULL) { (*env)->DeleteLocalRef(env,cls_string); return NULL; } // 创建一个字符串数组 str_array = (*env)->NewObjectArray(env, count, cls_string, obj_str); if (str_array == NULL) { (*env)->DeleteLocalRef(env,cls_string); (*env)->DeleteLocalRef(env,obj_str); return NULL; } // 给数组中每个元素赋值 for (i = 0; i < count; ++i) { memset(buff, 0, sizeof(buff)); // 初始一下缓冲区 sprintf(buff, c_str_sample,i); jstring newStr = (*env)->NewStringUTF(env, buff); (*env)->SetObjectArrayElement(env, str_array, i, newStr); } // 释放模板字符串所占的内存 (*env)->ReleaseStringUTFChars(env, sample, c_str_sample); // 释放局部引用所占用的资源 (*env)->DeleteLocalRef(env, cls_string); (*env)->DeleteLocalRef(env, obj_str); return str_array; } const JNINativeMethod g_methods[] = { {"getStrings", "(ILjava/lang/String;)[Ljava/lang/String;", (void*)getStrings} }; static jclass g_cls_MainActivity = NULL; JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { LOG_I("JNI_OnLoad method call begin"); JNIEnv* env = NULL; jclass cls = NULL; if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // 查找要加载的本地方法Class引用 cls = (*env)->FindClass(env, "com/example/jni/MainActivity"); if(cls == NULL) { return JNI_ERR; } // 将class的引用缓存到全局变量中 g_cls_MainActivity = (*env)->NewWeakGlobalRef(env, cls); (*env)->DeleteLocalRef(env, cls); // 手动删除局部引用是个好习惯 // 将java中的native方法与本地函数绑定 (*env)->RegisterNatives(env, g_cls_MainActivity, g_methods, sizeof(g_methods) / sizeof(g_methods[0])); LOG_I("JNI_OnLoad method call end"); return JNI_VERSION_1_6; } JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved) { LOG_I("JNI_OnUnload method call begin"); JNIEnv *env = NULL; if((*vm)->GetEnv(vm, (void**)&env, JNI_VERSION_1_6) != JNI_OK) { return; } (*env)->UnregisterNatives(env, g_cls_MainActivity); // so被卸载的时候解除注册 (*env)->DeleteWeakGlobalRef(env, g_cls_MainActivity); } #ifdef __cplusplus } #endif ~~~ 如果你是从之前的文章阅读过来的,上述本地代码有几个函数可能是没见过的。下面简单说明一下,后面会写文章详细介绍。其中`JNI_OnLoad`是在Java层调用System.loadLibrary方法加载共享库到虚拟机时的回调函数,在这里适合做一些初始化处理。`JNI_OnUnload`函数是在共享库被卸载的时候由虚拟机回调,适合做资源释放与内存回收的处理。第104行的`RegisterNatives`函数用于将本地函数与Java的native方法进行绑定。在本例中,没有按原来的方式用javah命令生成头文件的声明,而是用`RegisterNatives`函数将Java中的getStrings native方法与本地函数getStrings绑定在了一起。同样能实现函数查找的功能,而且效率更高。`JNINativeMethod`是一个数据结构,用于描述一个方法名称、函数签名和函数指针信息,用于绑定本地函数与Java native方法的映射关系。如下所示: ~~~ typedef struct { char *name; // 函数名称 char *signature; // 函数签名 void *fnPtr; // 函数指针 } JNINativeMethod; ~~~ 注意:`void *fnPtr`这个函数指针所指向的函数参数要注意,本地函数的第一个参数必须是JNIEnv*,**第二个参数**如果是实例方法则是jobject,静态方法则是jclass,后面的才是Java中native方法的参数。例如上例中MainActivity中声明的native方法getStrings:`public native String[] getStrings(int count, String sample);` 对应本地函数 `jobjectArray getStrings(JNIEnv *env, jobject obj, jint count, jstring sample)`。 `getStrings`的代码我就不详细介绍了,就是创建一个字符串数组的功能,之前的文章已经讲过很多次了。现在仔细阅读下这个函数的实现,看能不能找出哪个地会造成局部引用表溢出。如果现在就运行程序,并在文本框中输入大于501以上的值的话,就会看到因局部引用表溢出而崩溃的现象。如下图所示: ![JNI局部引用表溢出](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c947f173d.jpg "") 这时你可能会想到利用上篇文章学到的`EnsureLocalCapacity`或`PushLocalFrame/PopLocalFrame`接口来扩充局部引用的数量。例如,将第25行改成`if ((*env)->EnsureLocalCapacity(env, count + 3) != JNI_OK)`,保证在函数中可以创建count+3个数量的引用(这里的3是指str_array、cls_string和obj_str)。不过遗憾的是,`EnsureLocalCapacity`会试图申请指定数量的局部引用,但不一定会申请成功,因为局部引用是创建在栈中的,如果这个数量级的引用所申请的内存空间超出了栈的最大内存空间范围,就会造成内存溢出。结果如下图所示: ![内存溢出](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c94827c2c.jpg "") 所以在一个本地方法中,如果使用了大量的局部引用而没有及时释放的话,随时都有可能造成程序崩溃的现象。在上例中63行处,每遍历一次,都会创建一个新的字符串并返回指向这个字符串的局部引用,而在64行使用完之后,就没有管它了,从而造成创建较大数组的情况下,就会把局部引用表填满,造成引用表溢出。经测试,在Android中局部引用表默认最大容量是512个。这是虚拟机实现的,在程序中应该没办法修改这个数量。看到这,我想你应该知道怎么修正这个问题了吧。是的,直接在64行将字符串设置到数组元素中后,调用`DeleteLocalRef`删除即可。修改后的代码如下所示: ~~~ // 给数组中每个元素赋值 for (i = 0; i < count; ++i) { memset(buff, 0, sizeof(buff)); // 初始一下缓冲区 sprintf(buff, c_str_sample,i); jstring newStr = (*env)->NewStringUTF(env, buff); (*env)->SetObjectArrayElement(env, str_array, i, newStr); (*env)->DeleteLocalRef(env,newStr); // Warning: 这里如果不手动释放局部引用,很有可能造成局部引用表溢出 } ~~~ 修改完之后,你创建多大的字符串数组都没有问题了。当然不能超过物理内存的大小啦!因为Java中的创建的对象所分配的内存全都存储在堆空间。下面创建50万个长度的字符串数组来验证下,如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9485bcc3.jpg "") Demo GIT下载地址:git@code.csdn.net:xyang81/jnilocalrefoverflowtest.git
';

JNI/NDK开发指南(十)——JNI局部引用、全局引用和弱全局引用

最后更新于:2022-04-01 07:39:08

这篇文章比较偏理论,详细介绍了在编写本地代码时三种引用的使用场景和注意事项。可能看起来有点枯燥,但引用是在JNI中最容易出错的一个点,如果使用不当,容易使程序造成内存溢出,程序崩溃等现象。所以讲得比较细,有些地方看起来可能比较啰嗦,还请轻啪![《Android JNI局部引用表溢出:local reference table overflow (max=512)》](http://blog.csdn.net/xyang81/article/details/44873769)这篇文章是一个JNI引用使用不当造成引用表溢出,最终导致程序崩溃的例子。建议看完这篇文章之后,再去看。 做Java的朋友都知道,在编码的过程当中,内存管理这一块完全是透明的。new一个类的实例时,只知道创建完这个类的实例之后,会返回这个实例的一个引用,然后就可以拿着这个引用访问它的所有数据成员了(属性、方法)。完全不用管JVM内部是怎么实现的,如何为新创建的对象来申请内存,也不用管对象使用完之后内存是怎么释放的,只需知道有一个垃圾回器在帮忙管理这些事情就OK的了。有经验的朋友也许知道启动一个Java程序,如果没有手动创建其它线程,默认会有两个线程在跑,一个是main线程,另一个就是GC线程(负责将一些不再使用的对象回收)。如果你曾经是做Java的然后转去做C++,会感觉很“蛋疼”,在C++中new一个对象,使用完了还要做一次delete操作,malloc一次同样也要调用free来释放相应的内存,否则你的程序就会有内存泄露了。而且在C/C++中内存还分栈空间和堆空间,其中局部变量、函数形参变量、for中定义的临时变量所分配的内存空间都是存放在栈空间(而且还要注意大小的限制),用new和malloc申请的内存都存放在堆空间。。。但C/C++里的内存管理还远远不止这些,这些只是最基础的内存管理常识。做Java的童鞋听到这些肯定会偷乐了,咱写Java的时候这些都不用管,全都交给GC就万事无优了。手动管理内存虽然麻烦,而且需要特别细心,一不小心就有可能造成内存泄露和野指针访问等程序致命的问题,但凡事都有利弊,手动申请和释放内存对程序的掌握比较灵活,不会受到平台的限制。比如我们写Android程序的时候,内存使用就受Dalivk虚拟机的限制,从最初版本的16~24M,到后来的32M到64M,可能随着以后移动设备物理内存的不大扩大,后面的Android版本内存限制可能也会随着提高。但在C/C++这层,就完全不受虚拟机的限制了。比如要在Android中要存储一张超高清的图片,刚好这张图片的大小超过了Dalivk虚拟机对每个应用的内存大小限制,Java此时就显得无能为力了,但在C/C++看来就是小菜一碟了,malloc(1024*1024*50),要多少内存,您说个数。。。C/C++程序员得意的说道~~Java不是说是一门纯面象对象的语言吗,所以除了基本数据类型外,其它任何类型所创建的对象,JVM所申请的内存都存在堆空间。上面提高到了GC,是负责回收不再使用的对象,它的全称是Garbage Collection,也就是所谓的垃圾回收。JVM会在适当的时机触发GC操作,一旦进行GC操作,就会将一些不再使用的对象进行回收。那么哪些对象会被认为是不再使用,并且可以被回收的呢?我们来看下面二张图:(注:图摘自博主郭霖的[《Android最佳性能实践(二)——分析内存的使用情况》](http://blog.csdn.net/guolin_blog/article/details/42238633)) ![对象之间的引用关系](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9477224a.jpg "") 上图当中,每个蓝色的圆圈就代表一个内存当中的对象,而圆圈之间的箭头就是它们的引用关系。这些对象有些是处于活动状态的,而有些就已经不再被使用了。那么GC操作会从一个叫作Roots的对象开始检查,所有它可以访问到的对象就说明还在使用当中,应该进行保留,而其它的对象就表示已经不再被使用了,如下图所示: ![GC释放没有使用对象的原理](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c947ab300.jpg "") 可以看到,目前所有黄色的对象都处于活动状态,仍然会被系统继续保留,而蓝色的对象就会在GC操作当中被系统回收掉了,这就是JVM执行一次GC的简单流程。 上面说的废话好像有点多哈,下面进入正题。通过上面的讨论,大家都知道,如果一个Java对象没有被其它成员变量或静态变量所引用的话,就随时有可能会被GC回收掉。所以我们在编写本地代码时,要注意从JVM中获取到的引用在使用时被GC回收的可能性。由于本地代码不能直接通过引用操作JVM内部的数据结构,要进行这些操作必须调用相应的JNI接口来间接操作所引用的数据结构。JNI提供了和Java相对应的引用类型,供本地代码配合JNI接口间接操作JVM内部的数据内容使用。如:jobject、jstring、jclass、jarray、jintArray等。因为我们只通过JNI接口操作JNI提供的引用类型数据结构,而且每个JVM都实现了JNI规范相应的接口,所以我们不必担心特定JVM中对象的存储方式和内部数据结构等信息,我们只需要学习JNI中三种不同的引用即可。 > 由于Java程序运行在虚拟机中的这个特点,在Java中创建的对象、定义的变量和方法,内部对象的数据结构是怎么定义的,只有JVM自己知道。如果我们在C/C++中想要访问Java中对象的属性和方法时,是不能够直接操作JVM内部Java对象的数据结构的。想要在C/C++中正确的访问Java的数据结构,JVM就必须有一套规则来约束C/C++与Java互相访问的机制,所以才有了JNI规范,JNI规范定义了一系列接口,任何实现了这套JNI接口的Java虚拟机,C/C++就可以通过调用这一系列接口来间接的访问Java中的数据结构。比如前面文章中学习到的常用JNI接口有:GetStringUTFChars(从Java虚拟机中获取一个字符串)、ReleaseStringUTFChars(释放从JVM中获取字符串所分配的内存空间)、NewStringUTF、GetArrayLength、GetFieldID、GetMethodID、FindClass等。 ## 三种引用简介及区别     在JNI规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用(Weak Global Reference)。区别如下: 1、**局部引用:**通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放。`(*env)->DeleteLocalRef(env,local_ref)` ~~~ jclass cls_string = (*env)->FindClass(env, "java/lang/String"); jcharArray charArr = (*env)->NewCharArray(env, len); jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray); jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建 ... ~~~ 2、**全局引用:**调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放`(*env)->DeleteGlobalRef(env,g_cls_string);` ~~~ static jclass g_cls_string; void TestFunc(JNIEnv* env, jobject obj) { jclass cls_string = (*env)->FindClass(env, "java/lang/String"); g_cls_string = (*env)->NewGlobalRef(env,cls_string); } ~~~ 3、 **弱全局引用:**调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。`(*env)->DeleteWeakGlobalRef(env,g_cls_string)` ~~~ static jclass g_cls_string; void TestFunc(JNIEnv* env, jobject obj) { jclass cls_string = (*env)->FindClass(env, "java/lang/String"); g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string); } ~~~ ## 局部引用 局部引用也称本地引用,通常是在函数中创建并使用。会阻止GC回收所引用的对象。比如,调用NewObject接口创建一个新的对象实例并返回一个对这个对象的局部引用。局部引用只有在创建它的本地方法返回前有效,***本地方法返回到Java层之后,如果Java层没有对返回的局部引用使用的话***,局部引用就会被JVM自动释放。你可能会为了提高程序的性能,在函数中将局部引用存储在静态变量中缓存起来,供下次调用时使用。这种方式是错误的,因为函数返回后局部引很可能马上就会被释放掉,静态变量中存储的就是一个被释放后的内存地址,成了一个野针对,下次再使用的时候就会造成非法地址的访问,使程序崩溃。请看下面一个例子,错误的缓存了String的Class引用: ~~~ /*错误的局部引用*/ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len) { jcharArray elemArray; jchar *chars = NULL; jstring j_str = NULL; static jclass cls_string = NULL; static jmethodID cid_string = NULL; // 注意:错误的引用缓存 if (cls_string == NULL) { cls_string = (*env)->FindClass(env, "java/lang/String"); if (cls_string == NULL) { return NULL; } } // 缓存String的构造方法ID if (cid_string == NULL) { cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V"); if (cid_string == NULL) { return NULL; } } //省略额外的代码....... elemArray = (*env)->NewCharArray(env, len); // .... j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray); // 释放局部引用 (*env)->DeleteLocalRef(env, elemArray); return j_str; } ~~~ 上面代码中,我们省略了和我们讨论无关的代码。因为FindClass返回一个对java.lang.String对象的局部引用,上面代码中缓存cls_string做法是错误的。假设一个本地方法C.f调用了newString: ~~~ JNIEXPORT jstring JNICALL Java_C_f(JNIEnv *env, jobject this) { char *c_str = ...; ... return newString(c_str); } ~~~ > Java_com_study_jnilearn_AccessCache_newString 下面简称newString C.f方法返回后,JVM会释放在这个方法执行期间创建的所有局部引用,也包含对String的Class引用cls_string。当再次调用newString时,newString所指向引用的内存空间已经被释放,成为了一个野指针,再访问这个指针的引用时,会导致因非法的内存访问造成程序崩溃。 ~~~ ... ... = C.f(); // 第一次调是OK的 ... = C.f(); // 第二次调用时,访问的是一个无效的引用. ... ~~~ ### 释放局部引用 释放一个局部引用有两种方式,一个是本地方法执行完毕后JVM自动释放,另外一个是自己调用DeleteLocalRef手动释放。既然JVM会在函数返回后会自动释放所有局部引用,为什么还需要手动释放呢?大部分情况下,我们在实现一个本地方法时不必担心局部引用的释放问题,函数被调用完成后,JVM 会自动释放函数中创建的所有局部引用。尽管如此,以下几种情况下,为了避免内存溢出,我们应该手动释放局部引用: 1、JNI会将创建的局部引用都存储在一个局部引用表中,如果这个表超过了最大容量限制,就会造成局部引用表溢出,使程序崩溃。经测试,Android上的JNI局部引用表最大数量是512个。当我们在实现一个本地方法时,可能需要创建大量的局部引用,如果没有及时释放,就有可能导致JNI局部引用表的溢出,所以,在不需要局部引用时就立即调用DeleteLocalRef手动删除。比如,在下面的代码中,本地代码遍历一个特别大的字符串数组,每遍历一个元素,都会创建一个局部引用,当对使用完这个元素的局部引用时,就应该马上手动释放它。 ~~~ for (i = 0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* 使用jstr */ (*env)->DeleteLocalRef(env, jstr); // 使用完成之后马上释放 } ~~~ 2、在编写JNI工具函数时,工具函数在程序当中是公用的,被谁调用你是不知道的。上面newString这个函数演示了怎么样在工具函数中使用完局部引用后,调用DeleteLocalRef删除。不这样做的话,每次调用newString之后,都会遗留两个引用占用空间(elemArray和cls_string,cls_string不用static缓存的情况下)。 3、如果你的本地函数不会返回。比如一个接收消息的函数,里面有一个死循环,用于等待别人发送消息过来`while(true) { if (有新的消息) { 处理之。。。。} else { 等待新的消息。。。}}`。如果在消息循环当中创建的引用你不显示删除,很快将会造成JVM局部引用表溢出。 4、局部引用会阻止所引用的对象被GC回收。比如你写的一个本地函数中刚开始需要访问一个大对象,因此一开始就创建了一个对这个对象的引用,但在函数返回前会有一个大量的非常复杂的计算过程,而在这个计算过程当中是不需要前面创建的那个大对象的引用的。但是,在计算的过程当中,如果这个大对象的引用还没有被释放的话,会阻止GC回收这个对象,内存一直占用者,造成资源的浪费。所以这种情况下,在进行复杂计算之前就应该把引用给释放了,以免不必要的资源浪费。 ~~~ /* 假如这是一个本地方法实现 */ JNIEXPORT void JNICALL Java_pkg_Cls_func(JNIEnv *env, jobject this) { lref = ... /* lref引用的是一个大的Java对象 */ ... /* 在这里已经处理完业务逻辑后,这个对象已经使用完了 */ (*env)->DeleteLocalRef(env, lref); /* 及时删除这个对这个大对象的引用,GC就可以对它回收,并释放相应的资源*/ lengthyComputation(); /* 在里有个比较耗时的计算过程 */ return; /* 计算完成之后,函数返回之前所有引用都已经释放 */ } ~~~ ### 管理局部引用 JNI提供了一系列函数来管理局部引用的生命周期。这些函数包括:EnsureLocalCapacity、NewLocalRef、PushLocalFrame、PopLocalFrame、DeleteLocalRef。JNI规范指出,任何实现JNI规范的JVM,必须确保每个本地函数至少可以创建**16个局部引用**(可以理解为虚拟机默认支持创建16个局部引用)。实际经验表明,这个数量已经满足大多数不需要和JVM中内部对象有太多交互的本地方函数。如果需要创建更多的引用,可以通过调用EnsureLocalCapacity函数,确保在当前线程中创建指定数量的局部引用,如果创建成功则返回0,否则创建失败,并抛出OutOfMemoryError异常。EnsureLocalCapacity这个函数是1.2以上版本才提供的,为了向下兼容,在编译的时候,如果申请创建的局部引用超过了本地引用的最大容量,在运行时JVM会调用FatalError函数使程序强制退出。在开发过程当中,可以为JVM添加-verbose:jni参数,在编译的时如果发现本地代码在试图申请过多的引用时,会打印警告信息提示我们要注意。在下面的代码中,遍历数组时会获取每个元素的引用,使用完了之后不手动删除,不考虑内存因素的情况下,它可以为这种创建大量的局部引用提供足够的空间。由于没有及时删除局部引用,因此在函数执行期间,会消耗更多的内存。 ~~~ /*处理函数逻辑时,确保函数能创建len个局部引用*/ if((*env)->EnsureLocalCapacity(env,len) != 0) { ... /*申请len个局部引用的内存空间失败 OutOfMemoryError*/ return; } for(i=0; i < len; i++) { jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); // ... 使用jstr字符串 /*这里没有删除在for中临时创建的局部引用*/ } ~~~ 另外,除了EnsureLocalCapacity函数可以扩充指定容量的局部引用数量外,我们也可以利用Push/PopLocalFrame函数对创建作用范围层层嵌套的局部引用。例如,我们把上面那段处理字符串数组的代码用Push/PopLocalFrame函数对重写: ~~~ #define N_REFS ... /*最大局部引用数量*/ for (i = 0; i < len; i++) { if ((*env)->PushLocalFrame(env, N_REFS) != 0) { ... /*内存溢出*/ } jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* 使用jstr */ (*env)->PopLocalFrame(env, NULL); } ~~~ PushLocalFrame为当前函数中需要用到的局部引用创建了一个引用堆栈,(如果之前调用PushLocalFrame已经创建了Frame,在当前的本地引用栈中仍然是有效的)每遍历一次调用`(*env)->GetObjectArrayElement(env, arr, i);`返回一个局部引用时,JVM会自动将该引用压入当前局部引用栈中。而PopLocalFrame负责销毁栈中所有的引用。这样一来,Push/PopLocalFrame函数对提供了对局部引用生命周期更方便的管理,而不需要时刻关注获取一个引用后,再调用DeleteLocalRef来释放引用。在上面的例子中,如果在处理jstr的过程当中**又**创建了局部引用,则PopLocalFrame执行时,这些局部引用将全都会被销毁。在调用PopLocalFrame销毁当前frame中的所有引用前,如果第二个参数result不为空,会由result生成一个新的局部引用,再把这个新生成的局部引用存储在上一个frame中。请看下面的示例: ~~~ // 函数原型 jobject (JNICALL *PopLocalFrame)(JNIEnv *env, jobject result); jstring other_jstr; for (i = 0; i < len; i++) { if ((*env)->PushLocalFrame(env, N_REFS) != 0) { ... /*内存溢出*/ } jstring jstr = (*env)->GetObjectArrayElement(env, arr, i); ... /* 使用jstr */ if (i == 2) { other_jstr = jstr; } other_jstr = (*env)->PopLocalFrame(env, other_jstr); // 销毁局部引用栈前返回指定的引用 } ~~~ 还要注意的一个问题是,局部引用不能跨线程使用,只在创建它的线程有效。不要试图在一个线程中创建局部引用并存储到全局引用中,然后在另外一个线程中使用。 ## 全局引用 全局引用可以跨方法、跨线程使用,直到它被手动释放才会失效。同局部引用一样,也会阻止它所引用的对象被GC回收。与局部引用创建方式不同的是,只能通过NewGlobalRef函数创建。下面这个版本的newString演示怎么样使用一个全局引用: ~~~ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len) { // ... jstring jstr = NULL; static jclass cls_string = NULL; if (cls_string == NULL) { jclass local_cls_string = (*env)->FindClass(env, "java/lang/String"); if (cls_string == NULL) { return NULL; } // 将java.lang.String类的Class引用缓存到全局引用当中 cls_string = (*env)->NewGlobalRef(env, local_cls_string); // 删除局部引用 (*env)->DeleteLocalRef(env, local_cls_string); // 再次验证全局引用是否创建成功 if (cls_string == NULL) { return NULL; } } // .... return jstr; } ~~~ ## 弱全局引用 弱全局引用使用`NewGlobalWeakRef`创建,使用`DeleteGlobalWeakRef`释放。下面简称弱引用。与全局引用类似,弱引用可以跨方法、线程使用。**但与全局引用很重要不同的一点是,弱引用不会阻止GC回收它引用的对象。**在newString这个函数中,我们也可以使用弱引用来存储String的Class引用,因为java.lang.String这个类是系统类,永远不会被GC回收。当本地代码中缓存的引用不一定要阻止GC回收它所指向的对象时,弱引用就是一个最好的选择。假设,一个本地方法mypkg.MyCls.f需要缓存一个指向类mypkg.MyCls2的引用,如果在弱引用中缓存的话,仍然允许mypkg.MyCls2这个类被unload,因为弱引用不会阻止GC回收所引用的对象。请看下面的代码段: ~~~ JNIEXPORT void JNICALL Java_mypkg_MyCls_f(JNIEnv *env, jobject self) { static jclass myCls2 = NULL; if (myCls2 == NULL) { jclass myCls2Local = (*env)->FindClass(env, "mypkg/MyCls2"); if (myCls2Local == NULL) { return; /* 没有找到mypkg/MyCls2这个类 */ } myCls2 = NewWeakGlobalRef(env, myCls2Local); if (myCls2 == NULL) { return; /* 内存溢出 */ } } ... /* 使用myCls2的引用 */ } ~~~ 我们假设MyCls和MyCls2有相同的生命周期(例如,他们可能被相同的类加载器加载),因为弱引用的存在,我们不必担心MyCls和它所在的本地代码在被使用时,MyCls2这个类出现先被unload,后来又会preload的情况。当然,如果真的发生这种情况时(MyCls和MyCls2此时的生命周期不同),我们在使用弱引用时,必须先检查缓存过的弱引用是指向活动的类对象,还是指向一个已经被GC给unload的类对象。下面马上告诉你怎样检查弱引用是否活动,即引用的比较。 ## 引用比较 给定两个引用(不管是全局、局部还是弱全局引用),我们只需要调用IsSameObject来判断它们两个是否指向相同的对象。例如:`(*env)->IsSameObject(env, obj1, obj2)`,如果obj1和obj2指向相同的对象,则返回JNI_TRUE(或者1),否则返回JNI_FALSE(或者0)。有一个特殊的引用需要注意:NULL,JNI中的NULL引用指向JVM中的null对象。如果obj是一个局部或全局引用,使用`(*env)->IsSameObject(env, obj, NULL)` 或者 obj == NULL 来判断obj是否指向一个null对象即可。但需要注意的是,IsSameObject用于弱全局引用与NULL比较时,返回值的意义是不同于局部引用和全局引用的: ~~~ jobject local_obj_ref = (*env)->NewObject(env, xxx_cls,xxx_mid); jobject g_obj_ref = (*env)->NewWeakGlobalRef(env, local_ref); // ... 业务逻辑处理 jboolean isEqual = (*env)->IsSameObject(env, g_obj_ref, NULL); ~~~ 在上面的IsSameObject调用中,**如果g_obj_ref指向的引用已经被回收,会返回JNI_TRUE,如果wobj仍然指向一个活动对象,会返回JNI_FALSE。** ## 释放全局引用 > 每一个JNI引用被建立时,除了它所指向的JVM中对象的引用需要占用一定的内存空间外,引用本身也会消耗掉一个数量的内存空间。作为一个优秀的程序员,我们应该对程序在一个给定的时间段内使用的引用数量要十分小心。短时间内创建大量而没有被立即回收的引用很可能就会导致内存溢出。     当我们的本地代码不再需要一个全局引用时,应该马上调用DeleteGlobalRef来释放它。如果不手动调用这个函数,即使这个对象已经没用了,JVM也不会回收这个全局引用所指向的对象。     同样,当我们的本地代码不再需要一个弱全局引用时,也应该调用DeleteWeakGlobalRef来释放它,如果不手动调用这个函数来释放所指向的对象,JVM仍会回收弱引用所指向的对象,但弱引用本身在引用表中所占的内存永远也不会被回收。 ## 管理引用的规则 前面对三种引用已做了一个全面的介绍,下面来总结一下引用的管理规则和使用时的一些注意事项,使用好引用的目的就是为了减少内存使用和对象被引用保持而不能释放,造成内存浪费。所以在开发当中要特别小心! 通常情况下,有两种本地代码使用引用时要注意: 1、 直接实现Java层声明的native函数的本地代码 当编写这类本地代码时,要当心不要造成全局引用和弱引用的累加,因为本地方法执行完毕后,这两种引用不会被自动释放。 2、被用在任何环境下的工具函数。例如:方法调用、属性访问和异常处理的工具函数等。 编写工具函数的本地代码时,要当心不要在函数的调用轨迹上遗漏任何的局部引用,因为工具函数被调用的场合和次数是不确定的,一量被大量调用,就很有可能造成内存溢出。所以在编写工具函数时,请遵守下面的规则: 1> 一个返回值为基本类型的工具函数被调用时,它决不能造成局部、全局、弱全局引用被回收的累加 2> 当一个返回值为引用类型的工具函数被调用时,它除了返回的引用以外,它决不能造成其它局部、全局、弱引用的累加 对于工具函数来说,为了使用缓存技术而创建一些全局引用或者弱全局引用是正常的。如果一个工具函数返回的是一个引用,我们应该写好注释详细说明返回引用的类型,以便于使用者更好的管理它们。下面的代码中,频繁地调用工具函数GetInfoString,我们需要知道GetInfoString返回引用的类型是什么,以便于每次使用完成后调用相应的JNI函数来释放掉它。 ~~~ while (JNI_TRUE) { jstring infoString = GetInfoString(info); ... /* 处理infoString */ ??? /* 使用完成之后,调用DeleteLocalRef、DeleteGlobalRef、DeleteWeakGlobalRef哪一个函数来释放这个引用呢?*/ } ~~~ 函数NewLocalRef有时被用来确保一个工具函数返回一个局部引用。我们改造一下newString这个函数,演示一下这个函数的用法。下面的newString是把一个被频繁调用的字符串“CommonString”缓存在了全局引用里: ~~~ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString { static jstring result; /* 使用wstrncmp函数比较两个Unicode字符串 */ if (wstrncmp("CommonString", chars, len) == 0) { /* 将"CommonString"这个字符串缓存到全局引用中 */ static jstring cachedString = NULL; if (cachedString == NULL) { /* 先创建"CommonString"这个字符串 */ jstring cachedStringLocal = ...; /* 然后将这个字符串缓存到全局引用中 */ cachedString = (*env)->NewGlobalRef(env, cachedStringLocal); } // 基于全局引用创建一个局引用返回,也同样会阻止GC回收所引用的这个对象,因为它们指向的是同一个对象 return (*env)->NewLocalRef(env, cachedString); } ... return result; } ~~~ 在管理局部引用的生命周期中,Push/PopLocalFrame是非常方便且安全的。我们可以在本地函数的入口处调用PushLocalFrame,然后在出口处调用PopLocalFrame,这样的话,在函数内任何位置创建的局部引用都会被释放。而且,这两个函数是非常高效的,强烈建议使用它们。需要注意的是,如果在函数的入口处调用了PushLocalFrame,记住要在函数所有出口(有return语句出现的地方)都要调用PopLocalFrame。在下面的代码中,对PushLocalFrame的调用只有一次,但调用PopLocalFrame确有多次,当然你也可以使用goto语句来统一处理。 ~~~ jobject f(JNIEnv *env, ...) { jobject result; if ((*env)->PushLocalFrame(env, 10) < 0) { /* 调用PushLocalFrame获取10个局部引用失败,不需要调用PopLocalFrame */ return NULL; } ... result = ...; // 创建局部引用result if (...) { /* 返回前先弹出栈顶的frame */ result = (*env)->PopLocalFrame(env, result); return result; } ... result = (*env)->PopLocalFrame(env, result); /* 正常返回 */ return result; } ~~~ 上面的代码同样演示了函数PopLocalFrame的第二个参数的用法,局部引用result一开始在PushLocalFrame创建在当前frame里面,而把result传入PopLocalFrame中时,PopLocalFrame在弹出当前的frame前,会由result生成一个新的局部引用,再将这个新生成的局部引用存储在上一个frame当中。
';

JNI/NDK开发指南(九)——JNI调用性能测试及优化

最后更新于:2022-04-01 07:39:05

在前面几章我们学习到了,在Java中声明一个native方法,然后生成本地接口的函数原型声明,再用C/C++实现这些函数,并生成对应平台的动态共享库放到Java程序的类路径下,最后在Java程序中调用声明的native方法就间接的调用到了C/C++编写的函数了,在C/C++中写的程序可以避开JVM的内存开销过大的限制、处理高性能的计算、调用系统服务等功能。同时也学习到了在本地代码中通过JNI提供的接口,调用Java程序中的任意方法和对象的属性。这是JNI提供的一些优势。但做过Java的童鞋应该都明白,Java程序是运行在JVM上的,所以在Java中调用C/C++或其它语言这种跨语言的接口时,或者说在C/C++代码中通过JNI接口访问Java中对象的方法或属性时,相比Java调用自已的方法,性能是非常低的!!!网上有朋友针对**Java调用本地接口,Java调Java方法**做了一次详细的测试,来充分说明在享受JNI给程序带来优势的同时,也要接受其所带来的性能开销,下面请看一组测试数据: ## Java调用JNI空函数与Java调用Java空方法性能测试 测试环境:JDK1.4.2_19、JDK1.5.0_04和JDK1.6.0_14,测试的重复次数都是一亿次。测试结果的绝对数值意义不大,仅供参考。因为根据JVM和机器性能的不同,测试所产生的数值也会不同,但不管什么机器和JVM应该都能反应同一个问题,Java调用native接口,要比Java调用Java方法性能要低很多。 **Java调用Java空方法的性能:** | JDK版本 | Java调Java耗时 | 平均每秒调用次数 | |-----|-----|-----| | 1.6 | 329ms | 303951367次 | | 1.5 | 312ms | 320512820次 | | 1.4 | 312ms | 27233115次 | **Java调用JNI空函数的性能:** | JDK版本 | Java调JNI耗时 | 平均每秒调用次数 | |-----|-----|-----| | 1.6 | 1531ms | 65316786次 | | 1.5 | 1891ms | 52882072次 | | 1.4 | 3672ms | 27233115次 | 从上述测试数据可以看出JDK版本越高,JNI调用的性能也越好。在JDK1.5中,仅仅是空方法调用,JNI的性能就要比Java内部调用慢将近5倍,而在JDK1.4下更是慢了十多倍。 ## JNI查找方法ID、字段ID、Class引用性能测试 当我们在本地代码中要访问Java对象的字段或调用它们的方法时,本机代码必须调用FindClass()、GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID()。对于 GetFieldID()、GetStaticFieldID、GetMethodID() 和 GetStaticMethodID(),为特定类返回的 ID 不会在 JVM 进程的生存期内发生变化。但是,获取字段或方法的调用有时会需要在 JVM 中完成大量工作,因为字段和方法可能是从超类中继承而来的,这会让 JVM 向上遍历类层次结构来找到它们。由于 ID 对于特定类是相同的,因此只需要查找一次,然后便可重复使用。同样,查找类对象的开销也很大,因此也应该缓存它们。下面对调用JNI接口FindClass查找Class、GetFieldID获取类的字段ID和GetFieldValue获取字段的值的性能做的一个测试。**缓存**表示只调用一次,**不缓存**就是每次都调用相应的JNI接口: **java.version = 1.6.0_14** JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 79172 ms 平均每秒 : 1263072 JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 25015 ms 平均每秒 : 3997601 JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 50765 ms 平均每秒 : 1969861 JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 2125 ms 平均每秒 : 47058823 **java.version = 1.5.0_04** JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 87109 ms 平均每秒 : 1147987 JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 32031 ms 平均每秒 : 3121975 JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 51657 ms 平均每秒 : 1935846 JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 2187 ms 平均每秒 : 45724737 **java.version = 1.4.2_19** JNI 字段读取 (缓存Class=false ,缓存字段ID=false) 耗时 : 97500 ms 平均每秒 : 1025641 JNI 字段读取 (缓存Class=true ,缓存字段ID=false) 耗时 : 38110 ms 平均每秒 : 2623983 JNI 字段读取 (缓存Class=false ,缓存字段ID=true) 耗时 : 55204 ms 平均每秒 : 1811462 JNI 字段读取 (缓存Class=true ,缓存字段ID=true) 耗时 : 4187 ms 平均每秒 : 23883448 根据上面的测试数据得知,查找class和ID(属性和方法ID)消耗的时间比较大。只是读取字段值的时间基本上跟上面的JNI空方法是一个数量级。而如果每次都根据名称查找class和field的话,性能要下降高达**40倍**。读取一个字段值的性能在百万级上,在交互频繁的JNI应用中是不能忍受的。 消耗时间最多的就是查找class,因此在native里保存class和member id是很有必要的。class和member id在一定范围内是稳定的,但在动态加载的class loader下,保存全局的class要么可能失效,要么可能造成无法卸载classloader,在诸如OSGI框架下的JNI应用还要特别注意这方面的问题。在读取字段值和查找FieldID上,JDK1.4和1.5、1.6的差距是非常明显的。但在最耗时的查找class上,三个版本没有明显差距。 通过上面的测试可以明显的看出,在调用JNI接口获取方法ID、字段ID和Class引用时,如果没用使用缓存的话,性能低至4倍。所以在JNI开发中,合理的使用缓存技术能给程序提高极大的性能。缓存有两种,分别为使用时缓存和类静态初始化时缓存,区别主要在于缓存发生的时刻。 ## 使用时缓存 字段ID、方法ID和Class引用在函数当中使用的同时就缓存起来。下面看一个示例: ~~~ package com.study.jnilearn; public class AccessCache { private String str = "Hello"; public native void accessField(); // 访问str成员变量 public native String newString(char[] chars, int len); // 根据字符数组和指定长度创建String对象 public static void main(String[] args) { AccessCache accessCache = new AccessCache(); accessCache.nativeMethod(); char chars[] = new char[7]; chars[0] = '中'; chars[1] = '华'; chars[2] = '人'; chars[3] = '民'; chars[4] = '共'; chars[5] = '和'; chars[6] = '国'; String str = accessCache.newString(chars, 6); System.out.println(str); } static { System.loadLibrary("AccessCache"); } } ~~~ **javah生成的头文件:com_study_jnilearn_AccessCache.h** ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessCache */ #ifndef _Included_com_study_jnilearn_AccessCache #define _Included_com_study_jnilearn_AccessCache #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessCache * Method: accessField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField(JNIEnv *, jobject); /* * Class: com_study_jnilearn_AccessCache * Method: newString * Signature: ([CI)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString(JNIEnv *, jobject, jcharArray, jint); #ifdef __cplusplus } #endif #endif ~~~ **实现头文件中的函数:AccessCache.c** ~~~ // AccessCache.c #include "com_study_jnilearn_AccessCache.h" JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_accessField (JNIEnv *env, jobject obj) { // 第一次访问时将字段存到内存数据区,直到程序结束才会释放,可以起到缓存的作用 static jfieldID fid_str = NULL; jclass cls_AccessCache; jstring j_str; const char *c_str; cls_AccessCache = (*env)->GetObjectClass(env, obj); // 获取该对象的Class引用 if (cls_AccessCache == NULL) { return; } // 先判断字段ID之前是否已经缓存过,如果已经缓存过则不进行查找 if (fid_str == NULL) { fid_str = (*env)->GetFieldID(env,cls_AccessCache,"str","Ljava/lang/String;"); // 再次判断是否找到该类的str字段 if (fid_str == NULL) { return; } } j_str = (*env)->GetObjectField(env, obj, fid_str); // 获取字段的值 c_str = (*env)->GetStringUTFChars(env, j_str, NULL); if (c_str == NULL) { return; // 内存不够 } printf("In C:\n str = \"%s\"\n", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); // 释放从从JVM新分配字符串的内存空间 // 修改字段的值 j_str = (*env)->NewStringUTF(env, "12345"); if (j_str == NULL) { return; } (*env)->SetObjectField(env, obj, fid_str, j_str); // 释放本地引用 (*env)->DeleteLocalRef(env,cls_AccessCache); (*env)->DeleteLocalRef(env,j_str); } JNIEXPORT jstring JNICALL Java_com_study_jnilearn_AccessCache_newString (JNIEnv *env, jobject obj, jcharArray j_char_arr, jint len) { jcharArray elemArray; jchar *chars = NULL; jstring j_str = NULL; static jclass cls_string = NULL; static jmethodID cid_string = NULL; // 注意:这里缓存局引用的做法是错误,这里做为一个反面教材提醒大家,下面会说到。 if (cls_string == NULL) { cls_string = (*env)->FindClass(env, "java/lang/String"); if (cls_string == NULL) { return NULL; } } // 缓存String的构造方法ID if (cid_string == NULL) { cid_string = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V"); if (cid_string == NULL) { return NULL; } } printf("In C array Len: %d\n", len); // 创建一个字符数组 elemArray = (*env)->NewCharArray(env, len); if (elemArray == NULL) { return NULL; } // 获取数组的指针引用,注意:不能直接将jcharArray作为SetCharArrayRegion函数最后一个参数 chars = (*env)->GetCharArrayElements(env, j_char_arr,NULL); if (chars == NULL) { return NULL; } // 将Java字符数组中的内容复制指定长度到新的字符数组中 (*env)->SetCharArrayRegion(env, elemArray, 0, len, chars); // 调用String对象的构造方法,创建一个指定字符数组为内容的String对象 j_str = (*env)->NewObject(env, cls_string, cid_string, elemArray); // 释放本地引用 (*env)->DeleteLocalRef(env, elemArray); return j_str; } ~~~ 例1、在Java_com_study_jnilearn_AccessCache_accessField函数中的第8行定义了一个静态变量fid_str用于存储字段的ID,每次调用函数的时候,在第18行先判断字段ID是否已经缓存,如果没有先取出来存到fid_str中,下次再调用的时候该变量已经有值了,不用再去JVM中获取,起到了缓存的作用。 例2、在Java_com_study_jnilearn_AccessCache_newString函数中的53和54行定义了两个变量cls_string和cid_string,分别用于存储java.lang.String类的Class引用和String的构造方法ID。在56行和64行处,使用前会先判断是否已经缓存过,如果没有则调用JNI的接口从JVM中获取String的Class引用和构造方法ID存储到静态变量当中。下次再调用该函数时就可以直接使用,不需要再去找一次了,也达到了缓存的效果,大家第一反映都会这么认为。但是请注意:cls_string是一个局部引用,与方法和字段ID不一样,局部引用在函数结束后会被VM自动释放掉,这时cls_string成为了一个野针对(指向的内存空间已被释放,但变量的值仍然是被释放后的内存地址,不为NULL),当下次再调用Java_com_xxxx_newString这个函数的时候,会试图访问一个无效的局部引用,从而导致非法的内存访问造成程序崩溃。所以在函数内用static缓存局部引用这种方式是错误的。下篇文章会介绍局部引用和全局引用,利用全局引用来防止这种问题,请关注。 ## 类静态初始化缓存 在调用一个类的方法或属性之前,Java虚拟机会先检查该类是否已经加载到内存当中,如果没有则会先加载,然后紧接着会调用该类的静态初始化代码块,所以在静态初始化该类的过程当中计算并缓存该类当中的字段ID和方法ID也是个不错的选择。下面看一个示例: ~~~ package com.study.jnilearn; public class AccessCache { public static native void initIDs(); public native void nativeMethod(); public void callback() { System.out.println("AccessCache.callback invoked!"); } public static void main(String[] args) { AccessCache accessCache = new AccessCache(); accessCache.nativeMethod(); } static { System.loadLibrary("AccessCache"); initIDs(); } } ~~~ ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessCache */ #ifndef _Included_com_study_jnilearn_AccessCache #define _Included_com_study_jnilearn_AccessCache #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessCache * Method: initIDs * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs (JNIEnv *, jclass); /* * Class: com_study_jnilearn_AccessCache * Method: nativeMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod (JNIEnv *, jobject); #ifdef __cplusplus } #endif #endif ~~~ ~~~ // AccessCache.c #include "com_study_jnilearn_AccessCache.h" jmethodID MID_AccessCache_callback; JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_initIDs (JNIEnv *env, jclass cls) { printf("initIDs called!!!\n"); MID_AccessCache_callback = (*env)->GetMethodID(env,cls,"callback","()V"); } JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessCache_nativeMethod (JNIEnv *env, jobject obj) { printf("In C Java_com_study_jnilearn_AccessCache_nativeMethod called!!!\n"); (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback); } ~~~ JVM加载AccessCache.class到内存当中之后,会调用该类的静态初始化代码块,即static代码块,先调用System.loadLibrary加载动态库到JVM中,紧接着调用native方法initIDs,会调用用到本地函数Java_com_study_jnilearn_AccessCache_initIDs,在该函数中获取需要缓存的ID,然后存入全局变量当中。下次需要用到这些ID的时候,直接使用全局变量当中的即可,如18行当中调用Java的callback函数。 ~~~ (*env)->CallVoidMethod(env, obj, MID_AccessCache_callback); ~~~ ## 两种缓存方式比较 如果在写JNI接口时,不能控制方法和字段所在类的源码的话,用使用时缓存比较合理。但比起类静态初始化时缓存来说,用使用时缓存有一些缺点: 1. 使用前,每次都需要检查是否已经缓存该ID或Class引用 2. 如果在用使用时缓存的ID,要注意只要本地代码依赖于这个ID的值,那么这个类就不会被unload。另外一方面,如果缓存发生在静态初始化时,当类被unload或reload时,ID会被重新计算。因为,尽量在类静态初始化时就缓存字段ID、方法ID和类的Class引用。
';

JNI/NDK开发指南(八)——调用构造方法和父类实例方法

最后更新于:2022-04-01 07:39:03

在第6章我们学习到了在Native层如何调用Java静态方法和实例方法,其中调用实例方法的示例代码中也提到了调用构造函数来实始化一个对象,但没有详细介绍,一带而过了。还没有阅读过的同学请移步《[JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法](http://blog.csdn.net/xyang81/article/details/42582213)》阅读。这章详细来介绍下初始一个对象的两种方式,以及如何调用子类对象重写的父类实例方法。 我们先回过一下,在Java中实例化一个对象和调用父类实例方法的流程。先看一段代码: ~~~ package com.study.jnilearn; public class Animal { public void run() { System.out.println("Animal.run..."); } } package com.study.jnilearn; public class Cat extends Animal { @Override public void run() { System.out.println(name + " Cat.run..."); } } public static void main(String[] args) { Animal cat = new Cat("汤姆"); cat.run(); } ~~~ 正如你所看到的那样,上面这段代码非常简单,有两个类Animal和Cat,Animal类中定义了run和getName两个方法,Cat继承自Animal,并重写了父类的run方法。在main方法中,首先定义了一个Animal类型的变量cat,并指向了Cat类的实例对象,然后调用了它的run方法。***在执行new Cat(“汤姆”)这段代码时,会先为Cat类分配内存空间(所分配的内存空间大小由Cat类的成员变量数量决定),然后调用Cat的带参构造方法初始化对象。***cat是Animal类型,但它指向的是Cat实例对象的引用,而且Cat重写了父类的run方法,因为调用run方法时有多态存在,所以访问的是Cat的run而非Animal的run,运行后打印的结果为:***汤姆 Cat.run…*** 如果要调用父类的run方法,只需在Cat的run方法中调用super.run()即可,相当的简单。 > 写过C或C++的同学应该都有一个很深刻的内存管理概念,**栈空间和堆空间**,栈空间的内存大小受操作系统限制,由操作系统自动来管理,速度较快,所以在函数中定义的局部变量、函数形参变量都存储在栈空间。操作系统没有限制堆空间的内存大小,只受物理内存的限制,内存需要程序员自己管理。在C语言中用malloc关键字动态分配的内存和在C++中用new创建的对象所分配内存都存储在堆空间,内存使用完之后分别用free或delete/delete[]释放。*这里不过多的讨论C/C++内存管理方面的知识,有兴趣的同学请自行百度。*做Java的童鞋众所周知,写Java程序是不需要手动来管理内存的,内存管理那些烦锁的事情全都交由一个叫GC的线程来管理(当一个对象没有被其它对象所引用时,该对象就会被GC释放)。但我觉得Java内部的内存管理原理和C/C++是非常相似的,上例中,Animal cat = new Cat(“汤姆”); 局部变量cat存放在栈空间上,new Cat(“汤姆”);创建的实例对象存放在堆空间,返回一个内存地址的引用,存储在cat变量中。这样就可以通过cat变量所指向的引用访问Cat实例当中所有可见的成员了。 所以创建一个对象分为2步: 1. 为对象分配内存空间 2. 初始化对象(调用对象的构造方法) 下面通过一个示例来了解在JNI中是如何调用对象构造方法和父类实例方法的。为了让示例能清晰的体现构造方法和父类实例方法的调用流程,定义了Animal和Cat两个类,Animal定义了一个String形参的构造方法,一个成员变量name、两个成员函数run和getName,Cat继承自Animal,并重写了run方法。在JNI中实现创建Cat对象的实例,调用Animal类的run和getName方法。代码如下所示: ~~~ // Animal.java package com.study.jnilearn; public class Animal { protected String name; public Animal(String name) { this.name = name; System.out.println("Animal Construct call..."); } public String getName() { System.out.println("Animal.getName Call..."); return this.name; } public void run() { System.out.println("Animal.run..."); } } // Cat.java package com.study.jnilearn; public class Cat extends Animal { public Cat(String name) { super(name); System.out.println("Cat Construct call...."); } @Override public String getName() { return "My name is " + this.name; } @Override public void run() { System.out.println(name + " Cat.run..."); } } // AccessSuperMethod.java package com.study.jnilearn; public class AccessSuperMethod { public native static void callSuperInstanceMethod(); public static void main(String[] args) { callSuperInstanceMethod(); } static { System.loadLibrary("AccessSuperMethod"); } } ~~~ AccessSuperMethod类是程序的入口,其中定义了一个native方法callSuperInstanceMethod。用javah生成的jni函数原型如下: ~~~ /* Header for class com_study_jnilearn_AccessSuperMethod */ #ifndef _Included_com_study_jnilearn_AccessSuperMethod #define _Included_com_study_jnilearn_AccessSuperMethod #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessSuperMethod * Method: callSuperInstanceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ~~~ 实现Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod函数,如下所示: ~~~ // AccessSuperMethod.c #include "com_study_jnilearn_AccessSuperMethod.h" JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessSuperMethod_callSuperInstanceMethod (JNIEnv *env, jclass cls) { jclass cls_cat; jclass cls_animal; jmethodID mid_cat_init; jmethodID mid_run; jmethodID mid_getName; jstring c_str_name; jobject obj_cat; const char *name = NULL; // 1、获取Cat类的class引用 cls_cat = (*env)->FindClass(env, "com/study/jnilearn/Cat"); if (cls_cat == NULL) { return; } // 2、获取Cat的构造方法ID(构造方法的名统一为:<init>) mid_cat_init = (*env)->GetMethodID(env, cls_cat, "<init>", "(Ljava/lang/String;)V"); if (mid_cat_init == NULL) { return; // 没有找到只有一个参数为String的构造方法 } // 3、创建一个String对象,作为构造方法的参数 c_str_name = (*env)->NewStringUTF(env, "汤姆猫"); if (c_str_name == NULL) { return; // 创建字符串失败(内存不够) } // 4、创建Cat对象的实例(调用对象的构造方法并初始化对象) obj_cat = (*env)->NewObject(env,cls_cat, mid_cat_init,c_str_name); if (obj_cat == NULL) { return; } //-------------- 5、调用Cat父类Animal的run和getName方法 -------------- cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) { return; } // 例1: 调用父类的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 获取父类Animal中run方法的id if (mid_run == NULL) { return; } // 注意:obj_cat是Cat的实例,cls_animal是Animal的Class引用,mid_run是Animal类中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run); // 例2:调用父类的getName方法 // 获取父类Animal中getName方法的id mid_getName = (*env)->GetMethodID(env, cls_animal, "getName", "()Ljava/lang/String;"); if (mid_getName == NULL) { return; } c_str_name = (*env)->CallNonvirtualObjectMethod(env, obj_cat, cls_animal, mid_getName); name = (*env)->GetStringUTFChars(env, c_str_name, NULL); printf("In C: Animal Name is %s\n", name); // 释放从java层获取到的字符串所分配的内存 (*env)->ReleaseStringUTFChars(env, c_str_name, name); quit: // 删除局部引用(jobject或jobject的子类才属于引用变量),允许VM释放被局部变量所引用的资源 (*env)->DeleteLocalRef(env, cls_cat); (*env)->DeleteLocalRef(env, cls_animal); (*env)->DeleteLocalRef(env, c_str_name); (*env)->DeleteLocalRef(env, obj_cat); } ~~~ ### 运行结果: ![这里写图片描述](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c94754c8b.jpg "") ### 代码讲解 - 调用构造方法 调用构造方法和调用对象的实例方法方式是相似的,传入”< init >”作为方法名查找类的构造方法ID,然后调用JNI函数NewObject调用对象的构造函数初始化对象。如下代码所示: ~~~ obj_cat = (*env)->NewObject(env,cls_cat,mid_cat_init,c_str_name); ~~~ 上述这段代码调用了JNI函数NewObject创建了Class引用的一个实例对象。这个函数做了2件事情,1> 创建一个未初始化的对象并分配内存空间 2> 调用对象的构造函数初始化对象。这两步也可以分开进行,为对象分配内存,然后再初始化对象,如下代码所示: ~~~ // 1、创建一个未初始化的对象,并分配内存 obj_cat = (*env)->AllocObject(env, cls_cat); if (obj_cat) { // 2、调用对象的构造函数初始化对象 (*env)->CallNonvirtualVoidMethod(env,obj_cat, cls_cat, mid_cat_init, c_str_name); if ((*env)->ExceptionCheck(env)) { // 检查异常 goto quit; } } ~~~ AllocObject函数创建的是一个未初始化的对象,后面在用这个对象之前,必须调用CallNonvirtualVoidMethod调用对象的构造函数初始化该对象。而且在使用时一定要非常小心,确保在一个对象上面,构造函数最多被调用一次。有时,先创建一个初始化的对象,然后在合适的时间再调用构造函数的方式是很有用的。尽管如此,大部分情况下,应该使用 NewObject,尽量避免使用容易出错的AllocObject/CallNonvirtualVoidMethod函数。 ### 代码讲解 - 调用父类实例方法 如果一个方法被定义在父类中,在子类中被覆盖,也可以调用父类中的这个实例方法。JNI 提供了一系列函数CallNonvirtualXXXMethod来支持调用各种返回值类型的实例方法。调用一个定义在父类中的实例方法,须遵循下面的步骤: 1.使用GetMethodID函数从一个指向父类的Class引用当中获取方法ID ~~~ cls_animal = (*env)->FindClass(env, "com/study/jnilearn/Animal"); if (cls_animal == NULL) { return; } //例1: 调用父类的run方法 mid_run = (*env)->GetMethodID(env, cls_animal, "run", "()V"); // 获取父类Animal中run方法的id if (mid_run == NULL) { return; } ~~~ 2.传入子类对象、父类Class引用、父类方法 ID 和参数,并调用 CallNonvirtualVoidMethod、 CallNonvirtualBooleanMethod、CallNonvirtualIntMethod等一系列函数中的一个。其中CallNonvirtualVoidMethod 也可以被用来调用父类的构造函数。 ~~~ // 注意:obj_cat是Cat的实例,cls_animal是Animal的Class引用,mid_run是Animal类中的方法ID (*env)->CallNonvirtualVoidMethod(env, obj_cat, cls_animal, mid_run); ~~~ 其实在开发当中,这种调用父类实例方法的情况是很少遇到的,通常在 JAVA 中可以很简单地做到: super.func();但有些特殊需求也可能会用到,所以知道有这么回事还是很有必要的。 *[示例代码下载地址:https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)*
';

JNI/NDK开发指南(七)——C/C++访问Java实例变量和静态变量

最后更新于:2022-04-01 07:39:01

在上一章中我们学习到了如何在本地代码中访问任意Java类中的静态方法和实例方法,本章我们也通过一个示例来学习Java中的实例变量和静态变量,在本地代码中如何来访问和修改。静态变量也称为类变量(属性),在所有实例对象中共享同一份数据,可以直接通过【类名.变量名】来访问。实例变量也称为成员变量(属性),每个实例都拥有一份实例变量数据的拷贝,它们之间修改后的数据互不影响。下面看一个例子: ~~~ package com.study.jnilearn; /** * C/C++访问类的实例变量和静态变量 * @author yangxin */ public class AccessField { private native static void accessInstanceField(ClassField obj); private native static void accessStaticField(); public static void main(String[] args) { ClassField obj = new ClassField(); obj.setNum(10); obj.setStr("Hello"); // 本地代码访问和修改ClassField为中的静态属性num accessStaticField(); accessInstanceField(obj); // 输出本地代码修改过后的值 System.out.println("In Java--->ClassField.num = " + obj.getNum()); System.out.println("In Java--->ClassField.str = " + obj.getStr()); } static { System.loadLibrary("AccessField"); } } ~~~ AccessField是程序的入口类,定义了两个native方法:accessInstanceField和accessStaticField,分别用于演示在本地代码中访问Java类中的实例变量和静态变量。其中accessInstaceField方法访问的是类的实例变量,所以该方法需要一个ClassField实例作为形参,用于访问该对象中的实例变量。 ~~~ package com.study.jnilearn; /** * ClassField.java * 用于本地代码访问和修改该类的属性 * @author yangxin * */ public class ClassField { private static int num; private String str; public int getNum() { return num; } public void setNum(int num) { ClassField.num = num; } public String getStr() { return str; } public void setStr(String str) { this.str = str; } } ~~~ 在本例中没有将实例变量和静态变量定义在程序入口类中,新建了一个ClassField的类来定义类的属性,目的是为了加深在C/C++代码中可以访问任意Java类中的属性。在这个类中定义了一个int类型的实例变量num,和一个java.lang.String类型的静态变量str。这两个变量会被本地代码访问和修改。 ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessField */ #ifndef _Included_com_study_jnilearn_AccessField #define _Included_com_study_jnilearn_AccessField #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessField * Method: accessInstanceField * Signature: (Lcom/study/jnilearn/ClassField;)V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessInstanceField (JNIEnv *, jclass, jobject); /* * Class: com_study_jnilearn_AccessField * Method: accessStaticField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessStaticField (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ~~~ 以上代码是程序入口类AccessField.class为native方法生成的本地代码函数原型头文件 ~~~ // AccessField.c #include "com_study_jnilearn_AccessField.h" /* * Class: com_study_jnilearn_AccessField * Method: accessInstanceField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessInstanceField (JNIEnv *env, jclass cls, jobject obj) { jclass clazz; jfieldID fid; jstring j_str; jstring j_newStr; const char *c_str = NULL; // 1.获取AccessField类的Class引用 clazz = (*env)->GetObjectClass(env,obj); if (clazz == NULL) { return; } // 2. 获取AccessField类实例变量str的属性ID fid = (*env)->GetFieldID(env,clazz,"str", "Ljava/lang/String;"); if (clazz == NULL) { return; } // 3. 获取实例变量str的值 j_str = (jstring)(*env)->GetObjectField(env,obj,fid); // 4. 将unicode编码的java字符串转换成C风格字符串 c_str = (*env)->GetStringUTFChars(env,j_str,NULL); if (c_str == NULL) { return; } printf("In C--->ClassField.str = %s\n", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); // 5. 修改实例变量str的值 j_newStr = (*env)->NewStringUTF(env, "This is C String"); if (j_newStr == NULL) { return; } (*env)->SetObjectField(env, obj, fid, j_newStr); // 6.删除局部引用 (*env)->DeleteLocalRef(env, clazz); (*env)->DeleteLocalRef(env, j_str); (*env)->DeleteLocalRef(env, j_newStr); } /* * Class: com_study_jnilearn_AccessField * Method: accessStaticField * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessField_accessStaticField (JNIEnv *env, jclass cls) { jclass clazz; jfieldID fid; jint num; //1.获取ClassField类的Class引用 clazz = (*env)->FindClass(env,"com/study/jnilearn/ClassField"); if (clazz == NULL) { // 错误处理 return; } //2.获取ClassField类静态变量num的属性ID fid = (*env)->GetStaticFieldID(env, clazz, "num", "I"); if (fid == NULL) { return; } // 3.获取静态变量num的值 num = (*env)->GetStaticIntField(env,clazz,fid); printf("In C--->ClassField.num = %d\n", num); // 4.修改静态变量num的值 (*env)->SetStaticIntField(env, clazz, fid, 80); // 删除属部引用 (*env)->DeleteLocalRef(env,clazz); } ~~~ 以上代码是对头文件中函数原型的实现。 *** *** **运行程序,输出结果如下:** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9473b468.jpg) **代码解析:** **一、访问实例变量** 在main方法中,通过调用accessInstanceField()方法来调用本地函数Java_com_study_jnilearn_AccessField_accessInstanceField,定位到函数32行: ~~~ j_str = (jstring)(*env)->GetObjectField(env,obj,fid); ~~~ 该函数就是用于获取ClassField对象中num的值。下面是函数的原型: ~~~ jobject (JNICALL *GetObjectField) (JNIEnv *env, jobject obj, jfieldID fieldID); ~~~ 因为实例变量str是String类型,属于引用类型。在JNI中获取引用类型字段的值,调用GetObjectField函数获取。同样的,获取其它类型字段值的函数还有GetIntField,GetFloatField,GetDoubleField,GetBooleanField等。这些函数有一个共同点,函数参数都是一样的,只是函数名不同,我们只需学习其中一个函数如何调用即可,依次类推,就自然知道其它函数的使用方法。 GetObjectField函数接受3个参数,env是JNI函数表指针,obj是实例变量所属的对象,fieldID是变量的ID(也称为属性描述符或签名),和上一章中方法描述符是同一个意思。env和obj参数从Java_com_study_jnilearn_AccessField_accessInstanceField函数形参列表中可以得到,那fieldID怎么获取呢?了解Java反射的童鞋应该知道,在Java中任何一个类的.class字节码文件被加载到内存中之后,该class子节码文件统一使用Class类来表示该类的一个引用(相当于Java中所有类的基类是Object一样)。然后就可以从该类的Class引用中动态的获取类中的任意方法和属性。注意:Class类在Java SDK继承体系中是一个独立的类,没有继承自Object。请看下面的例子,通过Java反射机制,动态的获取一个类的私有实例变量的值: ~~~ public static void main(String[] args) throws Exception { ClassField obj = new ClassField(); obj.setStr("YangXin"); // 获取ClassField字节码对象的Class引用 Class<?> clazz = obj.getClass(); // 获取str属性 Field field = clazz.getDeclaredField("str"); // 取消权限检查,因为Java语法规定,非public属性是无法在外部访问的 field.setAccessible(true); // 获取obj对象中的str属性的值 String str = (String)field.get(obj); System.out.println("str = " + str); } ~~~ 运行程序后,输出结果当然是打印出str属性的值“YangXin”。所以我们在本地代码中调用JNI函数访问Java对象中某一个属性的时候,首先第一步就是要获取该对象的Class引用,然后在Class中查找需要访问的字段ID,最后调用JNI函数的GetXXXField系列函数,获取字段(属性)的值。上例中,首先调用GetObjectClass函数获取ClassField的Class引用: ~~~ clazz = (*env)->GetObjectClass(env,obj); ~~~ 然后调用GetFieldID函数从Class引用中获取字段的ID(str是字段名,Ljava/lang/String;是字段的类型) ~~~ fid = (*env)->GetFieldID(env,clazz,"str", "Ljava/lang/String;"); ~~~ 最后调用GetObjectField函数,传入实例对象和字段ID,获取属性的值 ~~~ j_str = (jstring)(*env)->GetObjectField(env,obj,fid); ~~~ 调用SetXXXField系列函数,可以修改实例属性的值,最后一个参数为属性的值。引用类型全部调用SetObjectField函数,基本类型调用SetIntField、SetDoubleField、SetBooleanField等 ~~~ (*env)->SetObjectField(env, obj, fid, j_newStr); ~~~ **二、访问静态变量** 访问静态变量和实例变量不同的是,获取字段ID使用GetStaticFieldID,获取和修改字段的值使用Get/SetStaticXXXField系列函数,比如上例中获取和修改静态变量num: ~~~ // 3.获取静态变量num的值 num = (*env)->GetStaticIntField(env,clazz,fid); // 4.修改静态变量num的值 (*env)->SetStaticIntField(env, clazz, fid, 80); ~~~ **总结:** 1、由于JNI函数是直接操作JVM中的数据结构,不受Java访问修饰符的限制。即,在本地代码中可以调用JNI函数可以访问Java对象中的非public属性和方法 **2、访问和修改实例变量操作步聚:**  1>、调用GetObjectClass函数获取实例对象的Class引用  2>、调用GetFieldID函数获取Class引用中某个实例变量的ID  3>、调用GetXXXField函数获取变量的值,需要传入实例变量所属对象和变量ID  4>、调用SetXXXField函数修改变量的值,需要传入实例变量所属对象、变量ID和变量的值 **3、访问和修改静态变量操作步聚:**  1>、调用FindClass函数获取类的Class引用  2>、调用GetStaticFieldID函数获取Class引用中某个静态变量ID  3>、调用GetStaticXXXField函数获取静态变量的值,需要传入变量所属Class的引用和变量ID  4>、调用SetStaticXXXField函数设置静态变量的值,需要传入变量所属Class的引用、变量ID和变量的值 示例代码下载地址:[https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)
';

JNI/NDK开发指南(六)——C/C++访问Java实例方法和静态方法

最后更新于:2022-04-01 07:38:58

通过前面5章的学习,我们知道了如何通过JNI函数来访问JVM中的基本数据类型、字符串和数组这些数据类型。下一步我们来学习本地代码如何与JVM中任意对象的**属性**和**方法**进行交互。比如本地代码调用Java层某个对象的方法或属性,也就是通常我们所说的来自C/C++层本地函数的callback(回调)。这个知识点分2篇文章分别介绍,本篇先介绍方法回调,在第七章中介绍本地代码访问Java的属性。 在这之前,先回顾一下在Java中调用一个方法时在JVM中的实现原理,有助于下面讲解本地代码调用Java方法实现的机制。写过Java的童鞋都知道,调用一个类的静态方法,直接通过 **类名.方法**就可以调用。这也太简单了,有什么好讲的呢。。。但在这个调用过程中,JVM是帮我们做了很多工作的。当我们在运行一个Java程序时,JVM会先将程序运行时所要用到所有**相关的class**文件加载到JVM中,并采用按需加载的方式加载,也就是说某个类只有在被用到的时候才会被加载,这样设计的目的也是为了提高程序的性能和节约内存。所以我们在用类名调用一个静态方法之前,JVM首先会判断该类是否已经加载,如果没有被**ClassLoader**加载到JVM中,JVM会从classpath路径下查找该类,如果找到了,会将其加载到JVM中,然后才是调用该类的静态方法。如果没有找到,JVM会抛出java.lang.ClassNotFoundException异常,提示找不到这个类。ClassLoader是JVM加载class字节码文件的一种机制,不太了解的童鞋,请移步阅读[《深入分析Java ClassLoader原理》](http://blog.csdn.net/xyang81/article/details/7292380)一文。其实在JNI开发当中,本地代码也是按照上面的流程来访问类的静态方法或实例方法的,下面通过一个例子,详细介绍本地代码调用Java方法流程当中的每个步聚: ~~~ package com.study.jnilearn; /** * AccessMethod.java * 本地代码访问类的实例方法和静态方法 * @author yangxin */ public class AccessMethod { public static native void callJavaStaticMethod(); public static native void callJavaInstaceMethod(); public static void main(String[] args) { callJavaStaticMethod(); callJavaInstaceMethod(); } static { System.loadLibrary("AccessMethod"); } } ~~~ ~~~ package com.study.jnilearn; /** * ClassMethod.java * 用于本地代码调用 * @author yangxin */ public class ClassMethod { private static void callStaticMethod(String str, int i) { System.out.format("ClassMethod::callStaticMethod called!-->str=%s," + " i=%d\n", str, i); } private void callInstanceMethod(String str, int i) { System.out.format("ClassMethod::callInstanceMethod called!-->str=%s, " + "i=%d\n", str, i); } } ~~~ 由AccessMethod.class生成的头文件: ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_AccessMethod */ #ifndef _Included_com_study_jnilearn_AccessMethod #define _Included_com_study_jnilearn_AccessMethod #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaStaticMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *, jclass); /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaInstaceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *, jclass); #ifdef __cplusplus } #endif #endif ~~~ 本地代码对头文件中函数原型的实现: ~~~ // AccessMethod.c #include "com_study_jnilearn_AccessMethod.h" /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaStaticMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *env, jclass cls) { jclass clazz = NULL; jstring str_arg = NULL; jmethodID mid_static_method; // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象 clazz =(*env)->FindClass(env,"com/study/jnilearn/ClassMethod"); if (clazz == NULL) { return; } // 2、从clazz类中查找callStaticMethod方法 mid_static_method = (*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V"); if (mid_static_method == NULL) { printf("找不到callStaticMethod这个静态方法。"); return; } // 3、调用clazz类的callStaticMethod静态方法 str_arg = (*env)->NewStringUTF(env,"我是静态方法"); (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); // 删除局部引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,str_arg); } /* * Class: com_study_jnilearn_AccessMethod * Method: callJavaInstaceMethod * Signature: ()V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *env, jclass cls) { jclass clazz = NULL; jobject jobj = NULL; jmethodID mid_construct = NULL; jmethodID mid_instance = NULL; jstring str_arg = NULL; // 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象 clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod"); if (clazz == NULL) { printf("找不到'com.study.jnilearn.ClassMethod'这个类"); return; } // 2、获取类的默认构造方法ID mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V"); if (mid_construct == NULL) { printf("找不到默认的构造方法"); return; } // 3、查找实例方法的ID mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V"); if (mid_instance == NULL) { return; } // 4、创建该类的实例 jobj = (*env)->NewObject(env,clazz,mid_construct); if (jobj == NULL) { printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法"); return; } // 5、调用对象的实例方法 str_arg = (*env)->NewStringUTF(env,"我是实例方法"); (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); // 删除局部引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,jobj); (*env)->DeleteLocalRef(env,str_arg); } ~~~ **运行结果:** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c946e267c.jpg) **代码解析:** AccessMethod.java是程序的入口,在main方法中,分别调用了callJavaStaticMethod和callJavaInstaceMethod这两个native方法,用于测试native层调用**MethodClass.java中的**callStaticMethod静态方法和callInstanceMethod实例方法,这两个方法的返回值都为Void,参数都有两个,分别为String和int **一、callJavaStaticMethod静态方法实现说明** ~~~ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaStaticMethod (JNIEnv *env, jclass cls) ~~~ 定位到AccessMethod.c的31行: ~~~ (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); ~~~ CallStaticVoidMethod函数的原型如下: ~~~ void (JNICALL *CallStaticVoidMethod)(JNIEnv *env, jclass cls, jmethodID methodID, ...); ~~~ **该函数接收4个参数:** env:JNI函数表指针 cls:调用该静态方法的Class对象 methodID:方法ID(因为一个类中会存在多个方法,需要一个唯一标识来确定调用类中的哪个方法)  参数4:方法实参列表 **根据函数参数的提示,分以下四步完成Java静态方法的回调:** **第一步:**调用FindClass函数,传入一个Class描述符,JVM会从classpath路径下搜索该类,并返回jclass类型(用于存储Class对象的引用)。注意ClassMethod的Class描述符为com/study/jnilearn/ClassMethod,要将**.**(点)全部换成**/**(反斜杠) ~~~ (*env)->FindClass(env,"com/study/jnilearn/ClassMethod"); ~~~ **第二步:**调用GetStaticMethodID函数,从ClassMethod类中获取callStaticMethod方法ID,返回jmethodID类型(用于存储方法的引用)。实参clazz是第一步找到的jclass对象,实参"callStaticMethod"为方法名称,实参“(Ljava/lang/String;I)V”为方法的签名 ~~~ (*env)->GetStaticMethodID(env,clazz,"callStaticMethod","(Ljava/lang/String;I)V"); ~~~ **第三步:**调用CallStaticVoidMethod函数,执行ClassMethod.callStaticMethod方法调用。str_arg和100是callStaticMethod方法的实参。 ~~~ str_arg = (*env)->NewStringUTF(env,"我是静态方法"); (*env)->CallStaticVoidMethod(env,clazz,mid_static_method, str_arg, 100); ~~~ 注意:JVM针对所有数据类型的返回值都定义了相关的函数。上面callStaticMethod方法的返回类型为Void,所以调用CallStaticVoidMethod。根据返回值类型不同,JNI提供了一系列不同返回值的函数,如:CallStaticIntMethod、CallStaticFloatMethod、CallStaticShortMethod、CallStaticObjectMethod等,分别表示调用返回值为int、float、short、Object类型的函数,引用类型统一调用CallStaticObjectMethod函数。另外,每种返回值类型的函数都提供了接收3种实参类型的实现:CallStatic**XXX**Method(env, clazz, methodID, ...),CallStaticXXXMethodV(env, clazz, methodID, va_list args),CallStaticXXXMethodA(env, clazz, methodID, const jvalue *args),分别表示:接收可变参数列表、接收va_list作为实参和接收const jvalue*为实参。下面是jni.h头文件中CallStaticVoidMethod的三种实参的函数原型: ~~~ void (JNICALL *CallStaticVoidMethod) (JNIEnv *env, jclass cls, jmethodID methodID, ...); void (JNICALL *CallStaticVoidMethodV) (JNIEnv *env, jclass cls, jmethodID methodID, va_list args); void (JNICALL *CallStaticVoidMethodA) (JNIEnv *env, jclass cls, jmethodID methodID, const jvalue * args); ~~~ **第四步**、释放局部变量 ~~~ // 删除局部引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,str_arg); ~~~ 虽然函数结束后,JVM会自动释放所有局部引用变量所占的内存空间。但还是手动释放一下比较安全,因为在JVM中维护着一个引用表,用于存储局部和全局引用变量,经测试在Android NDK环境下,这个表的最大存储空间是512个引用,如果超过这个数就会造成引用表溢出,JVM崩溃。在PC环境下测试,不管申请多少局部引用也不释放都不会崩,我猜可能与JVM和Android Dalvik虚拟机实现方式不一样的原因。所以有申请就及时释放是一个好的习惯!*(局部引用和全局引用在后面的文章中会详细介绍)* **二、callInstanceMethod实例方法实现说明** ~~~ JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod (JNIEnv *env, jclass cls) ~~~ 定位到AccessMethod.c的43行: ~~~ (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); ~~~ CallVoidMethod函数的原型如下: ~~~ void (JNICALL *CallVoidMethod) (JNIEnv *env, jobject obj, jmethodID methodID, ...); ~~~ **该函数接收4个参数:** env:JNI函数表指针 obj:调用该方法的实例 methodID:方法ID  参数4:方法的实参列表 **根据函数参数的提示,分以下六步完成Java静态方法的回调:** **第一步、**同调用静态方法一样,首先通过FindClass函数获取 类的Class对象 **第二步、**获取类的构造方法ID,因为创建类的对象首先会调用类的构造方法。这里以默认构造方法为例 ~~~ (*env)->GetMethodID(env,clazz, "<init>","()V"); ~~~ <init>代表类的构造方法名称,()V代表无参无返回值的构造方法(即默认构造方法) **第三步、**调用GetMethodID获取callInstanceMethod的方法ID ~~~ (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V"); ~~~ **第四步、**调用NewObject函数,创建类的实例对象 ~~~ (*env)->NewObject(env,clazz,mid_construct); ~~~ **第五步、**调用CallVoidMethod函数,执行ClassMethod.callInstanceMethod方法调用,str_arg和200是方法实参 ~~~ str_arg = (*env)->NewStringUTF(env,"我是实例方法"); (*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200); ~~~ 同JNI调用Java静态方法一样,JVM针对所有数据类型的返回值都定义了相关的函数(CallXXXMethod),如:CallIntMethod、CallFloatMethod、CallObjectMethod等,也同样提供了支持三种类型实参的函数实现,以CallVoidMethod为例,如下是jni.h头文件中该函数的原型: ~~~ void (JNICALL *CallVoidMethod)(JNIEnv *env, jobject obj, jmethodID methodID, ...); void (JNICALL *CallVoidMethodV)(JNIEnv *env, jobject obj, jmethodID methodID, va_list args); void (JNICALL *CallVoidMethodA)(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue * args); ~~~ **第六步、**删除局部引用(从引用表中移除) ~~~ // 删除局部引用 (*env)->DeleteLocalRef(env,clazz); (*env)->DeleteLocalRef(env,jobj); (*env)->DeleteLocalRef(env,str_arg); ~~~ **三、方法签名** 在上面的的例子中,无论是调用静态方法还是实例方法,都必须传入一个jmethodID的参数。因为在Java中存在方法重载(方法名相同,参数列表不同),所以要明确告诉JVM调用的是类或实例中的哪一个方法。调用JNI的GetMethodID函数获取一个jmethodID时,需要传入一个方法名称和方法签名,方法名称就是在Java中定义的方法名,**方法签名的格式为**:**(形参参数类型列表)返回值**。形参参数列表中,引用类型以L开头,后面紧跟类的全路径名(需将.全部替换成/),以分号结尾。下面是一些示例: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9470a971.jpg) Java基本类型与方法签名中参数类型和返回值类型的映射关系如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c94724de1.jpg) 比如,String fun(int a, float b, boolean c, String d) 对应的JNI方法签名为:"(IFZLjava/lang/String;)Ljava/lang/String;" **总结:** 1、调用静态方法使用CallStaticXXXMethod/V/A函数,XXX代表返回值的数据类型。如:CallStaticIntMethod 2、调用实例方法使用CallXXXMethod/V/A函数,XXX代表返回的数据类型,如:CallIntMethod 3、获取一个实例方法的ID,使用GetMethodID函数,传入方法名称和方法签名 4、获以一个静态方法的ID,使用GetStaticMethodID函数,传入方法名称和方法签名 5、获取构造方法ID,方法名称使用"<init>" 6、获取一个类的Class实例,使用FindClass函数,传入类描述符。JVM会从classpath目录下开始搜索。 7、创建一个类的实例,使用NewObject函数,传入Class引用和构造方法ID 8、删除局部变量引用,使用DeleteLocalRef,传入引用变量 9、方法签名格式:(形参参数列表)返回值类型。注意:形参参数列表之间不需要用空格或其它字符分隔 10、类描述符格式:L包名路径/类名;,包名之间用/分隔。如:Ljava/lang/String; 11、调用GetMethodID获取方法ID和调用FindClass获取Class实例后,要做异常判断 示例代码下载地址:[https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)
';

JNI/NDK开发指南(五)——访问数组(基本类型数组与对象数组)

最后更新于:2022-04-01 07:38:56

JNI中的数组分为基本类型数组和对象数组,它们的处理方式是不一样的,基本类型数组中的所有元素都是JNI的基本数据类型,可以直接访问。而对象数组中的所有元素是一个类的实例或其它数组的引用,和字符串操作一样,不能直接访问Java传递给JNI层的数组,必须选择合适的JNI函数来访问和设置Java层的数组对象。阅读此文假设你已经了解了JNI与Java数据类型的映射关系,如果还不了解的童鞋,请移步《[JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系](http://blog.csdn.net/xyang81/article/details/42047899)》阅读。下面以int类型为例说明基本数据类型数组的访问方式,对象数组类型用一个创建二维数组的例子来演示如何访问: **一、访问基本类型数组** ~~~ package com.study.jnilearn; // 访问基本类型数组 public 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 < arr.length; i++) { arr[i] = i; } int sum = p.sumArray(arr); System.out.println("sum = " + sum); } static { System.loadLibrary("IntArray"); } } ~~~ **本地代码:** ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_IntArray */ #ifndef _Included_com_study_jnilearn_IntArray #define _Included_com_study_jnilearn_IntArray #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_IntArray * Method: sumArray * Signature: ([I)I */ JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray (JNIEnv *, jobject, jintArray); #ifdef __cplusplus } #endif #endif // IntArray.c #include "com_study_jnilearn_IntArray.h" #include <string.h> #include <stdlib.h> /* * Class: com_study_jnilearn_IntArray * Method: sumArray * Signature: ([I)I */ JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray (JNIEnv *env, jobject obj, jintArray j_array) { jint i, sum = 0; jint *c_array; jint arr_len; //1. 获取数组长度 arr_len = (*env)->GetArrayLength(env,j_array); //2. 根据数组长度和数组元素的数据类型申请存放java数组元素的缓冲区 c_array = (jint*)malloc(sizeof(jint) * arr_len); //3. 初始化缓冲区 memset(c_array,0,sizeof(jint)*arr_len); printf("arr_len = %d ", arr_len); //4. 拷贝Java数组中的所有元素到缓冲区中 (*env)->GetIntArrayRegion(env,j_array,0,arr_len,c_array); for (i = 0; i < arr_len; i++) { sum += c_array[i]; //5. 累加数组元素的和 } free(c_array); //6. 释放存储数组元素的缓冲区 return sum; } ~~~ 上例中,在Java中定义了一个sumArray的native方法,参数类型是int[],对应JNI中jintArray类型。在本地代码中,首先通过JNI的**GetArrayLength**函数获取数组的长度,已知数组是jintArray类型,可以得出数组的元素类型是jint,然后根据数组的长度和数组元素类型,申请相应大小的缓冲区。如果缓冲区不大的话,当然也可以直接在栈上申请内存,那样效率更高,但是没那么灵活,因为Java数组的大小变了,本地代码也跟着修改。接着调用**GetIntArrayRegion**函数将Java数组中的所有元素拷贝到C缓冲区中,并累加数组中所有元素的和,最后释放存储java数组元素的C缓冲区,并返回计算结果。GetIntArrayRegion函数第1个参数是JNIEnv函数指针,第2个参数是Java数组对象,第3个参数是拷贝数组的开始索引,第4个参数是拷贝数组的长度,第5个参数是拷贝目的地。下图是计算结果: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c946b03f8.jpg) 在前面的例子当中,我们通过调用GetIntArrayRegion函数,将int数组中的所有元素拷贝到C临时缓冲区中,然后在本地代码中访问缓冲区中的元素来实现求和的计算,JNI还提供了一个和GetIntArrayRegion相对应的函SetIntArrayRegion,本地代码可以通过这个函数来修改所有基本数据类型数组的元素。另外JNI还提供一系列直接获取数组元素指针的函数Get/Release<Type>ArrayElements,比如:GetIntArrayElements、ReleaseArrayElements、GetFloatArrayElements、ReleaseFloatArrayElements等。下面我们用这种方式重新实现计算数组元素的和: ~~~ JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray2 (JNIEnv *env, jobject obj, jintArray j_array) { jint i, sum = 0; jint *c_array; jint arr_len; // 可能数组中的元素在内存中是不连续的,JVM可能会复制所有原始数据到缓冲区,然后返回这个缓冲区的指针 c_array = (*env)->GetIntArrayElements(env,j_array,NULL); if (c_array == NULL) { return 0; // JVM复制原始数据到缓冲区失败 } arr_len = (*env)->GetArrayLength(env,j_array); printf("arr_len = %d\n", arr_len); for (i = 0; i < arr_len; i++) { sum += c_array[i]; } (*env)->ReleaseIntArrayElements(env,j_array, c_array, 0); // 释放可能复制的缓冲区 return sum; } ~~~ GetIntArrayElements第三个参数表示返回的数组指针是原始数组,还是拷贝原始数据到临时缓冲区的指针,如果是JNI_TRUE:表示临时缓冲区数组指针,JNI_FALSE:表示临时原始数组指针。开发当中,我们并不关心它从哪里返回的数组指针,这个参数填NULL即可,但在获取到的指针必须做校验,因为当原始数据在内存当中不是连续存放的情况下,JVM会复制所有原始数据到一个临时缓冲区,并返回这个临时缓冲区的指针。有可能在申请开辟临时缓冲区内存空间时,会内存不足导致申请失败,这时会返回NULL。   写过Java的程序员都知道,在Java中创建的对象全都由GC(垃圾回收器)自动回收,不需要像C/C++一样需要程序员自己管理内存。GC会实时扫描所有创建的对象是否还有引用,如果没有引用则会立即清理掉。当我们创建一个像int数组对象的时候,当我们在本地代码想去访问时,发现这个对象正被GC线程占用了,这时本地代码会一直处于阻塞状态,直到等待GC释放这个对象的锁之后才能继续访问。为了避免这种现象的发生,JNI提供了Get/ReleasePrimitiveArrayCritical这对函数,本地代码在访问数组对象时会暂停GC线程。不过使用这对函数也有个限制,在Get/ReleasePrimitiveArrayCritical这两个函数期间不能调用任何会让线程阻塞或等待JVM中其它线程的本地函数或JNI函数,和处理字符串的Get/ReleaseStringCritical函数限制一样。这对函数和GetIntArrayElements函数一样,返回的是数组元素的指针。下面用这种方式重新实现上例中的功能: ~~~ JNIEXPORT jint JNICALL Java_com_study_jnilearn_IntArray_sumArray (JNIEnv *env, jobject obj, jintArray j_array) { jint i, sum = 0; jint *c_array; jint arr_len; jboolean isCopy; c_array = (*env)->GetPrimitiveArrayCritical(env,j_array,&isCopy); printf("isCopy: %d \n", isCopy); if (c_array == NULL) { return 0; } arr_len = (*env)->GetArrayLength(env,j_array); printf("arr_len = %d\n", arr_len); for (i = 0; i < arr_len; i++) { sum += c_array[i]; } (*env)->ReleasePrimitiveArrayCritical(env, j_array, c_array, 0); return sum; } ~~~ **小结:** 1、对于小量的、固定大小的数组,应该选择**Get/SetArrayRegion**函数来操作数组元素是效率最高的。因为这对函数要求提前分配一个C临时缓冲区来存储数组元素,你可以直接在Stack(栈)上或用malloc在堆上来动态申请,当然在栈上申请是最快的。有童鞋可能会认为,访问数组元素还需要将原始数据全部拷贝一份到临时缓冲区才能访问而觉得效率低?我想告诉你的是,像这种复制少量数组元素的代价是很小的,几乎可以忽略。这对函数的另外一个优点就是,允许你传入一个开始索引和长度来实现对子数组元素的访问和操作(SetArrayRegion函数可以修改数组),不过传入的索引和长度不要越界,函数会进行检查,如果越界了会抛出ArrayIndexOutOfBoundsException异常。 2、如果不想预先分配C缓冲区,并且原始数组长度也不确定,而本地代码又不想在获取数组元素指针时被阻塞的话,使用Get/ReleasePrimitiveArrayCritical函数对,就像Get/ReleaseStringCritical函数对一样,使用这对函数要非常小心,以免死锁。 3、Get/Release<type>ArrayElements系列函数永远是安全的,JVM会选择性的返回一个指针,这个指针可能指向原始数据,也可能指向原始数据的复制。 **二、访问对象数组** JNI提供了两个函数来访问对象数组,GetObjectArrayElement返回数组中指定位置的元素,SetObjectArrayElement修改数组中指定位置的元素。与基本类型不同的是,我们不能一次得到数据中的所有对象元素或者一次复制多个对象元素到缓冲区。因为字符串和数组都是引用类型,只能通过Get/SetObjectArrayElement这样的JNI函数来访问字符串数组或者数组中的数组元素。下面的例子通过调用一个本地方法来创建一个二维的int数组,然后打印这个二维数组的内容: ~~~ package com.study.jnilearn; public class ObjectArray { private native int[][] initInt2DArray(int size); public static void main(String[] args) { ObjectArray obj = new ObjectArray(); int[][] arr = obj.initInt2DArray(3); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { System.out.format("arr[%d][%d] = %d\n", i, j, arr[i][j]); } } } static { System.loadLibrary("ObjectArray"); } } ~~~ **本地代码:** ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_ObjectArray */ #ifndef _Included_com_study_jnilearn_ObjectArray #define _Included_com_study_jnilearn_ObjectArray #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_ObjectArray * Method: initInt2DArray * Signature: (I)[[I */ JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray (JNIEnv *, jobject, jint); #ifdef __cplusplus } #endif #endif // ObjectArray.c #include "com_study_jnilearn_ObjectArray.h" /* * Class: com_study_jnilearn_ObjectArray * Method: initInt2DArray * Signature: (I)[[I */ JNIEXPORT jobjectArray JNICALL Java_com_study_jnilearn_ObjectArray_initInt2DArray (JNIEnv *env, jobject obj, jint size) { jobjectArray result; jclass clsIntArray; jint i,j; // 1.获得一个int型二维数组类的引用 clsIntArray = (*env)->FindClass(env,"[I"); if (clsIntArray == NULL) { return NULL; } // 2.创建一个数组对象(里面每个元素用clsIntArray表示) result = (*env)->NewObjectArray(env,size,clsIntArray,NULL); if (result == NULL) { return NULL; } // 3.为数组元素赋值 for (i = 0; i < size; ++i) { jint buff[256]; jintArray intArr = (*env)->NewIntArray(env,size); if (intArr == NULL) { return NULL; } for (j = 0; j < size; j++) { buff[j] = i + j; } (*env)->SetIntArrayRegion(env,intArr, 0,size,buff); (*env)->SetObjectArrayElement(env,result, i, intArr); (*env)->DeleteLocalRef(env,intArr); } return result; } ~~~ **结果:** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c946c57e8.jpg) 本地函数initInt2DArray首先调用JNI函数FindClass获得一个int型的二维数组类的引用,传递给FindClass的参数"[I"是JNI class descript(JNI类型描述符,后面为详细介绍),它对应着JVM中的int[]类型。如果int[]类加载失败的话,FindClass会返回NULL,然后抛出一个java.lang.NoClassDefFoundError: [I异常。 接下来,NewObjectArray创建一个新的数组,这个数组里面的元素类型用intArrCls(int[])类型来表示。函数NewObjectArray只能分配第一维,JVM没有与多维数组相对应的数据结构,JNI也没有提供类似的函数来创建二维数组。由于JNI中的二维数组直接操作的是JVM中的数据结构,相比JAVA和C/C++创建二维数组要复杂很多。给二维数组设置数据的方式也非常直接,首先用NewIntArray创建一个JNI的int数组,并为每个数组元素分配空间,然后用SetIntArrayRegion把buff[]缓冲中的内容复制到新分配的一维数组中去,最后在外层循环中依次将int[]数组赋值到jobjectArray数组中,一维数组中套一维数组,就形成了一个所谓的二维数组。 另外,为了避免在循环内创建大量的JNI局部引用,造成JNI引用表溢出,所以在外层循环中每次都要调用DeleteLocalRef将新创建的jintArray引用从引用表中移除。在JNI中,只有jobject以及子类属于引用变量,会占用引用表的空间,jint,jfloat,jboolean等都是基本类型变量,不会占用引用表空间,即不需要释放。引用表最大空间为512个,如果超出这个范围,JVM就会挂掉。 示例代码下载地址:[https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)
';

Android NDK开发Crash错误定位

最后更新于:2022-04-01 07:38:54

在Android开发中,程序Crash分三种情况:未捕获的异常、ANR(Application Not Responding)和闪退(NDK引发错误)。其中**未捕获的异常**根据logcat打印的堆栈信息很容易定位错误。**ANR**错误也好查,Android规定,应用与用户进行交互时,如果5秒内没有响应用户的操作,则会引发ANR错误,并弹出一个系统提示框,让用户选择继续等待或立即关闭程序。并会在/data/anr目录下生成一个traces.txt文件,记录系统产生anr异常的堆栈和线程信息。如果是**闪退**,这问题比较难查,通常是项目中用到了NDK引发某类致命的错误导致闪退。因为NDK是使用C/C++来进行开发,熟悉C/C++的程序员都知道,指针和内存管理是最重要也是最容易出问题的地方,稍有不慎就会遇到诸如内存地址访问错误、使用野针对、内存泄露、堆栈溢出、初始化错误、类型转换错误、数字除0等常见的问题,导致最后都是同一个结果:程序崩溃。不会像在Java层产生的异常时弹出“xxx程序无响应,是否立即关闭”之类的提示框。当发生NDK错误后,logcat打印出来的那堆日志根据看不懂,更别想从日志当中定位错误的根源,让我时常有点抓狂,火冒三丈,喝多少加多宝都不管用。当时尝试过在各个jni函数中打印日志来跟踪问题,那效率实在是太低了,而且还定位不到问题。还好老天有眼,让我找到了NDK提供的几款调试工具,能够精确的定位到产生错误的根源。 NDK安装包中提供了三个调试工具:addr2line、objdump和ndk-stack,其中ndk-stack放在$NDK_HOME目录下,与ndk-build同级目录。addr2line和objdump在ndk的交叉编译器工具链目录下,下面是我本机NDK交叉编译器工具链的目录结构: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9463216f.jpg) 从上图的目录结构中可以看出来,NDK针对不同的CPU架构实现了多套相同的工具。所以在选择addr2line和objdump工具的时候,要根据你目标机器的CPU架构来选择。如果是arm架构,选择arm-linux-androidabi-4.6/4.8(一般选择高版本)。x86架构,选择x86-4.6/4.8。mipsel架构,选择mipsel-linux-android-4.6/4.8。如果不知道目标机器的CPU架构,把手机连上电脑,用adb shell cat /proc/cpuinfo可以查看手机的CPU信息。下图是我本机的arm架构工具链目录结构: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c946490b7.jpg) 下面通过NDK自带的例子hello-jni项目来演示一下如何精确的定位错误 ~~~ #include <string.h> #include <jni.h> // hell-jni.c #ifdef __cplusplus extern "C" { #endif void willCrash() { int i = 10; int y = i / 0; } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) { willCrash(); return JNI_VERSION_1_4; } jstring Java_com_example_hellojni_HelloJni_stringFromJNI( JNIEnv* env, jobject thiz ) { // 此处省略实现逻辑。。。 } #ifdef __cplusplus } #endif ~~~ 第7行定义了一个willCrash函数,函数中有一个除0的非法操作,会造成程序崩溃。第13行JNI_OnLoad函数中调用了willCrash,这个函数会在Java加载完.so文件之后回调,也就是说程序一启动就会崩溃。下面是运行程序后打印的log: ~~~ 01-01 17:59:38.246: D/dalvikvm(20794): Late-enabling CheckJNI 01-01 17:59:38.246: I/ActivityManager(1185): Start proc com.example.hellojni for activity com.example.hellojni/.HelloJni: pid=20794 uid=10351 gids={50351, 1028, 1015} 01-01 17:59:38.296: I/dalvikvm(20794): Enabling JNI app bug workarounds for target SDK version 3... 01-01 17:59:38.366: D/dalvikvm(20794): Trying to load lib /data/app-lib/com.example.hellojni-1/libhello-jni.so 0x422a4f58 01-01 17:59:38.366: D/dalvikvm(20794): Added shared lib /data/app-lib/com.example.hellojni-1/libhello-jni.so 0x422a4f58 01-01 17:59:38.366: A/libc(20794): Fatal signal 8 (SIGFPE) at 0x0000513a (code=-6), thread 20794 (xample.hellojni) 01-01 17:59:38.476: I/DEBUG(253): pid: 20794, tid: 20794, name: xample.hellojni >>> com.example.hellojni <<< 01-01 17:59:38.476: I/DEBUG(253): signal 8 (SIGFPE), code -6 (SI_TKILL), fault addr 0000513a 01-01 17:59:38.586: I/DEBUG(253): r0 00000000 r1 0000513a r2 00000008 r3 00000000 01-01 17:59:38.586: I/DEBUG(253): r4 00000008 r5 0000000d r6 0000513a r7 0000010c 01-01 17:59:38.586: I/DEBUG(253): r8 75226d08 r9 00000000 sl 417c5c38 fp bedbf134 01-01 17:59:38.586: I/DEBUG(253): ip 41705910 sp bedbf0f0 lr 4012e169 pc 4013d10c cpsr 000f0010 // 省略部份日志 。。。。。。 01-01 17:59:38.596: I/DEBUG(253): backtrace: 01-01 17:59:38.596: I/DEBUG(253): #00 pc 0002210c /system/lib/libc.so (tgkill+12) 01-01 17:59:38.596: I/DEBUG(253): #01 pc 00013165 /system/lib/libc.so (pthread_kill+48) 01-01 17:59:38.596: I/DEBUG(253): #02 pc 00013379 /system/lib/libc.so (raise+10) 01-01 17:59:38.596: I/DEBUG(253): #03 pc 00000e80 /data/app-lib/com.example.hellojni-1/libhello-jni.so (__aeabi_idiv0+8) 01-01 17:59:38.596: I/DEBUG(253): #04 pc 00000cf4 /data/app-lib/com.example.hellojni-1/libhello-jni.so (willCrash+32) 01-01 17:59:38.596: I/DEBUG(253): #05 pc 00000d1c /data/app-lib/com.example.hellojni-1/libhello-jni.so (JNI_OnLoad+20) 01-01 17:59:38.596: I/DEBUG(253): #06 pc 00052eb1 /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+468) 01-01 17:59:38.596: I/DEBUG(253): #07 pc 0006a62d /system/lib/libdvm.so 01-01 17:59:38.596: I/DEBUG(253): // 省略部份日志 。。。。。。 01-01 17:59:38.596: I/DEBUG(253): stack: 01-01 17:59:38.596: I/DEBUG(253): bedbf0b0 71b17034 /system/lib/libsechook.so 01-01 17:59:38.596: I/DEBUG(253): bedbf0b4 7521ce28 01-01 17:59:38.596: I/DEBUG(253): bedbf0b8 71b17030 /system/lib/libsechook.so 01-01 17:59:38.596: I/DEBUG(253): bedbf0bc 4012c3cf /system/lib/libc.so (dlfree+50) 01-01 17:59:38.596: I/DEBUG(253): bedbf0c0 40165000 /system/lib/libc.so 01-01 17:59:38.596: I/DEBUG(253): // 省略部份日志 。。。。。。 01-01 17:59:38.736: W/ActivityManager(1185): Force finishing activity com.example.hellojni/.HelloJni ~~~ **日志分析:** 第3行开始启动应用,第5行尝试加载应用数据目录下的so,第6行在加载so文件的时候产生了一个致命的错误,第7行的Fatal signal 8提示这是一个致命的错误,这个信号是由linux内核发出来的,信号8的意思是浮点数运算异常,应该是在willCrash函数中做除0操作所产生的。下面重点看第15行backtrace的日志,backtrace日志可以看作是JNI调用的堆栈信息,以“#两位数字 pc”开头的都是backtrace日志。**注意看第20行和21行,是我们自己编译的so文件和定义的两个函数,在这里引发了异常,导致程序崩溃**。 ~~~ 01-01 17:59:38.596: I/DEBUG(253): #04 pc 00000cf4 /data/app-lib/com.example.hellojni-1/libhello-jni.so (willCrash+32) 01-01 17:59:38.596: I/DEBUG(253): #05 pc 00000d1c /data/app-lib/com.example.hellojni-1/libhello-jni.so (JNI_OnLoad+20) ~~~ 开始有些眉目了,但具体崩在这两个函数的哪个位置,我们是不确定的,如果函数代码比较少还好查,如果比较复杂的话,查起来也费劲。这时候就需要靠NDK为我们提供的工具来精确定位了。在这之前,我们先记录下让程序崩溃的汇编指令地址,willCrash:00000cf4,JNI_OnLoad:00000d1c **方式1:使用arm-linux-androideabi-addr2line  定位出错位置** 以arm架构的CPU为例,执行如下命令: ~~~ /Users/yangxin/Documents/devToos/java/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-addr2line -e /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/obj/local/armeabi-v7a/libhello-jni.so 00000cf4 00000d1c ~~~ -e:指定so文件路径 0000cf4 0000d1c:出错的汇编指令地址 结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9466888c.jpg) 是不是惊喜的看到我们想要的结果了,分别在hello-jni.c的10和15行的出的错,再回去看看hello-jni.c的源码,15行的Jni_OnLoad函内调用了willCrash函数,第10行做了除0的操作引发的crash。 **方式2:使用arm-linux-androideabi-objdump  定位出错的函数信息** 在第一种方式中,通过addr2lin已经获取到了代码出错的位置,但是不知道函数的上下文信息,显得有点不是那么的“完美”,对于追求极致的我来说,这显然是不够的,下面我们来看一下怎么来定位函数的信息。 首先使用如下命令导出so的函数表信息: ~~~ /Users/yangxin/Documents/devToos/java/android-ndk-r9d/toolchains/arm-linux-androideabi-4.8/prebuilt/darwin-x86_64/bin/arm-linux-androideabi-objdump -S -D /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/obj/local/armeabi-v7a/libhello-jni.so > Users/yangxin/Desktop/dump.log ~~~ 在生成的asm文件中,找出我们开始定位到的那两个出错的汇编指令地址(在文件中搜索cf4或willCrash可以找到),如下图所示: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c94680f21.jpg) 通过这种方式,也可以查出这两个出错的指针地址分别位于哪个函数中。 **方式3:ndk-stack** 如果你觉得上面的方法太麻烦的话,ndk-stack可以帮你减轻操作步聚,直接定位到代码出错的位置。 **实时分析日志:** 使用adb获取logcat的日志,并通过管道输出给ndk-stack分析,并指定包含符号表的so文件位置。如果程序包含多种CPU架构,需要根据手机的CPU类型,来选择不同的CPU架构目录。以armv7架构为例,执行如下命令: ~~~ adb logcat | ndk-stack -sym /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/obj/local/armeabi-v7a ~~~ 当程序发生crash时,会输出如下信息: ~~~ pid: 22654, tid: 22654, name: xample.hellojni >>> com.example.hellojni <<< signal 8 (SIGFPE), code -6 (SI_TKILL), fault addr 0000587e Stack frame #00 pc 0002210c /system/lib/libc.so (tgkill+12) Stack frame #01 pc 00013165 /system/lib/libc.so (pthread_kill+48) Stack frame #02 pc 00013379 /system/lib/libc.so (raise+10) Stack frame #03 pc 00000e80 /data/app-lib/com.example.hellojni-1/libhello-jni.so (__aeabi_idiv0+8): Routine __aeabi_idiv0 at /s/ndk-toolchain/src/build/../gcc/gcc-4.6/libgcc/../gcc/config/arm/lib1funcs.asm:1270 Stack frame #04 pc 00000cf4 /data/app-lib/com.example.hellojni-1/libhello-jni.so (willCrash+32): Routine willCrash at /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/jni/hello-jni.c:10 Stack frame #05 pc 00000d1c /data/app-lib/com.example.hellojni-1/libhello-jni.so (JNI_OnLoad+20): Routine JNI_OnLoad at /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/jni/hello-jni.c:15 Stack frame #06 pc 00052eb1 /system/lib/libdvm.so (dvmLoadNativeCode(char const*, Object*, char**)+468) Stack frame #07 pc 0006a62d /system/lib/libdvm.so ~~~ 第7行和第8行分别打印出了在源文件中出错的位置,和addr2line得到的结果一样。 **先获取日志再分析:** 这种方式和上面的方法差不多,只是获取log的来源不一样。适用于应用或游戏给测试部们测试的时候,测试人员发现crash,用adb logcat保存日志文件,然后发给程序员通过ndk-stack命令分析。操作流程如下: ~~~ adb logcat > crash.log ndk-stack -sym /Users/yangxin/Documents/devToos/java/android-ndk-r9d/samples/hello-jni/obj/local/armeabi-v7a -dump crash.log ~~~ 得到的结果和上面的方式是一样的。
';

JNI/NDK开发指南(四)——字符串处理

最后更新于:2022-04-01 07:38:52

从[第三章](http://blog.csdn.net/xyang81/article/details/42047899#demo)中可以看出JNI中的基本类型和Java中的基本类型都是一一对应的,接下来先看一下JNI的基本类型定义: ~~~ typedef unsigned char jboolean; typedef unsigned short jchar; typedef short jshort; typedef float jfloat; typedef double jdouble; typedef int jint; #ifdef _LP64 /* 64-bit Solaris */ typedef long jlong; #else typedef long long jlong; #endif typedef signed char jbyte; ~~~ 基本类型很容易理解,就是对C/C++中的基本类型用typedef重新定义了一个新的名字,在JNI中可以直接访问。 JNI把Java中的所有对象当作一个C指针传递到本地方法中,这个指针指向JVM中的内部数据结构,而内部的数据结构在内存中的存储方式是不可见的。只能从JNIEnv指针指向的函数表中选择合适的JNI函数来操作JVM中的数据结构。[第三章](http://blog.csdn.net/xyang81/article/details/42047899#demo)的示例中,访问java.lang.String对应的JNI类型jstring时,没有像访问基本数据类型一样直接使用,因为它在Java是一个引用类型,所以在本地代码中只能通过GetStringUTFChars这样的JNI函数来访问字符串的内容。 下面先看一个例子: Sample.java: ~~~ package com.study.jnilearn; public class Sample { public native static String sayHello(String text); public static void main(String[] args) { String text = sayHello("yangxin"); System.out.println("Java str: " + text); } static { System.loadLibrary("Sample"); } } ~~~ com_study_jnilearn_Sample.h和Sample.c: ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_Sample */ #ifndef _Included_com_study_jnilearn_Sample #define _Included_com_study_jnilearn_Sample #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_Sample * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif // Sample.c #include "com_study_jnilearn_Sample.h" /* * Class: com_study_jnilearn_Sample * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { const char *c_str = NULL; char buff[128] = {0}; jboolean isCopy; // 返回JNI_TRUE表示原字符串的拷贝,返回JNI_FALSE表示返回原字符串的指针 c_str = (*env)->GetStringUTFChars(env, j_str, &isCopy); printf("isCopy:%d\n",isCopy); if(c_str == NULL) { return NULL; } printf("C_str: %s \n", c_str); sprintf(buff, "hello %s", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); return (*env)->NewStringUTF(env,buff); } ~~~ 运行结果如下: ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c94616fb8.jpg) ***示例解析:*** **1> 访问字符串** sayHello函数接收一个jstring类型的参数text,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型char*不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字符串数据结构。 **GetStringUTFChars(env, j_str, &isCopy) 参数说明:**  env:JNIEnv函数表指针 j_str:jstring类型(Java传递给本地代码的字符串指针) **isCopy:**取值JNI_TRUE和JNI_FALSE,如果值为JNI_TRUE,表示返回JVM内部源字符串的一份拷贝,并为新产生的字符串分配内存空间。如果值为JNI_FALSE,表示返回JVM内部源字符串的指针,意味着可以通过指针修改源字符串的内容,不推荐这么做,因为这样做就打破了Java字符串不能修改的规定。但我们在开发当中,并不关心这个值是多少,通常情况下这个参数填NULL即可。 因为Java默认使用Unicode编码,而C/C++默认使用UTF编码,所以在本地代码中操作字符串的时候,必须使用合适的JNI函数把jstring转换成C风格的字符串。JNI支持字符串在Unicode和UTF-8两种编码之间转换,GetStringUTFChars可以把一个jstring指针(指向JVM内部的Unicode字符序列)转换成一个UTF-8格式的C字符串。在上例中sayHello函数中我们通过GetStringUTFChars正确取得了JVM内部的字符串内容。 **2> 异常检查** 调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空间,当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个OutOfMemoryError异常。JNI的异常和Java中的异常处理流程是不一样的,Java遇到异常如果没有捕获,程序会立即停止运行。而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非常危险的,因此,我们需要用return语句跳过后面的代码,并立即结束当前方法。 **3> 释放字符串** 在调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用ReleaseStringUTFChars函数通知JVM这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用了GetXXX就必须调用ReleaseXXX,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一样。 **4> 创建字符串** 通过调用NewStringUTF函数,会构建一个新的java.lang.String字符串对象。这个新创建的字符串会自动转换成Java支持的Unicode编码。如果JVM不能为构造java.lang.String分配足够的内存,NewStringUTF会抛出一个OutOfMemoryError异常,并返回NULL。在这个例子中我们不必检查它的返回值,如果NewStringUTF创建java.lang.String失败,OutOfMemoryError这个异常会被在Sample.main方法中抛出。如果NewStringUTF创建java.lang.String成功,则返回一个JNI引用,这个引用指向新创建的java.lang.String对象。 **其它字符串处理函数:** **1> GetStringChars和ReleaseStringChars:**这对函数和Get/ReleaseStringUTFChars函数功能差不多,用于获取和释放以Unicode格式编码的字符串。后者是用于获取和释放UTF-8编码的字符串。 **2> GetStringLength:**由于UTF-8编码的字符串以'\0'结尾,而Unicode字符串不是。如果想获取一个指向Unicode编码的jstring字符串长度,在JNI中可通过这个函数获取。 **3> GetStringUTFLength:**获取UTF-8编码字符串的长度,也可以通过标准C函数strlen获取 **4> GetStringCritical和ReleaseStringCritical:**提高JVM返回源字符串直接指针的可能性 Get/ReleaseStringChars和Get/ReleaseStringUTFChars这对函数返回的源字符串会后分配内存,如果有一个字符串内容相当大,有1M左右,而且只需要读取里面的内容打印出来,用这两对函数就有些不太合适了。此时用Get/ReleaseStringCritical可直接返回源字符串的指针应该是一个比较合适的方式。不过这对函数有一个很大的限制,在这两个函数之间的本地代码不能调用任何会让线程阻塞或等待JVM中其它线程的本地函数或JNI函数。因为通过GetStringCritical得到的是一个指向JVM内部字符串的直接指针,获取这个直接指针后会导致暂停GC线程,当GC被暂停后,如果其它线程触发GC继续运行的话,都会导致阻塞调用者。所以在Get/ReleaseStringCritical这对函数中间的任何本地代码都不可以执行导致阻塞的调用或为新对象在JVM中分配内存,否则,JVM有可能死锁。另外一定要记住检查是否因为内存溢出而导致它的返回值为NULL,因为JVM在执行GetStringCritical这个函数时,仍有发生数据复制的可能性,尤其是当JVM内部存储的数组不连续时,为了返回一个指向连续内存空间的指针,JVM必须复制所有数据。下面代码演示这对函数的正确用法: ~~~ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { const jchar* c_str= NULL; char buff[128] = "hello "; char* pBuff = buff + 6; /* * 在GetStringCritical/RealeaseStringCritical之间是一个关键区。 * 在这关键区之中,绝对不能呼叫JNI的其他函数和会造成当前线程中断或是会让当前线程等待的任何本地代码, * 否则将造成关键区代码执行区间垃圾回收器停止运作,任何触发垃圾回收器的线程也会暂停。 * 其他触发垃圾回收器的线程不能前进直到当前线程结束而激活垃圾回收器。 */ c_str = (*env)->GetStringCritical(env,j_str,NULL); // 返回源字符串指针的可能性 if (c_str == NULL) // 验证是否因为字符串拷贝内存溢出而返回NULL { return NULL; } while(*c_str) { *pBuff++ = *c_str++; } (*env)->ReleaseStringCritical(env,j_str,c_str); return (*env)->NewStringUTF(env,buff); } ~~~ JNI中没有Get/ReleaseStringUTFCritical这样的函数,因为在进行编码转换时很可能会促使JVM对数据进行复制,因为JVM内部表示的字符串是使用Unicode编码的。 **5> GetStringRegion和GetStringUTFRegion:**分别表示获取Unicode和UTF-8编码字符串指定范围内的内容。这对函数会把源字符串复制到一个预先分配的缓冲区内。下面代码用GetStringUTFRegion重新实现sayHello函数: ~~~ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_Sample_sayHello (JNIEnv *env, jclass cls, jstring j_str) { jsize len = (*env)->GetStringLength(env,j_str); // 获取unicode字符串的长度 printf("str_len:%d\n",len); char buff[128] = "hello "; char* pBuff = buff + 6; // 将JVM中的字符串以utf-8编码拷入C缓冲区,该函数内部不会分配内存空间 (*env)->GetStringUTFRegion(env,j_str,0,len,pBuff); return (*env)->NewStringUTF(env,buff); } ~~~ GetStringUTFRegion这个函数会做越界检查,如果检查发现越界了,会抛出StringIndexOutOfBoundsException异常,这个方法与GetStringUTFChars比较相似,不同的是,GetStringUTFRegion内部不分配内存,不会抛出内存溢出异常。 注意:GetStringUTFRegion和GetStringRegion这两个函数由于内部没有分配内存,所以JNI没有提供ReleaseStringUTFRegion和ReleaseStringRegion这样的函数。 **字符串操作总结:** 1、对于小字符串来说,GetStringRegion和GetStringUTFRegion这两对函数是最佳选择,因为缓冲区可以被编译器提前分配,而且永远不会产生内存溢出的异常。当你需要处理一个字符串的一部分时,使用这对函数也是不错。因为它们提供了一个开始索引和子字符串的长度值。另外,复制少量字符串的消耗 也是非常小的。 2、使用GetStringCritical和ReleaseStringCritical这对函数时,必须非常小心。一定要确保在持有一个由 GetStringCritical 获取到的指针时,本地代码不会在 JVM 内部分配新对象,或者做任何其它可能导致系统死锁的阻塞性调用 3、获取Unicode字符串和长度,使用GetStringChars和GetStringLength函数 4、获取UTF-8字符串的长度,使用GetStringUTFLength函数 5、创建Unicode字符串,使用NewStringUTF函数 6、从Java字符串转换成C/C++字符串,使用GetStringUTFChars函数 7、通过GetStringUTFChars、GetStringChars、GetStringCritical获取字符串,这些函数内部会分配内存,必须调用相对应的ReleaseXXXX函数释放内存 示例代码下载地址:[https://code.csdn.net/xyang81/jnilearn](https://code.csdn.net/xyang81/jnilearn)
';

JNI/NDK开发指南(三)——JNI数据类型及与Java数据类型的映射关系

最后更新于:2022-04-01 07:38:49

当我们在调用一个Java native方法的时候,方法中的参数是如何传递给C/C++本地函数中的呢?Java方法中的参数与C/C++函数中的参数,它们之间是怎么转换的呢?我猜你应该也有相关的疑虑吧,咱们先来看一个例子,还是以HelloWorld为例: HelloWorld.java: ~~~ package com.study.jnilearn; class MyClass {} public class HelloWorld { public static native void test(short s, int i, long l, float f, double d, char c, boolean z, byte b, String str, Object obj, MyClass p, int[] arr); public static void main(String[] args) { String obj = "obj"; short s = 1; long l = 20; byte b = 127; test(s, 1, l, 1.0f, 10.5, 'A', true, b, "中国", obj, new MyClass(), new int[] {}); } static { System.loadLibrary("HelloWorld"); } } ~~~ 在HelloWorld.java中定义了一个test的native方法,该方法中一个共有12个参数,其中前面8个为基本数据类型,后面4个全部为引用类型。 由HelloWorld.class生成的native函数原型及实现: ~~~ /* * Class: com_study_jnilearn_HelloWorld * Method: test * Signature: (SIJFDCZBLjava/lang/String;Ljava/lang/Object;Lcom/study/jnilearn/MyClass;[I)V */ JNIEXPORT void JNICALL Java_com_study_jnilearn_HelloWorld_test (JNIEnv *env, jclass cls, jshort s, jint i, jlong l, jfloat f, jdouble d, jchar c, jboolean z, jbyte b, jstring j_str, jobject jobj1, jobject job2, jintArray j_int_arr) { printf("s=%hd, i=%d, l=%ld, f=%f, d=%lf, c=%c, z=%c, b=%d", s, i, l, f, d, c, z, b); const char *c_str = NULL; c_str = (*env)->GetStringUTFChars(env, j_str, NULL); if (c_str == NULL) { return; // memory out } (*env)->ReleaseStringUTFChars(env, j_str, c_str); printf("c_str: %s\n", (char*)c_str); } ~~~ **调用test方法的输出结果:** **![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9459e352.jpg)** 从头文件函数的原型可以得知,test方法中形参的数据类型自动转换成了JNI中相应的数据类型,不难理解,在调用Java native方法将实参传递给C/C++函数的时候,会自动将java形参的数据类型自动转换成C/C++相应的数据类型,所以我们在写JNI程序的时候,必须要明白它们之间数据类型的对应关系。 在Java语言中数据类型分为基本数据类型和引用类型,其中基本数据类型有8种:byte、char、short、int、long、float、double、boolean,除了基本数据类型外其它都是引用类型:Object、String、数组等。8种基本数据类型分别对应JNI数据类型中的jbyte、jchar、jshort、jint、jlong、jfloat、jdouble、jboolean。所有的JNI引用类型全部是jobject类型,为了使用方便和类型安全,JNI定义了一个引用类型集合,集合当中的所有类型都是jobject的子类,这些子类和Java中常用的引用类型相对应。例如:jstring表示字符串、jclass表示class字节码对象、jthrowable表示异常、jarray表示数组,另外jarray派生了8个子类,分别对应Java中的8种基本数据类型(jintArray、jshortArray、jlongArray等)。下面再回顾头来看看test方法与Java_com_study_jnilearn_HelloWorld_test函数中参数类型的对应关系: ~~~ // HelloWorld.java public static native void test(short s, int i, long l, float f, double d, char c, boolean z, byte b, String str, Object obj, MyClass p); // HelloWorld.h JNIEXPORT void JNICALL Java_com_study_jnilearn_HelloWorld_test (JNIEnv *, jclass, jshort, jint, jlong, jfloat, jdouble, jchar, jboolean, jbyte, jstring, jobject, jobject, jintArray); ~~~ 从上面两个函数的参数中可以看出来,除了JNIEnv和jclass这两个参数外,其它参数都是一一对应的。下面是JNI规范文档中描述Java与JNI数据类型的对应关系: **基本数据类型:** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c945b35bd.jpg) **引用类型:** ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c945d27a6.jpg) 注意: 1、JNI如果使用C++语言编写的话,所有引用类型派生自jobject,使用C++的继承结构特性,使用相应的类型。如下所示: ~~~ class _jobject {}; class _jclass : public _jobject {}; class _jstring : public _jobject {}; class _jarray : public _jobject {}; class _jbooleanArray : public _jarray {}; class _jbyteArray : public _jarray {}; ... ~~~ 2、JNI如果使用C语言编写的话,所有引用类型使用jobject,其它引用类型使用typedef重新定义,如:typedef jobject jstring **jvalue类型:** jvalue是一个unio(联合)类型,在C语中为了节约内存,会用联合类型变量来存储声明在联合体中的任意类型数据 。在JNI中将基本数据类型与引用类型定义在一个联合类型中,表示用jvalue定义的变量,可以存储任意JNI类型的数据,后面会介绍jvalue在JNI编程当中的应用。原型如下: ~~~ typedef union jvalue { jboolean z; jbyte b; jchar c; jshort s; jint i; jlong j; jfloat f; jdouble d; jobject l; } jvalue; ~~~ 如果对unio类型不太明白的同学,请参考相关资料,在这里不细讲。
';

JNI/NDK开发指南(二)——JVM查找java native方法的规则

最后更新于:2022-04-01 07:38:47

通过第一篇文章,大家明白了调用native方法之前,首先要调用System.loadLibrary接口加载一个实现了native方法的动态库才能正常访问,否则就会抛出java.lang.UnsatisfiedLinkError异常,找不到XX方法的提示。现在我们想想,在Java中调用某个native方法时,JVM是通过什么方式,能正确的找到动态库中C/C++实现的那个native函数呢? **JVM查找native方法有两种方式:** 1> 按照JNI规范的命名规则 2> 调用JNI提供的RegisterNatives函数,将本地函数注册到JVM中。(后面会详细介绍) 本文通过第一篇文章HelloWorld示例中的Java_com_study_jnilearn_HelloWorld_sayHello函数来详细介绍第一种方式: ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_HelloWorld */ #ifndef _Included_com_study_jnilearn_HelloWorld #define _Included_com_study_jnilearn_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_HelloWorld * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif ~~~ ***JNIEXPORT 和 JNICALL的作用:*** 在上篇文章中,我们在将HelloWorld.c编译成动态库的时候,用-I参数包含了JDK安装目录下的两个头文件目录: ~~~ gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin ~~~ 其中第一个目录为jni.h头文件所在目录,第二个是跨平台头文件目录(mac os x系统下的目录名为darwin,在windows下目录名为win32,linux下目录名为linux),用于定义与平台相关的宏,其中用于标识函数用途的两个宏**JNIEXPORT**和 JNICALL,就定义在darwin目录下的**jni_md.h**头文件中。在Windows中编译dll动态库规定,如果动态库中的函数要被外部调用,需要在函数声明中添加__declspec(dllexport)标识,表示将该函数导出在外部可以调用。在Linux/Unix系统中,这两个宏可以省略不加。这两个平台的区别是由于各自的编译器所产生的可执行文件格式不一样。这里有篇文章详细介绍了两个平台编译的动态库区别:[http://www.cnblogs.com/chio/archive/2008/11/13/1333119.html](http://www.cnblogs.com/chio/archive/2008/11/13/1333119.html)。JNICALL在windows中的值为__stdcall,用于约束函数入栈顺序和堆栈清理的规则。 Windows下jni_md.h头文件内容: ~~~ #ifndef _JAVASOFT_JNI_MD_H_ #define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT __declspec(dllexport) #define JNIIMPORT __declspec(dllimport) #define JNICALL __stdcall typedef long jint; typedef __int64 jlong; typedef signed char jbyte; #endif ~~~ **Linux下jni_md.h头文件内容:** ~~~ #ifndef _JAVASOFT_JNI_MD_H_ #define _JAVASOFT_JNI_MD_H_ #define JNIEXPORT #define JNIIMPORT #define JNICALL typedef int jint; #ifdef _LP64 /* 64-bit Solaris */ typedef long jlong; #else typedef long long jlong; #endif typedef signed char jbyte; #endif ~~~ 从Linux下的jni_md.h头文件可以看出来,JNIEXPORT 和 JNICALL是一个空定义,所以在Linux下JNI函数声明可以省略这两个宏。 **函数的命名规则:** 用javah工具生成函数原型的头文件,函数命名规则为:Java_类全路径_方法名。如Java_com_study_jnilearn_HelloWorld_sayHello,其中Java_是函数的前缀,com_study_jnilearn_HelloWorld是类名,sayHello是方法名,它们之间用 _(下划线) 连接。 **函数参数:** JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello(JNIEnv *, jclass, jstring); 第一个参数:JNIEnv* 是定义任意native函数的**第一个参数**(包括调用JNI的RegisterNatives函数注册的函数),指向JVM函数表的指针,函数表中的每一个入口指向一个JNI函数,每个函数用于访问JVM中特定的数据结构。 第二个参数:调用java中native方法的实例或Class对象,如果这个native方法是实例方法,则该参数是jobject,如果是静态方法,则是jclass 第三个参数:Java对应JNI中的数据类型,Java中String类型对应JNI的jstring类型。(后面会详细介绍JAVA与JNI数据类型的映射关系) **函数返回值类型:** 夹在JNIEXPORT和JNICALL宏中间的jstring,表示函数的返回值类型,对应Java的String类型 **总结:** 当我们熟悉了JNI的native函数命名规则之后,就可以不用通过javah命令去生成相应java native方法的函数原型了,只需要按照函数命名规则编写相应的函数原型和实现即可。 比如com.study.jni.Utils类中还有一个计算加法的native实例方法add,有两个int参数和一个int返回值:public native int add(int num1, int num2),对应JNI的函数原型就是:JNIEXPORT jint JNICALL Java_com_study_jni_Utils_add(JNIEnv *, jobject, jint,jint); 
';

JNI/NDK开发指南(一)—— JNI开发流程及HelloWorld

最后更新于:2022-04-01 07:38:45

JNI全称是Java Native Interface(Java本地接口)单词首字母的缩写,本地接口就是指用C和C++开发的接口。由于JNI是**JVM规范**中的一部份,因此可以将我们写的JNI程序在任何实现了JNI规范的Java虚拟机中运行。同时,这个特性使我们可以复用以前用C/C++写的大量代码。 开发JNI程序会受到系统环境的限制,因为用C/C++语言写出来的代码或模块,编译过程当中要依赖当前操作系统环境所提供的一些库函数,并和本地库链接在一起。而且编译后生成的二进制代码只能在本地操作系统环境下运行,因为不同的操作系统环境,有自己的本地库和CPU指令集,而且各个平台对标准C/C++的规范和标准库函数实现方式也有所区别。这就造成使用了JNI接口的JAVA程序,不再像以前那样自由的跨平台。如果要实现跨平台,就必须将本地代码在不同的操作系统平台下编译出相应的动态库。 JNI开发流程主要分为以下6步: 1、编写声明了native方法的Java类 2、将Java源代码编译成class字节码文件 3、用javah -jni命令生成.h头文件(javah是jdk自带的一个命令,-jni参数表示将class中用native声明的函数生成jni规则的函数) 4、用本地代码实现.h头文件中的函数 5、将本地代码编译成动态库(windows:***.dll**,linux/unix:***.so**,mac os x:***.jnilib**) 6、拷贝动态库至 java.library.path 本地库搜索目录下,并运行Java程序 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9455b390.jpg) 通过上面的介绍,相信大家对JNI及开发流程有了一个整体的认识,下面通过一个HelloWorld的示例,再深入了解JNI开发的各个环节及注意事项。 *PS:本人的开发环境为Mac os x 10.10.1 ,Eclipse 3.8(Juno),如果在其它操作系统下开发也是一样,只需将本地代码编译成当前操作系统所支持的动态库即可。* *这个案例用命令行的方式介绍开发流程,这样大家对JNI开发流程的印象会更加深刻,后面的案例都采用eclipse+cdt来开发。* **第一步、并新建一个HelloWorld.java源文件** ~~~ package com.study.jnilearn; public class HelloWorld { public static native String sayHello(String name); // 1.声明这是一个native函数,由本地代码实现 public static void main(String[] args) { String text = sayHello("yangxin"); // 3.调用本地函数 System.out.println(text); } static { System.loadLibrary("HelloWorld"); // 2.加载实现了native函数的动态库,只需要写动态库的名字 } } ~~~ **第二步、用javac命令将.java源文件编译成.class字节码文件** *注意:HelloWorld放在com.study.jnilearn包下面* ~~~ javac src/com/study/jnilearn/HelloWorld.java -d ./bin ~~~ -d 表示将编译后的class文件放到指定的目录下,这里我把它放到和src同级的bin目录下 **第三步、用javah -jni命令,根据class字节码文件生成.h头文件**(*-jni参数是可选的*) ~~~ javah -jni -classpath ./bin -d ./jni com.study.jnilearn.HelloWorld ~~~ 默认生成的.h头文件名为:com_study_jnilearn_HelloWorld.h(包名+类名.h),也可以通过-o参数指定生成头文件名称: ~~~ javah -jni -classpath ./bin -o HelloWorld.h com.study.jnilearn.HelloWorld ~~~ 参数说明: -classpath :类搜索路径,这里表示从当前的bin目录下查找 -d :将生成的头文件放到当前的jni目录下 -o : 指定生成的头文件名称,默认以类全路径名生成(包名+类名.h) 注意:-d和-o只能使用其中一个参数。 **第四步、用本地代码实现.h头文件中的函数** **com_study_jnilearn_HelloWorld.h:** ~~~ /* DO NOT EDIT THIS FILE - it is machine generated */ #include <jni.h> /* Header for class com_study_jnilearn_HelloWorld */ #ifndef _Included_com_study_jnilearn_HelloWorld #define _Included_com_study_jnilearn_HelloWorld #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_HelloWorld * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello (JNIEnv *, jclass, jstring); #ifdef __cplusplus } #endif #endif ~~~ **HelloWorld.c:** ~~~ // HelloWorld.c #include "com_study_jnilearn_HelloWorld.h" #ifdef __cplusplus extern "C" { #endif /* * Class: com_study_jnilearn_HelloWorld * Method: sayHello * Signature: (Ljava/lang/String;)Ljava/lang/String; */ JNIEXPORT jstring JNICALL Java_com_study_jnilearn_HelloWorld_sayHello( JNIEnv *env, jclass cls, jstring j_str) { const char *c_str = NULL; char buff[128] = { 0 }; c_str = (*env)->GetStringUTFChars(env, j_str, NULL); if (c_str == NULL) { printf("out of memory.\n"); return NULL; } printf("Java Str:%s\n", c_str); sprintf(buff, "hello %s", c_str); (*env)->ReleaseStringUTFChars(env, j_str, c_str); return (*env)->NewStringUTF(env, buff); } #ifdef __cplusplus } #endif ~~~ **第五步、将C/C++代码编译成本地动态库文件**     ***动态库文件名命名规则:***lib+动态库文件名+后缀(操作系统不一样,后缀名也不一样)如:     Mac OS X : libHelloWorld.jnilib     Windows :HelloWorld.dll(不需要lib前缀)     Linux/Unix:libHelloWorld.so ***1> Mac OS X*** ~~~ gcc -dynamiclib -o /Users/yangxin/Library/Java/Extensions/libHelloWorld.jnilib jni/HelloWorld.c -framework JavaVM -I/$JAVA_HOME/include -I/$JAVA_HOME/include/darwin ~~~ 我的$JAVA_HOME目录在:/Library/Java/JavaVirtualMachines/jdk1.7.0_21.jdk/Contents/Home 参数选项说明: -dynamiclib:表示编译成动态链接库 -o:指定动态链接库编译后生成的路径及文件名 -framework JavaVM -I:编译JNI需要用到JVM的头文件(jni.h),第一个目录是平台无关的,第二个目录是与操作系统平台相关的头文件 ***2> Windows(以Windows7下VS2012为例)*** 开始菜单-->所有程序-->Microsoft Visual Studio 2012-->打开VS2012 X64本机工具命令提示,用cl命令编译成dll动态库: ~~~ cl -I"%JAVA_HOME%\include" -I"%JAVA_HOME%\include\win32" -LD HelloWorld.c -FeHelloWorld.dll ~~~ 参数选项说明: -I :   和mac os x一样,包含编译JNI必要的头文件 -LD:标识将指定的文件编译成动态链接库 -Fe:指定编译后生成的动态链接库的路径及文件名 ![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-02-16_56c2c9457865b.jpg) **3> Linux/Unix** ~~~ gcc -I$JAVA_HOME/include -I$JAVA_HOME/include/linux -fPIC -shared HelloWorld.c -o libHelloWorld.so ~~~ 参数说明: -I:          包含编译JNI必要的头文件 -fPIC:    编译成与位置无关的独立代码 -shared:编译成动态库 -o:         指定编译后动态库生成的路径和文件名 **第六步、运行Java程序**      Java在调用native(本地)方法之前,需要先加载动态库。如果在未加载动态之前就调用native方法,会抛出找不到动态链接库文件的异常。如下所示: ~~~ Exception in thread "main" java.lang.UnsatisfiedLinkError: com.study.jnilearn.HelloWorld.sayHello(Ljava/lang/String;)Ljava/lang/String; at com.study.jnilearn.HelloWorld.sayHello(Native Method) at com.study.jnilearn.HelloWorld.main(HelloWorld.java:9) ~~~ 一般在类的静态(static)代码块中加载动态库最合适,因为在创建类的实例时,类会被ClassLoader先加载到虚拟机,随后立马调用类的static静态代码块。这时再去调用native方法就万无一失了。加载动态库的两种方式: ~~~ System.loadLibrary("HelloWorld"); System.load("/Users/yangxin/Desktop/libHelloWorld.jnilib"); ~~~ 方式1:只需要指定动态库的名字即可,不需要加lib前缀,也不要加.so、.dll和.jnilib后缀 方式2:指定动态库的绝对路径名,需要加上前缀和后缀 如果使用方式1,java会去java.library.path系统属性指定的目录下查找动态库文件,如果没有找到会抛出java.lang.UnsatisfiedLinkError异常。 ~~~ Exception in thread "main" java.lang.UnsatisfiedLinkError: no HelloWorld2 in java.library.path at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1860) at java.lang.Runtime.loadLibrary0(Runtime.java:845) at java.lang.System.loadLibrary(System.java:1084) at com.study.jnilearn.HelloWorld.<clinit>(HelloWorld.java:13) ~~~ 大家从异常中可以看出来,他是在java.library.path中查找该名称对应的动态库,如果在mac下找libHelloWorld.jnilib文件,linux下找libHelloWorld.so文件,windows下找libHelloWorld.dll文件,可以通过调用System.getProperties("java.library.path")方法获取查找的目录列表,下面是我本机mac os x 系统下的查找目录: ~~~ String libraryDirs = System.getProperty("java.library.path"); System.out.println(libraryDirs); // 输出结果如下: /Users/yangxin/Library/Java/Extensions:/Library/Java/Extensions:/Network/Library/Java/Extensions:/System/Library/Java/Extensions:/usr/lib/java:. ~~~ 有两种方式可以让java从java.library.path找到动态链接库文件,聪明的你应该已经想到了。 方式1:将动态链接库拷贝到java.library.path目录下 方式2:给jvm添加“-Djava.library.path=动态链接库搜索目录”参数,指定系统属性java.library.path的值 java -Djava.library.path=/Users/yangxin/Desktop **Linux/Unix环境下可以通过设置LD_LIBRARY_PATH环境变量,指定库的搜索目录。** ***费了那么大劲,终于可以运行写好的Java程序了,结果如下:*** ~~~ yangxin-MacBook-Pro:JNILearn yangxin$ java -classpath ./bin com.study.jnilearn.HelloWorld Java Str:yangxin hello yangxin ~~~ 如果没有将动态库拷贝到本地库搜索目录下,执行java命令,可通过添加系统属性java.library.path来指定动态库的目录,如下所示: ~~~ yangxin-MacBook-Pro:JNILearn yangxin$ java -Djava.library.path=/Users/yangxin/Desktop -classpath ./bin com.study.jnilearn.HelloWorld Java Str:yangxin hello yangxin ~~~
';

JNI/NDK开发指南(开山篇)

最后更新于:2022-04-01 07:38:42

相信很多做过Java或Android开发的朋友经常会接触到JNI方面的技术,由其做过Android的朋友,为了应用的安全性,会将一些复杂的逻辑和算法通过本地代码(C或C++)来实现,然后打包成so动态库文件,并提供Java接口供应用层调用,这么做的目的主要就是为了提供应用的安全性,防止被反编译后被不法分子分析应用的逻辑。当然打包成so也不能说完全安全了,只是相对反编译Java的class字节码文件来说,反汇编so动态库来分析程序的逻辑要复杂得多,没那么容易被破解。比如百度开放平台提供的定位服务、搜索服务、LBS服务、推送服务的Android SDK,除了Java接口的jar包之外,还有一个.so文件,这个so就是实现了Java层定义的native接口的动态库。用兴趣的朋友可以了解一下相关服务的接口:[http://lbsyun.baidu.com/sdk/download](http://lbsyun.baidu.com/sdk/download)。 以前公司有一个JavaWeb的项目,其中有一个用户注册的模块,需要验证用户的手机号(流程大家都懂的),由于这个项目的用户量不大,没用采用运营商的短信网关接口,直接采购了一台16口的短信猫设备和SIM卡来解决这个事情。由于短信猫设备只提供了C的接口,而Java是不能直接与C语言进行交互的,所以JNI就派上用场了,先在Java层定义好发送短信、接收短信、短信发送队列等相关native方法,然后用javah命令将定义Java native接口的class字节码文件生成.h头文件(这个后面会讲到),最后用设备场商提供的C接口来实现java的native方法,完了之后编译成dll或so动态库,提供给Java程序使用即可。 JNI在[Cocos2d-x](http://cn.cocos2d-x.org/)游戏引擎中也经常用到,该引擎是用纯C++开发的,而且是跨平台的,依托C++的跨平台特性,只需用C++编写一次逻辑,就可以将游戏打包发布到不同的平台(IOS、Android、WinPhone、黑莓、Linux、Windows),打包发布的细节就不在这里讨论了。如果游戏要发布到Android平台,开发过程当中,少不了C++层和Java层进行交互,比如游戏当中要打开一个网页、播放一段视频或打开一个新的窗口等,这些在C++层实现是非常麻烦的,如果用Android应用层提供的API就变得相当容易。所以这时就不得不写JNI来完成这些功能的需求。当然这些常用的JNI操作,Cocos2d-x引擎进行了封装,相关的接口定义在JniHelper.cpp这个类中,可以拿来直接使用。(后面会有例子详细介绍) 虽然现在的物联网和智能家居行业还处于萌芽状态,但随着这个时代在技术的创新与不断改进的发展下,想象5年后,物联网和智能家居行业真正成熟起来,由于Android系统的开源,自然会被各大硬件场商所采用,相当于这几年Android智能手机的市场一样,仍然可能会处于移动智能终端的霸主地位。你可能会问,但这和JNI和有什么关系呢?当各种设备接入互联网的同时,自然少不了人机交互的应用程序,当应用程序需要调用硬件特定的功能时,此时只能通过C或C++封装对应功能的JNI接口来供上层应用使用。比如要用手机中的app控制家里的电灯、窗帘、冰箱、空调等一切智能的电子设备时,自然少不了应用要和底层硬件进行通讯,至于各种智能设备的运行控制,自然是由厂商来实现,他们只需提供操作设备相关功能的接口即可。虽然厂商会封装好JNI接口,但我们也要了解下jni与java通讯的原理,以便我们在开发过程当中遇到问题时,能够快速定位到问题。这只是我对未来物联网或智能家居发展的一些猜测,欢迎大家一起讨论! 讲了这么多,我想说明的目的只有一个:JNI在未来的用途很广,现在积累技术就是为未来积累财富!有兴趣的朋友一起来和我学习JNI开发吧。后面我会写一系列从浅入深的JNI/NDK开发文章,系统的介绍JNI开发当中所涉及到的相关技术。首先会讲JNI开发的一些基础知识,每个知识点都会结合一个案例来贯通,最后讲NDK开发,NDK这块主要讲编译环境的配置、Android.mk的编写、模块的编译与NDK编译系统的介绍,因为NDK接口的开发和JNI是一样的(这里不讲NDK开发应用方面的知识)。有兴趣的朋友请关注我的博客。下面是后续文章的大纲: 1、[JNI开发流程(不同操作系统环境下编译的动态库)(用一个HelloWorld示例拉开JNI开发的序幕)](http://blog.csdn.net/xyang81/article/details/41777471) 2、[JVM查找java native方法的规则](http://blog.csdn.net/xyang81/article/details/41854185) 3、[JNI数据类型及与Java数据类型的映射关系](http://blog.csdn.net/xyang81/article/details/42047899) 4、[JNI字符串处理](http://blog.csdn.net/xyang81/article/details/42066665) 5、[访问数组(基本类型数组与对象数组)](http://blog.csdn.net/xyang81/article/details/42346165) 6、[C/C++访问AVA静态方法和实例方法](http://blog.csdn.net/xyang81/article/details/42582213) 7、[C/C++访问JAVA实例变量和静态变量](http://blog.csdn.net/xyang81/article/details/42836783) 8、[调用构造方法和实例方法](http://blog.csdn.net/xyang81/article/details/44002089) 9、[JNI调用性能测试及优化](http://blog.csdn.net/xyang81/article/details/44279725) 10、[JNI局部引用、全局引用和弱全局引用](http://blog.csdn.net/xyang81/article/details/44657385) 11、[异常处理](http://blog.csdn.net/xyang81/article/details/45770551) 12、多线程 13、本地代码嵌入JVM 14、JNI开发的一些注意事项 15、常见错误分享(局部引用表溢出、本地线程未附加到JVM中的问题) 16、NDK开发环境建 17、NDK编译系统详解 18、NDK开发综合实例(Android、Cocos2d-x)
';

前言

最后更新于:2022-04-01 07:38:40

> 原文出处:[JNI/NDK开发指南](http://blog.csdn.net/column/details/blogjnindk.html) 作者:[杨信](http://blog.csdn.net/xyang81) **本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!** # JNI/NDK开发指南 > 系统介绍JNI/NDK开发方面的知识及示例,包括各平台环境下JNI开发流程、JNI数据类型、JNI函数查找命名规则、字符串处理、本地代码访问Java的属性和方法、局部引用与全局引用、开发当中常见错误分享、NDK开发环境搭建、NDK编译系统详解和NDK开发综合案例等。
';