5.【干货】火爆全网的《超全NDK精品教程》JNI 异常处理
Exception
为了确保Java、C/C++代码可以正常执行下去,需要:
在JNI层手动清空异常信息(ExceptionClear),保证代码可以运行。
补救措施保证C/C++代码继续运行。
c层判断java层的异常
步骤:
1.判断是否有异常
2.1 清楚异常
2.2 抛出异常到java
用户可以手动通过ThrowNew函数抛出异常,同样可以被Java代码捕获:
//异常处理
JNIEXPORT void JNICALL Java_com_test_JniTest_testException1
(JNIEnv * env, jobject jobj){
jclass clz = (*env)->GetObjectClass(env, jobj);
//属性名字不小心写错了,拿到的是空的jfieldID
jfieldID fid = (*env)->GetFieldID(env, clz, "key1", "Ljava/lang/String;");
jthrowable err = (*env)->ExceptionOccurred(env);
if (err != NULL){
//手动清空异常信息,保证Java代码能够继续执行
(*env)->ExceptionClear(env);
//提供补救措施,例如获取另外一个属性
fid = (*env)->GetFieldID(env, clz, "key", "Ljava/lang/String;");
}
jstring key = (*env)->GetObjectField(env, jobj, fid);
char* c_str = (*env)->GetStringUTFChars(env, key, NULL);
}
// 两种方式解决
// 1. 补救措施, 不拿 name3 拿 name
// 1.1 有没有异常
jthrowable throwable = (*env)->ExceptionOccurred(env);
/*if (throwable){
// 补救措施,先把异常清除
printf("native have a exception");
// 清除异常
(*env)->ExceptionClear(env);
// 重新获取 name 属性
jfid = (*env)->GetStaticFieldID(env, j_clz, "name", "Ljava/lang/String;");
}*/
// 2. 想给 java 层抛一个异常
if (throwable){
// 清除异常
(*env)->ExceptionClear(env);
// 给 java 层抛 一个 Throwable 异常
// 第一种方式,直接把异常抛给 java 层
(*env)->Throw(env, throwable);
// 第二种方式抛异常
// (*env)->ThrowNew(env, no_such_clz, "NoSuchFieldException name3");
return; // 必须 return 如果不的话,程序会接着往下运行,肯定还会crash
}
jstring name = (*env)->NewStringUTF(env, "Eastrise");
(*env)->SetStaticObjectField(env, j_clz, jfid, name);
}
API: 异常处理
jthrowable throwable = (*env)->ExceptionOccurred(env);:正在抛出一个异常的本地引用
(*env)->ExceptionClear(env);:清除异常
(*env)->Throw(env, throwable);:将ExceptionOccurred获取到的异常直接抛给java层
(*env)->ThrowNew(env, no_such_clz, "NoSuchFieldException name3");:抛出自己想抛出的异常
————————————————
非常好:JNI Crash:异常定位与捕获处理
https://www.jianshu.com/p/b6129f110e86
https://juejin.cn/post/7041062858917937165
https://blog.csdn.net/ddxxii/article/details/84781110
java---jni异常处理
异常处理,异常捕获:jni
https://blog.csdn.net/u013718120/article/details/65629074
http://www.droidsec.cn/常见android-native崩溃及错误原因/
总结:
1.c本身代码,通过try catch
2.c调用java 。通过异常检测 ExceptionCheck 。这样就不会奔溃,同时还可以打印异常的信息。
异常处理总结
JNI自己抛出的异常,是Error类型,Java可以通过Throwable或者Error来捕获得到,捕获异常后Java代码可以继续执行下去。在C层可以清空(ExceptionClear),保证try中的代码Java代码继续执行,并且最好要提供补救措施,确保JNI层代码正常继续运行。
用户通过ThrowNew手动抛出的异常,同样可以在Java层捕捉得到。
c层出问题了,java try catch不住的
C++本身的异常处理:
程序运行时常会碰到一些异常情况,例如:
做除法的时候除数为 0;
用户输入年龄时输入了一个负数;
用 new 运算符动态分配空间时,空间不够导致无法分配;
访问数组元素时,下标越界;打开文件读取时,文件不存在。
能够捕获任何异常的 catch 语句
如果希望不论拋出哪种类型的异常都能捕获,可以编写如下 catch 块:
catch(...) {
...
}
实战举例:
try {
in.substract_mean_normalize(mean_vals, norm_vals);
}catch (...) {
return -1;
}
C++:异常捕获;
http://c.biancheng.net/view/422.html
c++常见的错误,和如何处理?
一. 空指针:
说明:
指针引用不能访问地址
指针为空
空指针是很容易出现的一种bug,但是它也很容易被发现和修复。比如以下代码就会报空指针:
操作不能访问地址:
/**
* 空指针
*/
void crashNull() {
//0为其实地址,是不可读写的,会立即崩溃
int *p = 0;
*p = 1;
LOGE("p:%d", *p);
}
解决方案:使用前加非空判断
//判断不为空再进行执行
if (!*p == NULL) {
*p = 1;
LOGE("p:%d", *p);
}
二. 数组越界
说明:
数组越界和野指针有点像,访问了其他地址,如果访问地址是不可读写的,那么立马会Crash(内核给进程发送错误信号SIGSEGV),如果是可读写的地址,修改了该地址内存,造成内存破坏,那么有可能等会在别的地方发生Crash。
解决方案:
所有数组遍历的循环,都要加上越界判断
用下标访问数组的时候,要判断是否越界
通过代码分析工具可以发现绝大部分数组越界问题
int data[10];
//越界赋值,先判断下size
int sizeData = sizeof(data);
for (int i = 0; i < 17; ++i) {
if (sizeData <= 17) {
data[i] = 666;
}
}
LOGE("data[11]:%d", data[11]);
六. 格式化参数错误
需要格式化参数类型的时候,有可能出现错误
比如
int b=0;
LOGE("b:%s", b);
解决方案
在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致。
在GCC的编译选项中加入-wformat,让GCC在编译时检测出此类错误。
六. 除以0
分母为0的这种情况,会很快Crash,一般都是在实际运行环境中还有可能出现,所以编码习惯的时候应该尽量习惯性去判断下
示例代码
/**
* 除以0
*/
void zeroDiv() {
int a = 1;
int b = a / 0; //整数除以0,产生SIGFPE信号,导致Crash
LOGE("b:%d", b);
}
解决方案
在有除法的时候,判断下分母为0的情况
三. 野指针:
说明:指针指向无效地址:
如果该地址是不可读不可写,那么立马会遇到crash(内核给进程发送错误信息SIGSEGV)
如果该指针的地址可写,那么可能等一会才会出现崩溃(其他指针修改了这一块地址),这时候查看调用栈和野指针所在代码部分可能根本没有关联。
/**
* 野指针
*/
void wildPointer() {
//开始没有初始化
int *p;
//中途使用
*p = 1;
}
这种情况不会立马crash,但是是很不安全的,说不定在之后某一时刻就崩溃了
解决方案:
指针变量一定要初始化,特别是结构体或类中的成员变量的指针
使用完后,在不用的情况,尽量执为NULL(如果别的地方也有指针指向这段内存就不好解决)
例如:
int o;
//初始化,指向给o的地址
int *p = &o;
//中途使用赋值,就是给o赋值了
*p = 1;
LOGE("o:%d", *p);
//用完后指向NULL
*p = NULL;
注意: 野指针造成的内存破坏,很难发现,一般需要专业内存检测工具,才能发现这一类的bug
Bug评述
野指针的bug,特别是内存破坏的问题,有时候查起来毫无头绪,没有一点线索,让开发者感觉到很茫然和无助( Bugly上报的堆栈看不出任何问题)。可以说内存破坏bug是服务器稳定性最大的杀手,也是C/C++在开发应用方面相比于其它语言(如Java, C#)的最大劣势之一。
四. 内存泄漏
说明:
c与c++没有像java那样自动回收的GC机制,所以使用完需要手动释放,如果没有释放,就造成内存泄漏。
比如:
jstring str = env->NewStringUTF("哈哈哈哈");
if (str == NULL) {
LOGE("str is null");
return;
}
const char *stringChar = env->GetStringUTFChars(str, 0);
LOGE("str:%s", stringChar);
解决方案:
用完后进行释放
例如:
jstring str = env->NewStringUTF("哈哈哈哈");
if (str == NULL) {
LOGE("str is null");
return;
}
const char *stringChar = env->GetStringUTFChars(str, 0);
LOGE("str:%s", stringChar);
//用完后进行释放
env->DeleteLocalRef(str);
env->ReleaseStringUTFChars(str,stringChar);
五. 堆栈溢出
说明:
与java的堆栈溢出异常一样,jni中一样有堆栈溢出异常,比如超过堆大小,或超过栈深度时候爆出。
解决方案:
对于堆溢出,在做好释放操作,只要做好避免内存泄漏,合理分配内存使用,一般不会出,现在堆大小还是挺大的
对于栈溢出,做递归的时候重复调用方法的时候,考虑好栈的深度,适时的做好切换操作,避免栈溢出
1、内存溢出
内存溢出是指程序在申请内存时没有足够的内存空间供其使用。原因可能如下:
(1)内存中加载的数据过于庞大;
(2)代码中存在死循环;
(3)递归调用太深,导致堆栈溢出等;
(4)内存泄漏最终导致内存溢出;
2、内存泄漏
内存泄漏是指使用new申请内存, 但是使用完后没有使用delete释放内存,导致占用了有效内存。
1.解决java.lang.StackOverflowError: stack size 8MB报错问题:
自己调用,内存异常
递归:循环调用的问题
超过了8M
export NDK_HOME=/Users/mac/Library/Android/sdk/ndk/16.1.4479499/ndk-build
export PATH=$PATH:$NDK_HOME/
/Users/mac/Nokia-AI-sport/nokia-ai/moduleHealth/jni
/Users/mac/Downloads/android-ndk-r16b
2.理解StackOverflowError与OutOfMemoryError
1.递归
2.循环依赖
3.如果这里的对象属性比较多的时候,这种循环调用之后会导致该线程的堆栈异常,最终导致StackOverflowError异常;如何避免这种情况发生呢?可以克隆或者新建对象:
//避免循环调用出现StackOverflowError异常,这里用临时对象studentTemp
https://www.jianshu.com/p/e4e224b87aa3
http://www.imooc.com/article/details/id/291031(非常好)
在Eclipse中JDK的配置中加上 -XX:MaxDirectMemorySize=128 这代码,就行了,默认是64M
如果你确认递归实现没有问题,你可以通过-Xss参数增加栈的大小,这个参数可以在项目配置或命令行指定。
发现不行:
{
try{
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "*****222*error*******error****************************",2);
int *p=NULL;
*p=1;
}catch(Exception){
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "**111111****error*******error****************************",2);
return 9999;
}
不是说奔溃了,还会执行c吗?我发现没有
也不能检查异常:异常检查是对于java
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "*****222*error*******error****************************",2);
int *p=NULL;
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "*****444444444***************************",2);
*p=1;
if(env->ExceptionCheck()) {
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "*****33333333333*error*******error****************************",2);
env->ExceptionDescribe(); // writes to logcat
env->ExceptionClear();
// return 9999999;
}else{
__android_log_print(ANDROID_LOG_DEBUG, "NDK-peng", "正常",2);
// return 555;
}
发现者2个异常不一样,指针如果异常,不能检查到。如果是普通空指针可以
char* a = NULL;
int val1 = a[1] -'0';
LOGE("JNI, process code %d, cnt %d", m, n);
int *p=NULL;
*p=1;
这个会奔溃
int a=1;
int b=0;
int yyyy=a/b;
这个不会奔溃
int a=1;
int b=0;
int yyyy=a/0;
自己些了一个demo,这个不奔溃,但是catch没有效果
#include
#include
extern "C" JNIEXPORT jstring JNICALL
Java_sport_yuedong_com_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject/* this */) {
std::string hello ="333333";
int a =1;
int b =0;
int yyyy = a / b;
try {
}catch (...) {
}
return env->NewStringUTF(hello.c_str());
}
异常检查也是没有效果的
extern "C" JNIEXPORT jstring JNICALL
Java_sport_yuedong_com_myapplication_MainActivity_stringFromJNI(
JNIEnv *env,
jobject/* this */) {
std::string hello ="333333";
try {
}catch (...) {
}
char* a = NULL;
int val1 = a[1] -'0';
if(env->ExceptionCheck()) {
env->ExceptionDescribe(); // writes to logcat
env->ExceptionClear();
}
demo:
extern "C"
JNIEXPORT jstring JNICALL
Java_sport_yuedong_com_myapplication_MyException_stringFromJNI(JNIEnv *env, jobject jobjectOther) {
std::string hello ="22";
std::string error ="error";
jclass jclass = env->FindClass("sport/yuedong/com/myapplication/MyException");
jmethodID mid = env->GetMethodID(jclass, "operation", "()I");
jmethodID mid2 = env->GetMethodID(jclass, "<init>", "()V");
jobject jobject1 = env->NewObject(jclass, mid2);
env->CallIntMethod(jobject1, mid);
jthrowable jthrowable1 = env->ExceptionOccurred();
//c调用java的处理,可以做异常检查
if (jthrowable1) {
env->ExceptionDescribe();
env->ExceptionClear();
return env->NewStringUTF(error.c_str());
}
return env->NewStringUTF(hello.c_str());
}
异常检查:异常检查只有c调用java的时候才有用。 异常检查对c来说没有效果。