5.NDK Android jni開發 異常處理 native奔潰解決(相機圖片美化)

 
程序運行時常會碰到一些異常情況,例如:
  • 做除法的時候除數爲 0;
  • 用戶輸入年齡時輸入了一個負數;
  • 用 new 運算符動態分配空間時,空間不夠導致無法分配;
  • 訪問數組元素時,下標越界;打開文件讀取時,文件不存在。
 

NDK異常信息一般有三個要素:

  1. 信號
  2. 調用棧信息
  3. 寄存器信息
  • 錯誤信號:11是信號量sigNum,SIGSEGV是信號的名字,SEGV_MAPERR是SIGSEGV下的一種類型。
  • 寄存器快照:進程收到錯誤信號時保存下來的寄存器快照,其中PC寄存器存儲的就是下個要運行的指令(出錯的位置)。
  • 調用棧:#00是棧頂,#02是棧底,#02調用#01調用#00方法,#00的方法時libspirit.so中的Spirit類下的testCrash方法,出錯的地方是testCrash方法內彙編偏移17(不是行號哦!)

 

通常的來源有三個:
1、硬件發生異常,即硬件(通常是CPU)檢測到一個錯誤條件並通知Linux內核,內核處理該異常,給相應的進程發送信號。硬件異常的例子包括執行一條異常的機器語言指令,諸如,被0除,或者引用了無法訪問的內存區域。大部分信號如果沒有被進程處理,默認的操作就是殺死進程。在本文中,SIGSEGV(段錯誤),SIGBUS(內存訪問錯誤),SIGFPE(算數異常)屬於這種信號。

2、進程調用的庫發現錯誤,給自己發送中止信號,默認情況下,該信號會終止進程。在本文中,SIGABRT(中止進程)屬於這種信號。

3、用戶(手賤)或第三方App(惡意)通過kill-信號 pid的方式給錯誤進程發送,這時signal中的si_code會小於0。

能夠捕獲任何異常的 catch 語句

如果希望不論拋出哪種類型的異常都能捕獲,可以編寫如下 catch 塊:

catch(...) {
    ...
}

實戰舉例:
   try {
   in.substract_mean_normalize(mean_vals, norm_vals);

} catch (...) {
       return -1;
   }
 
C++:異常捕獲;
c++常見的錯誤,和如何處理?

一. 空指針:

說明:

  1. 指針引用不能訪問地址
  2. 指針爲空

空指針是很容易出現的一種bug,但是它也很容易被發現和修復。比如以下代碼就會報空指針:

  • 操作不能訪問地址:
/**
 * 空指針
 */
void crashNull() {
    //0爲其實地址,是不可讀寫的,會立即崩潰
    int *p = 0;
    *p = 1;
    LOGE("p:%d", *p);
}
 

解決方案:使用前加非空判斷

//判斷不爲空再進行執行
if (!*p == NULL) {
        *p = 1;
        LOGE("p:%d", *p);
    }
 

二. 野指針:

說明:指針指向無效地址:

  1. 如果該地址是不可讀不可寫,那麼立馬會遇到crash(內核給進程發送錯誤信息SIGSEGV)
  2. 如果該指針的地址可寫,那麼可能等一會纔會出現崩潰(其他指針修改了這一塊地址),這時候查看調用棧和野指針所在代碼部分可能根本沒有關聯。
/**
 * 野指針
 */
void wildPointer() {
    //開始沒有初始化
    int *p;
    //中途使用
    *p = 1;
}
這種情況不會立馬crash,但是是很不安全的,說不定在之後某一時刻就崩潰了
 

解決方案:

  1. 指針變量一定要初始化,特別是結構體或類中的成員變量的指針
  2. 使用完後,在不用的情況,儘量執爲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#)的最大劣勢之一。

三. 數組越界
說明:
數組越界和野指針有點像,訪問了其他地址,如果訪問地址是不可讀寫的,那麼立馬會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]);
 

四. 內存泄漏

說明:

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);
 

解決方案:

  1. 用完後進行釋放

例如:

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. 需要格式化參數類型的時候,有可能出現錯誤

比如

int b=0;
    LOGE("b:%s", b);
解決方案
  1. 在書寫輸出格式和參數時,要做到參數個數和類型都要與輸出格式一致。
  2. 在GCC的編譯選項中加入-wformat,讓GCC在編譯時檢測出此類錯誤。
 

六. 除以0

分母爲0的這種情況,會很快Crash,一般都是在實際運行環境中還有可能出現,所以編碼習慣的時候應該儘量習慣性去判斷下

示例代碼

/**

 * 除以0

 */

void zeroDiv() {

    int a = 1;

    int b = a / 0; //整數除以0,產生SIGFPE信號,導致Crash

    LOGE("b:%d", b);

}

 

解決方案

  1. 在有除法的時候,判斷下分母爲0的情況
實戰:
jni異常捕獲:(ok)
非常好:JNI Crash:異常定位與捕獲處理
 
發現不行:
{

 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 <jni.h>
#include <string>

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來說沒有效果。
=============================================
java---jni異常處理
異常處理,異常捕獲:jni
 
1.數據源修改
2.釋放掉數據
可能問題:內存,因爲不是必先的。在低端手機和一些手機內存回收不是很好的
===================
工具函數

JNI中拋異常很經典:找異常類,調用ThrowNew拋出之;所以,可以寫一個工具函數。


 
 

1.解決java.lang.StackOverflowError: stack size 8MB報錯問題:

自己調用,內存異常
遞歸:循環調用的問題
超過了8M
<span style="color:#000000"><span style="color:#cccccc"><code class="language-bash">export NDK_HOME=/Users/mac/Library/Android/sdk/ndk/16.1.4479499/ndk-build
export PATH=$PATH:$NDK_HOME/</code></span></span>
/Users/mac/Nokia-AI-sport/nokia-ai/moduleHealth/jni
/Users/mac/Downloads/android-ndk-r16b

2.理解StackOverflowError與OutOfMemoryError

1.遞歸
2.循環依賴
3.如果這裏的對象屬性比較多的時候,這種循環調用之後會導致該線程的堆棧異常,最終導致StackOverflowError異常;如何避免這種情況發生呢?可以克隆或者新建對象:
     //避免循環調用出現StackOverflowError異常,這裏用臨時對象studentTemp
在Eclipse中JDK的配置中加上 -XX:MaxDirectMemorySize=128 這代碼,就行了,默認是64M
  • 如果你確認遞歸實現沒有問題,你可以通過-Xss參數增加棧的大小,這個參數可以在項目配置或命令行指定。
3.NDK問題 ,so庫類型不對奔潰

ndk {
    abiFilters "armeabi"
}
4.奔潰,因爲找不到方法或者so庫奔潰,方法找不到原因:要包名一樣纔行的
 java.lang.UnsatisfiedLinkError: No implementation found for double
6.NDK版本不對,導致奔潰問題

完美解決 No toolchains found in the NDK toolchains folder for ABI with prefix: mips64el-linux-android

7..地址配置有問題:拼接
Unable to resolve host "open-api1.51yund.comauth": No address associated with hostname
==========================
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章