1. 程式人生 > >Android Native 崩潰日誌收集

Android Native 崩潰日誌收集

通過崩潰捕獲和收集,可以收集到已釋出應用(遊戲)的異常,以便開發人員發現和修改bug,對於提高軟體質量有著極大的幫助。本文介紹了iOS和android平臺下崩潰捕獲和收集的原理及步驟,不過如果是個人開發應用或者沒有特殊限制的話,就不用往下看了,直接把友盟sdk(一個統計分析sdk)加入到工程中就萬事大吉了,其中的錯誤日誌功能完全能夠滿足需求,而且不需要額外準備接收伺服器。  但是如果你對其原理更感興趣,或者像我一樣必須要相容公司現有的bug收集系統,那麼下面的東西就值得一看了。

       要實現崩潰捕獲和收集的困難主要有這麼幾個:

       1、如何捕獲崩潰(比如c++常見的野指標錯誤或是記憶體讀寫越界,當發生這些情況時程式不是異常退出了嗎,我們如何捕獲它呢)

       2、如何獲取堆疊資訊(告訴我們崩潰是哪個函式,甚至是第幾行發生的,這樣我們才可能重現並修改問題)

       3、將錯誤日誌上傳到指定伺服器(這個最好辦)

        我們先進行一個簡單的綜述。會引發崩潰的程式碼本質上就兩類,一個是c++語言層面的錯誤,比如野指標,除零,記憶體訪問異常等等;另一類是未捕獲異常(Uncaught Exception),iOS下面最常見的就是objective-c的NSException(通過@throw丟擲,比如,NSArray訪問元素越界),android下面就是java丟擲的異常了。這些異常如果沒有在最上層try住,那麼程式就崩潰了。  無論是iOS還是android系統,其底層都是unix或者是類unix系統,對於第一類語言層面的錯誤,可以通過訊號機制來捕獲(signal或者是sigaction,不要跟qt的訊號插槽弄混了),即任何系統錯誤都會丟擲一個錯誤訊號,我們可以通過設定一個回撥函式,然後在回撥函式裡面列印併發送錯誤日誌。

      一、iOS平臺的崩潰捕獲和收集

1、設定開啟崩潰捕獲

static int s_fatal_signals[] = {
    SIGABRT,
    SIGBUS,
    SIGFPE,
    SIGILL,
    SIGSEGV,
    SIGTRAP,
	SIGTERM,
	SIGKILL,
};

static const char* s_fatal_signal_names[] = {
	"SIGABRT",
	"SIGBUS",
	"SIGFPE",
	"SIGILL",
	"SIGSEGV",
	"SIGTRAP",
	"SIGTERM",
	"SIGKILL",
};

static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);

void InitCrashReport()
{
        // 1     linux錯誤訊號捕獲
	for (int i = 0; i < s_fatal_signal_num; ++i) {
		signal(s_fatal_signals[i], SignalHandler);
	}
	
        // 2      objective-c未捕獲異常的捕獲
	NSSetUncaughtExceptionHandler(&HandleException);
}

在遊戲的最開始呼叫InitCrashReport()函式來開啟崩潰捕獲。  註釋1處對應上文所說的第一類崩潰,註釋2處對應objective-c(或者說是UIKit Framework)丟擲但是沒有被處理的異常。

2、列印堆疊資訊

+ (NSArray *)backtrace
{
	void* callstack[128];
	int frames = backtrace(callstack, 128);
	char **strs = backtrace_symbols(callstack, frames);
	
	int i;
	NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
	for (i = kSkipAddressCount;
		 i < __min(kSkipAddressCount + kReportAddressCount, frames);
		 ++i) {
	 	[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
	}
	free(strs);
	
	return backtrace;
}

幸好,蘋果的iOS系統支援backtrace,通過這個函式可以直接打印出程式崩潰的呼叫堆疊。優點是,什麼符號函式表都不需要,也不需要儲存釋出出去的對應版本,直接檢視崩潰堆疊。缺點是,不能打印出具體哪一行崩潰,很多問題知道了是哪個函式崩的,但是還是查不出是因為什麼崩的大哭

3、日誌上傳,這個需要看實際需求,比如我們公司就是把崩潰資訊http post到一個php伺服器。這裡就不多做聲明瞭。

4、技巧---崩潰後程序保持執行狀態而不退出

 CFRelease(allModes); 

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
	CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
	
	while (!dismissed)
	{
		for (NSString *mode in (__bridge NSArray *)allModes)
		{
			CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);
		}
	}
	
	CFRelease(allModes);

在崩潰處理函式上傳完日誌資訊後,呼叫上述程式碼,可以重新構建程式主迴圈。這樣,程式即便崩潰了,依然可以正常執行(當然,這個時候是處於不穩定狀態,但是由於手持遊戲和應用大多是短期操作,不會有掛機這種說法,所以穩定與否就無關緊要了)。玩家甚至感受不到崩潰。

這裡要在說明一個感念,那就是“可重入(reentrant)”。簡單來說,當我們的崩潰回撥函式是可重入的時候,那麼再次發生崩潰的時候,依然可以正常執行這個新的函式;但是如果是不可重入的,則無法執行(這個時候就徹底死了)。要實現上面描述的效果,並且還要保證回撥函式是可重入的幾乎不可能。所以,我測試的結果是,objective-c的異常觸發多少次都可以正常執行。但是如果多次觸發錯誤訊號,那麼程式就會卡死。  所以要慎重決定是否要應用這個技巧。

二、android崩潰捕獲和收集

1、android開啟崩潰捕獲

      首先是java程式碼的崩潰捕獲,這個可以仿照最下面的完整程式碼寫一個UncaughtExceptionHandler,然後在所有的Activity的onCreate函式最開始呼叫
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this));

      這樣,當發生崩潰的時候,就會自動呼叫UncaughtExceptionHandler的public void uncaughtException(Thread thread, Throwable exception)函式,其中的exception包含堆疊資訊,我們可以在這個函式裡面列印我們需要的資訊,並且上傳錯誤日誌

    然後是重中之重,jni的c++程式碼如何進行崩潰捕獲。

void InitCrashReport()
{
	CCLOG("InitCrashReport");

    // Try to catch crashes...
    struct sigaction handler;
    memset(&handler, 0, sizeof(struct sigaction));

    handler.sa_sigaction = android_sigaction;
    handler.sa_flags = SA_RESETHAND;

#define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
    CATCHSIG(SIGILL);
    CATCHSIG(SIGABRT);
    CATCHSIG(SIGBUS);
    CATCHSIG(SIGFPE);
    CATCHSIG(SIGSEGV);
    CATCHSIG(SIGSTKFLT);
    CATCHSIG(SIGPIPE);
}

通過singal的設定,當崩潰發生的時候就會呼叫android_sigaction函式。這同樣是linux的訊號機制。 此處設定訊號回撥函式的程式碼跟iOS有點不同,這個只是同一個功能的兩種不同寫法,沒有本質區別。有興趣的可以google下兩者的區別。

2、列印堆疊

      java語法可以直接通過exception獲取到堆疊資訊,但是jni程式碼不支援backtrace,那麼我們如何獲取堆疊資訊呢?    這裡有個我想嘗試的新方法,就是使用google breakpad,貌似它現在完整的跨平臺了(支援windows, mac, linux, iOS和android等),它自己實現了一套minidump,在android上面限制會小很多。  但是這個庫有些大,估計要加到我們的工程中不是一件非常容易的事,所以我們還是使用了簡潔的“傳統”方案。 思路是,當發生崩潰的時候,在回撥函式裡面呼叫一個我們在Activity寫好的靜態函式。在這個函式裡面通過執行命令獲取logcat的輸出資訊(輸出資訊裡面包含了jni的崩潰地址),然後上傳這個崩潰資訊。  當我們獲取到崩潰資訊後,可以通過arm-linux-androideabi-addr2line(具體可能不是這個名字,在android ndk裡面搜尋*addr2line,找到實際的程式)解析崩潰資訊。

      jni的崩潰回撥函式如下:

void android_sigaction(int signal, siginfo_t *info, void *reserved)
{
	if (!g_env)	{
		return;
	}

    jclass classID = g_env->FindClass(CLASS_NAME);
    if (!classID) {
    	return;
    }

    jmethodID methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed", "()V");
    if (!methodID) {
        return;
    }

    g_env->CallStaticVoidMethod(classID, methodID);

    old_sa[signal].sa_handler(signal);
}

可以看到,我們僅僅是通過jni呼叫了java的一個函式,然後所有的處理都是在java層面完成。

java對應的函式實現如下:

public static void onNativeCrashed() {
        // http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on-a
		Log.e("handller", "handle");
        new RuntimeException("crashed here (native trace should follow after the Java trace)").printStackTrace();
        s_instance.startActivity(new Intent(s_instance, CrashHandler.class));
    }

我們開啟了一個新的activity,因為當jni發生崩潰的時候,原始的activity可能已經結束掉了。  這個新的activity實現如下:

public class CrashHandler extends Activity
{
    public static final String TAG = "CrashHandler";
    protected void onCreate(Bundle state)
    {
        super.onCreate(state);
        setTitle(R.string.crash_title);
        setContentView(R.layout.crashhandler);
        TextView v = (TextView)findViewById(R.id.crashText);
        v.setText(MessageFormat.format(getString(R.string.crashed), getString(R.string.app_name)));
        final Button b = (Button)findViewById(R.id.report),
              c = (Button)findViewById(R.id.close);
        b.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                final ProgressDialog progress = new ProgressDialog(CrashHandler.this);
                progress.setMessage(getString(R.string.getting_log));
                progress.setIndeterminate(true);
                progress.setCancelable(false);
                progress.show();
                final AsyncTask task = new LogTask(CrashHandler.this, progress).execute();
                b.postDelayed(new Runnable(){
                    public void run(){
                        if (task.getStatus() == AsyncTask.Status.FINISHED)
                            return;
                        // It's probably one of these devices where some fool broke logcat.
                        progress.dismiss();
                        task.cancel(true);
                        new AlertDialog.Builder(CrashHandler.this)
                            .setMessage(MessageFormat.format(getString(R.string.get_log_failed), getString(R.string.author_email)))
                            .setCancelable(true)
                            .setIcon(android.R.drawable.ic_dialog_alert)
                            .show();
                    }}, 3000);
            }});
        c.setOnClickListener(new View.OnClickListener(){
            public void onClick(View v){
                finish();
            }});
    }

    static String getVersion(Context c)
    {
        try {
            return c.getPackageManager().getPackageInfo(c.getPackageName(),0).versionName;
        } catch(Exception e) {
            return c.getString(R.string.unknown_version);
        }
    }
}

class LogTask extends AsyncTask<Void, Void, Void>
{
    Activity activity;
    String logText;
    Process process;
    ProgressDialog progress; 

    LogTask(Activity a, ProgressDialog p) {
        activity = a;
        progress = p;
    }

    @Override
    protected Void doInBackground(Void... v) {
        try {
        	Log.e("crash", "doInBackground begin");
            process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"});
            logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream());
        	Log.e("crash", "doInBackground end");
        } catch (IOException e) {
            e.printStackTrace();
            Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show();
        }
        return null;
    }

    @Override
    protected void onCancelled() {
    	Log.e("crash", "onCancelled");
        process.destroy();
    }

    @Override
    protected void onPostExecute(Void v) {
    	Log.e("crash", "onPostExecute");
        progress.setMessage(activity.getString(R.string.starting_email));
        UncaughtExceptionHandler.sendLog(logText, activity);
        progress.dismiss();
        activity.finish();
        Log.e("crash", "onPostExecute over");
    }

最主要的地方是doInBackground函式,這個函式通過logcat獲取了崩潰資訊。 不要忘記在AndroidManifest.xml新增讀取LOG的許可權

  1. <uses-permissionandroid:name="android.permission.READ_LOGS"/>
<uses-permission android:name="android.permission.READ_LOGS" />

3、獲取到錯誤日誌後,就可以寫到sd卡(同樣不要忘記新增許可權),或者是上傳。  程式碼很容易google到,不多說了。  最後再說下如何解析這個錯誤日誌。

我們在獲取到的錯誤日誌中,可以擷取到如下資訊:

12-12 20:41:31.807 24206 24206 I DEBUG   : 
12-12 20:41:31.847 24206 24206 I DEBUG   :          #00  pc 004931f8  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #01  pc 005b3a5e  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #02  pc 005aab68  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #03  pc 005ad8aa  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #04  pc 005924a4  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
12-12 20:41:31.847 24206 24206 I DEBUG   :          #05  pc 005929b6  /data/data/org.cocos2dx.wing/lib/libhelloworld.so
004931f8

這個就是我們崩潰函式的地址,  libhelloworld.so就是崩潰的動態庫。我們要使用addr2line對這個動態庫進行解析(注意要是obj/local目錄下的那個比較大的,含有符號檔案的動態庫,不是Libs目錄下比較小的,同時釋出版本時,這個動態庫也要儲存好,之後查log都要有對應的動態庫)。命令如下:

arm-linux-androideabi-addr2line.exe -e 動態庫名稱  崩潰地址

例如:

$ /cygdrive/d/devandroid/android-ndk-r8c-windows/android-ndk-r8c/toolchains/arm-linux-androideabi-4.6/prebuilt/windows/bin/arm-linux-androideabi-addr2line.exe -e obj/local/armeabi-v7a/libhelloworld.so 004931f8

得到的結果就是哪個cpp檔案第幾行崩潰。  如果動態庫資訊不對,返回的就是 ?:0

相關推薦

Android Native 崩潰日誌收集

通過崩潰捕獲和收集,可以收集到已釋出應用(遊戲)的異常,以便開發人員發現和修改bug,對於提高軟體質量有著極大的幫助。本文介紹了iOS和android平臺下崩潰捕獲和收集的原理及步驟,不過如果是個人開發應用或者沒有特殊限制的話,就不用往下看了,直接把友盟sdk(一個統計分析sdk)加入到工程中就萬事大吉了,

android app崩潰日誌收集以及上傳

已經做成sdk的形式,原始碼已公開,原始碼看不懂的請自行google。 如果想定製適應自己app的sdk請自行fork。 AndroidLogCollector android app崩潰日誌收集sdk 1.0 作者:賈博士 崩潰日誌收集方法: 1.L

android app崩潰日誌收集

AndroidLogCollector android app崩潰日誌收集sdk 1.0 作者:賈博士 https://github.com/zhoualen/AndroidLogCollector 崩潰日誌收集方法: 1.LogCollector是lib包,

Android 應用崩潰日誌收集和上傳

如何將應用崩潰日誌收集起來? Android 應用難以避免的會 crash ,也稱為崩潰,無論你的程式多完美,總是無法避免 crash 的發生。這對使用者來說是很不友好的,也是開發者所不願意看到的。更糟糕的是,當用戶發生了 crash ,開發者卻不知道程式為何

Android崩潰日誌收集

crash日誌收集這個已經有許多成熟的第三方庫實現了這個功能了,大多是以服務的方式提供,這裡有個上傳日誌的需求,在使用者本地的資訊需要採集和資料處理。 如果是創業公司,國內建議使用網易雲捕或者騰訊Bugly 國外建議使用Crashlytics 如果要自己搭建伺服器

Android開發之app崩潰日誌收集以及上傳

已經做成sdk的形式,原始碼已公開,原始碼看不懂的請自行google。 如果想定製適應自己app的sdk請自行fork。 AndroidLogCollector android app崩潰日誌收集sdk 1.0 作者:賈博士 崩潰日誌收集方法: 1.Lo

移動應用崩潰日誌收集工具對比

背景 移動網際網路時代,由於 Android 裝置的碎片化,客服人員每天要接到很多使用者反饋在各種不同機型上的崩潰問題,又沒有辦法提供具體的 Crash 日誌給開發人員。測試人員每天需要對使用者的反饋

iOS 崩潰日誌 收集與傳送伺服器

iOS開發中我們會遇到程式丟擲異常退出的情況,如果是在除錯的過程中,異常的資訊是一目瞭然,我們可以很快的定位異常的位置並解決問題。那麼當應用已經打包,iPhone裝置通過ipa的包

Android獲取崩潰日誌並上傳到郵箱

好吧,老闆要獲取崩潰日誌並上傳伺服器,已經實現了,這個比較簡單,主要說說上傳到郵箱的一個主意的地方:上傳到伺服器和郵箱需要4個jar包(android-crash-1.0.jar,activation.jar,additionnal.jar,mail.jar),我是在別人的部

iOS APP 崩潰日誌收集

推薦Bugly(只用過這個第三方的) 使用時也很簡單,在Bugly中心新建產品,它會生成一個appid給你,你需要做的最後一步就是在appdelegate裡新增如下程式碼 - (BOOL)application:(UIApplication *)applicatio

iOS 崩潰日誌收集及分析

最近幾天,專案中在增加推送功能,選用的極光推送SDK,相信大家也都用過,官方文件的整合步驟很詳細,整合也很容易。但是這跟今天的主題有什麼關係呢??? 黑人問號???別急,下面就來說說我今天的遭遇。坑~~~ 話說,由於iOS10之後,蘋果對推送進行了重大更新,主要是新增了 U

騰訊Bugly,簡單實用的崩潰日誌收集

2011年初- 2014年10月 Bugly 服務於騰訊內部所有專案,如QQ郵箱、瀏覽器、手機QQ、騰訊視訊等。 2014年10月起,騰訊 Bugly 對外開放給更多的開發者使用,幫助開發人員更準確高效的定位解決問題。 對產生的問題進行24小時的監控,把握崩潰前後的各個時間節點。Bugly 目前支援

iOS 原生的崩潰日誌收集與傳送一

崩潰日誌工具類的建立 MyCrashExceptionHandler.h #import <Foundation/Foundation.h> @interface MyCras

Android收集崩潰日誌並上傳

public class CrashHandler implements Thread.UncaughtExceptionHandler { public static final String TAG = "CrashHandler"; // 系統預設的

Android收集程式崩潰日誌

開個頭 程式崩潰是我們開發人員最不想看到的,但也是我們不可避免的。在我們開發階段,當程式發生崩潰的時候,我們需要根據列印的錯誤日誌來定位,分析,解決錯誤。但是當我們把應用釋出到應用市場的之後,使用者使用我們應用的時候因為各種原因程式發生了崩潰,這個是非常影響使

Android 應用程式崩潰日誌捕捉

程式崩潰是應用迭代中不可避免的問題,即使有著5年或者10年經驗的程式猿也無法完全保證自己的程式碼沒有任何的bug導致崩潰,現在有一些第三方平臺可以幫助我們蒐集應用程式的崩潰,比如友盟,詳情如下圖 雖然能夠看到崩潰的日誌以及機型等,但還是不是很方便,如果需要精確定位的話需要使用者提供崩潰的時間點、機型

生產級部署 Python 指令碼,日誌收集崩潰自啟,一鍵搞定

今天介紹一個生產級的流程管理工具 PM2,通常我們說到 PM2 的時候,都是在說如何部署 Node.js 程式,但是實際上 PM2 很強大,不僅僅可以用來管理 Node.js,它還可以用來管理 Python、PHP、Ruby、perl 等等。 這裡就以 Python 舉

Android-Fk:[開源框架] 安卓崩潰資訊收集框架ACRA原理流程

Android-Fk:[開源框架] 安卓崩潰資訊收集框架ACRA原理流程 本文主要梳理ACRA原理及程式碼流程 順序圖的uml檔案 簡化圖的draw.io原始檔 分享至百度網盤 https://pan.baidu.com/s/1zAapEu9mmOZsTMDlCRCRQg 一. 學習

Android Firebase接入(三)--Firebase 崩潰日誌報告(Crashlytics)

Firebase崩潰日誌報告可以自動記錄應用內崩潰資訊,只需簡單的幾步,就可以將Firebase Crashlytics新增到安卓工程中,然後Firebase Crashlytics就會自動的收集應用內崩潰資訊,包括錯誤型別,程式碼定位等等,非常的方便實用。 一、配置A

Android自定義錯誤日誌收集

一、概述 一般做Android開發的朋友多多少少都會碰見各種各樣的問題,一般都怎麼解決這些bug尼?有的朋友會說Debug,但是有沒有想過,萬一客戶上線了尼?打過電話,說軟體出錯了,那這個時候如果不做錯誤收集,那麼就會無法知道發生什麼問題了,這個時候就需要我們自己手動的做錯