安卓捕獲RuntimeException,ANR,Native信號異常

三大崩潰

衆所周知,安卓端有三大崩潰,都會造成應用崩掉,分別是

  1. RuntimeException
    1. java端的運行時異常.比如一些空指針之類的,發生時應用會崩潰.
  2. ANR
    1. 安卓爲了用戶體驗設的保護機制,在應用在主線程做耗時操作的時候,長時間無響應會產生,一個問用戶是否要繼續等待的選擇框,若用戶選擇關閉,或者長時間不選擇,都會造成應用關閉.
  3. Native信號異常
    1. 當我們的代碼導入第三方的so包的時候,由於c/c++代碼的一些問題,產生native信號,就會造成應用直接崩掉,然後報一大堆的彙編的堆棧信息.

下面就分別講講如何捕獲這三種異常

捕獲RuntimException

 

/**
 * <p>
 * <h1>捕獲java運行時異常(Runtime-Exception)</h1>
 * <p>
    Uncaught異常發生時會終止線程,此時,系統便會通知UncaughtExceptionHandler,告訴它被終止的線程以及對應的異常,
    然後便會調用uncaughtException函數。如果該handler沒有被顯式設置,則會調用對應線程組的默認handler。
    如果我們要捕獲該異常,必須實現我們自己的handler,並通過以下函數進行設置:
    <p>
        MyCrashHandler myCrashHandler = new MyCrashHandler();
        Thread.setDefaultUncaughtExceptionHandler(myCrashHandler);
    <p>
    實現自定義的handler,只需要繼承JavaCrashHandler,並實現myUncaughtExceptionToDo方法即可。
*/
public class JavaCrashHandler implements UncaughtExceptionHandler{  
    @Override  
    public void uncaughtException(Thread thread, final Throwable throwable) {  
        // 捕獲異常
        String stackTraceInfo = getStackTraceInfo(throwable);
        
        myUncaughtExceptionToDo();
        
    }


    /**
     * 自定義的對異常的處理
     */
    public void myUncaughtExceptionToDo() {
//      // 重啓應用
        
    }


    /**
     * <h1>獲取Exception崩潰堆棧</h1>
     * <p>
                捕獲Exception之後,我們還需要知道崩潰堆棧的信息,這樣有助於我們分析崩潰的原因,查找代碼的Bug。
                異常對象的printStackTrace方法用於打印異常的堆棧信息,根據printStackTrace方法的輸出結果,
                我們可以找到異常的源頭,並跟蹤到異常一路觸發的過程。
    */
    public static String getStackTraceInfo(final Throwable throwable) {
        String trace = "";
        try {
            Writer writer = new StringWriter();
            PrintWriter pw = new PrintWriter(writer);
            throwable.printStackTrace(pw);
            trace = writer.toString();
            pw.close();
        } catch (Exception e) {
            return "";
        }
        return trace;
    }
}

捕獲ANR

ANR是無法捕獲的,但是你可以在事後收到該消息,做你需要做的操作

 

/**
 * 主要通過收聽ANR的廣播,來檢測是否發生ANR的現象,但是無法阻止ANR
 * 
 * 
    在onReceive()裏面做判斷
        if (intent.getAction().equals(ACTION_ANR)) {
            // do you want to do
        }
 * 
 * @author aaa
 *
 */
public class ANRCacheHelper {
    
    private static MyReceiver myReceiver;
    public static void registerANRReceiver(Context context){
        myReceiver = new MyReceiver();
        context.registerReceiver(myReceiver, new IntentFilter(ACTION_ANR));
    }
    
    public static void unregisterANRReceiver(Context context){
        if (myReceiver == null) {
            return;
        }
        context.unregisterReceiver(myReceiver);
    }

    private static final String ACTION_ANR = "android.intent.action.ANR";
    private static class MyReceiver extends BroadcastReceiver {
        
        @Override
        public void onReceive(Context context, Intent intent) {
            
            if (intent.getAction().equals(ACTION_ANR)) {
                // to do
            }
        
        }
    }
}

捕獲Native信號異常

NativeCacheHandler.java

 

package com.wtest.wlib.android.catchs;

import android.util.Log;

/**
 * 捕獲本地的native信號異常
 * @author aaa
 *
 */
public class NativeCacheHandler {
    static {
        // 寫Android.mk文件中定義好的類庫名
        // 也就是把libs/armeabi/libwlibcatchs.so這個文件,掐頭去尾
        System.loadLibrary("wlibcatchs");
    }
    
    /**
     * 註冊捕獲本地Native信號異常
     */
    public void registerNativeCacheHandler(){
        nativeRegisterHandler();
    }
    
    /**
     * 發生本地native信號異常的時候,會回調到這裏來
     */
    public void onNativeCrashed() {
        Log.d("wtest", "捕獲到本地異常,執行到這裏");
//        new RuntimeException("crashed here (native trace should follow after the Java trace)").printStackTrace();
//        startActivity(new Intent(this, MainActivity.class));
    }
    
    /**
     * 警告!!!
     * 用於測試,故意製造一個本地信號異常,非測試不要用該函數
     * 
     */
    @Deprecated
    public void makeError() {
        nativeMakeError();
    }
    
    
    private native int nativeRegisterHandler();
    private native boolean nativeMakeError();
}

NativeCacheHandler.cpp

 

/**
    預處理指令是以#號開頭的代碼行。
    #號必須是該行除了任何空白字符外的第一個字符。
    #後是指令關鍵字,在關鍵字和#號之間允許存在任意個數的空白字符。
    整行語句構成了一條預處理指令,該指令將在編譯器進行編譯之前對源代碼做某些轉換。
 */

// #include包含一個源代碼文件
#include <jni.h>    // 使用jni進行java和c語言互相調用,必須導入的頭文件
#include <stdlib.h> // <stdlib.h> 頭文件裏包含了C語言的中最常用的系統函數
#include <signal.h> // 在signal.h頭文件中,提供了一些函數用以處理執行過程中所產生的信號。
//#include "NativeActivity.hpp"
#include <android/log.h> // 谷歌提供的用於安卓JNI輸出log日誌的頭文件

// 條件編譯:即可以設置不同的條件,在編譯時編譯不同的代碼,預編譯指令中的表達式與C語言本身的表達式基本一至如邏輯運算、算術運算、位運算等均可以在預編譯指令中使用。
#ifdef MIKMOD   // #ifdef如果宏已經定義,則編譯下面代碼
//  #include "mikmod_build.h"
#endif  // 預處理指令#endif用來限定#ifdef命令的範圍

#define DO_TRY  // #define定義宏
#define DO_CATCH(loc)

#define CATCH_SIGNALS

extern "C" // 在C++中調用C庫函數,就需要在C++程序中用extern “C”聲明要引用的函數。這是給鏈接器用的,告訴鏈接器在鏈接的時候用C函數規範來鏈接。主要原因是C++和C程序編譯完成後在目標代碼中命名規則不同。
{

    #ifdef CATCH_SIGNALS
        static struct sigaction old_sa[NSIG];

        /**
            JNIEnv類中有很多函數可以用:
            NewObject:  創建Java類中的對象
            NewString:  創建Java類中的String對象
            New<Type>Array: 創建類型爲Type的數組對象
            Get<Type>Field: 獲取類型爲Type的字段
            Set<Type>Field: 設置類型爲Type的字段的值
            GetStatic<Type>Field:   獲取類型爲Type的static的字段
            SetStatic<Type>Field:   設置類型爲Type的static的字段的值
            Call<Type>Method:   調用返回類型爲Type的方法
            CallStatic<Type>Method: 調用返回值類型爲Type的static方法
            等許多的函數,具體的可以查看jni.h文件中的函數名稱。
         */
        static JNIEnv *g_sigEnv; // 定義一個靜態的JNIEnv類型的指針變量 // JNIEnv類型實際上代表了Java環境,通過這個JNIEnv* 指針,就可以對Java端的代碼進行操作。
                                // 例如,創建Java類中的對象,調用Java對象的方法,獲取Java對象中的屬性等等。JNIEnv的指針會被JNI傳入到本地方法的實現函數中來對Java端的代碼進行操作。
        static jobject g_sigObj; // 如果native方法不是static的話,這個obj就代表這個native方法的類實例
                                // 如果native方法是static的話,這個obj就代表這個native方法的類的class對象實例(static方法不需要類實例的,所以就代表這個類的class對象)
        static jmethodID g_sigNativeCrashed;

        // 信號處理函數,捕獲到底層信號異常會執行到這裏
        void android_sigaction(int signal, siginfo_t *info, void *reserved)
        {
            // 回調之前定義的要回調的java裏面的函數
            g_sigEnv->CallVoidMethod(g_sigObj, g_sigNativeCrashed);
            old_sa[signal].sa_handler(signal);
        }
    #endif

    static jshortArray g_audioSamples;

    // 當Android的VM(Virtual Machine)執行到System.loadLibrary()函數時,首先會去執行C組件裏的JNI_OnLoad()函數。
    // 當沒有JNI_OnLoad()函數時,Android調試信息會做出如下提示(No JNI_OnLoad found)
    // 13:53:12.204: D/dalvikvm(361): No JNI_OnLoad found in /data/data/com.conowen.helloworld/lib/libHelloWorld.so 0x44edea98, skipping init
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved)
    {
        JNIEnv *env = NULL;
        /*JavaVM::GetEnv 原型爲 jint (*GetEnv)(JavaVM*, void**, jint);
             * GetEnv()函數返回的  Jni 環境對每個線程來說是不同的,
             *  由於Dalvik虛擬機通常是Multi-threading的。每一個線程調用JNI_OnLoad()時,
             *  所用的JNI Env是不同的,因此我們必須在每次進入函數時都要通過vm->GetEnv重新獲取
             *
             */
            //得到JNI Env
        if (jvm->GetEnv((void **)&env, JNI_VERSION_1_2))
            return JNI_ERR;

        jclass cls = env->FindClass("com/wtest/wlib/android/catchs/NativeCacheHandler");

        #ifdef CATCH_SIGNALS
            g_sigEnv = env;
            g_sigNativeCrashed = env->GetMethodID(cls, "onNativeCrashed", "()V");
        #endif

        return JNI_VERSION_1_2;
    }

    JNIEXPORT jint JNICALL
    Java_com_wtest_wlib_android_catchs_NativeCacheHandler_nativeRegisterHandler(JNIEnv *env, jobject this_)
    {
        #ifdef CATCH_SIGNALS
            // Try to catch crashes...
            g_sigObj = env->NewGlobalRef(this_); // 全局引用在一個本機方法的多次不同調用之間使用。他們只能通過使用NewGlobalRef函數來創建。
            struct sigaction handler;   // struct定義結構體(類似於java中的javabean)
            memset( // c庫<string.h>下的函數,void *memset(void *buffer, int c, int count); 把buffer所指內存區域的前count個字節設置成字符c
                    &handler,   // 參1:指向要填充的內存塊。
                    0,  // 參2:要被設置的值。該值以 int 形式傳遞,但是函數在填充內存塊時是使用該值的無符號字符形式。
                    sizeof( // 用於獲取任何東西的內存大小
                            struct sigaction)); // 參3:要被設置爲該值的字節數。


    //        // 結構體sigaction包含了對特定信號的處理、信號所傳遞的信息、信號處理函數執行過程中應屏蔽掉哪些函數等等。
    //        struct sigaction {
    //            void     (*sa_handler)(int); // 指定對signum信號的處理函數,可以是SIG_DFL默認行爲,SIG_IGN忽略接送到的信號,或者一個信號處理函數指針。這個函數只有信號編碼一個參數。
    //            void     (*sa_sigaction)(int, siginfo_t *, void *); // 當sa_flags中存在SA_SIGINFO標誌時,sa_sigaction將作爲signum信號的處理函數。
    //            sigset_t   sa_mask;   // 指定信號處理函數執行的過程中應被阻塞的信號。
    //            int        sa_flags; // 指定一系列用於修改信號處理過程行爲的標誌,由0個或多個標誌通過or運算組合而成,比如SA_RESETHAND,SA_ONSTACK | SA_SIGINFO。
    //            void     (*sa_restorer)(void); // 已經廢棄,不再使用。
    //        }

            // 設置信號處理函數
            handler.sa_sigaction = android_sigaction;
            // 信號處理之後重新設置爲默認的處理方式。
                        //    SA_RESTART:使被信號打斷的syscall重新發起。
                        //    SA_NOCLDSTOP:使父進程在它的子進程暫停或繼續運行時不會收到 SIGCHLD 信號。
                        //    SA_NOCLDWAIT:使父進程在它的子進程退出時不會收到SIGCHLD信號,這時子進程如果退出也不會成爲僵 屍進程。
                        //    SA_NODEFER:使對信號的屏蔽無效,即在信號處理函數執行期間仍能發出這個信號。
                        //    SA_RESETHAND:信號處理之後重新設置爲默認的處理方式。
                        //    SA_SIGINFO:使用sa_sigaction成員而不是sa_handler作爲信號處理函數。
            handler.sa_flags = SA_RESETHAND;

            // 註冊信號處理函數
                // 參1  代表信號編碼,可以是除SIGKILL及SIGSTOP外的任何一個特定有效的信號,如果爲這兩個信號定義自己的處理函數,將導致信號安裝錯誤。
                // 參2  指向結構體sigaction的一個實例的指針,該實例指定了對特定信號的處理,如果設置爲空,進程會執行默認處理。
                // 參3  和參數act類似,只不過保存的是原來對相應信號的處理,也可設置爲NULL。
            #define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
            CATCHSIG(SIGILL);   // 信號4   非法指令
            CATCHSIG(SIGABRT);  // 信號6   來自abort函數的終止信號
            CATCHSIG(SIGBUS);   // 信號7   總線錯誤
            CATCHSIG(SIGFPE);   // 信號8   浮點異常
            CATCHSIG(SIGSEGV);  // 信號11   無效的存儲器引用(段故障)
            CATCHSIG(SIGSTKFLT);// 信號16 協處理器上的棧故障
            CATCHSIG(SIGPIPE);  // 信號13   向一個沒有讀用戶的管道做寫操作
        #endif

    }


    JNIEXPORT jboolean JNICALL
    Java_com_wtest_wlib_android_catchs_NativeCacheHandler_nativeMakeError()
    {

        // 故意製造一個信號11異常
        char *ptr = NULL;   // 賦值爲NULL,空指針,值爲0
            *ptr = '!'; // ERROR HERE! // 因爲在大多數操作系統中,程序不允許訪問地址爲 0 的內存,因爲該內存是操作系統保留的
    }

}



作者:wangwox
鏈接:https://www.jianshu.com/p/bb658019f97b
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章