android和iOS平台的崩潰捕獲和收集


通過崩潰捕獲和收集,可以收集到已發布應用(游戲)的異常,以便開發人員發現和修改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、設置開啟崩潰捕獲

 

  1. staticint s_fatal_signals[] = { 
  2.     SIGABRT, 
  3.     SIGBUS, 
  4.     SIGFPE, 
  5.     SIGILL, 
  6.     SIGSEGV, 
  7.     SIGTRAP, 
  8.     SIGTERM, 
  9.     SIGKILL, 
  10. }; 
  11.  
  12. staticconstchar* s_fatal_signal_names[] = { 
  13.     "SIGABRT"
  14.     "SIGBUS"
  15.     "SIGFPE"
  16.     "SIGILL"
  17.     "SIGSEGV"
  18.     "SIGTRAP"
  19.     "SIGTERM"
  20.     "SIGKILL"
  21. }; 
  22.  
  23. staticint s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]); 
  24.  
  25. void InitCrashReport() 
  26.         // 1     linux錯誤信號捕獲 
  27.     for (int i = 0; i < s_fatal_signal_num; ++i) { 
  28.         signal(s_fatal_signals[i], SignalHandler); 
  29.     } 
  30.      
  31.         // 2      objective-c未捕獲異常的捕獲 
  32.     NSSetUncaughtExceptionHandler(&HandleException); 
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、打印堆棧信息

 

  1. + (NSArray *)backtrace 
  2.     void* callstack[128]; 
  3.     int frames = backtrace(callstack, 128); 
  4.     char **strs = backtrace_symbols(callstack, frames); 
  5.      
  6.     int i; 
  7.     NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; 
  8.     for (i = kSkipAddressCount; 
  9.          i < __min(kSkipAddressCount + kReportAddressCount, frames); 
  10.          ++i) { 
  11.         [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; 
  12.     } 
  13.     free(strs); 
  14.      
  15.     return backtrace; 
+ (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、技巧---崩潰后程序保持運行狀態而不退出

 

  1. CFRunLoopRef runLoop = CFRunLoopGetCurrent(); 
  2.     CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); 
  3.      
  4.     while (!dismissed) 
  5.     { 
  6.         for (NSString *mode in (__bridge NSArray *)allModes) 
  7.         { 
  8.             CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false); 
  9.         } 
  10.     } 
  11.      
  12.     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++代碼如何進行崩潰捕獲。

 

  1. void InitCrashReport() 
  2.     CCLOG("InitCrashReport"); 
  3.  
  4.     // Try to catch crashes... 
  5.     struct sigaction handler; 
  6.     memset(&handler, 0, sizeof(struct sigaction)); 
  7.  
  8.     handler.sa_sigaction = android_sigaction; 
  9.     handler.sa_flags = SA_RESETHAND; 
  10.  
  11. #define CATCHSIG(X) sigaction(X, &handler, &old_sa[X]) 
  12.     CATCHSIG(SIGILL); 
  13.     CATCHSIG(SIGABRT); 
  14.     CATCHSIG(SIGBUS); 
  15.     CATCHSIG(SIGFPE); 
  16.     CATCHSIG(SIGSEGV); 
  17.     CATCHSIG(SIGSTKFLT); 
  18.     CATCHSIG(SIGPIPE); 
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的崩潰回調函數如下:

 

  1. void android_sigaction(int signal, siginfo_t *info, void *reserved) 
  2.     if (!g_env) { 
  3.         return
  4.     } 
  5.  
  6.     jclass classID = g_env->FindClass(CLASS_NAME); 
  7.     if (!classID) { 
  8.         return
  9.     } 
  10.  
  11.     jmethodID methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed", "()V"); 
  12.     if (!methodID) { 
  13.         return
  14.     } 
  15.  
  16.     g_env->CallStaticVoidMethod(classID, methodID); 
  17.  
  18.     old_sa[signal].sa_handler(signal); 
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對應的函數實現如下:

 

  1. publicstaticvoid onNativeCrashed() { 
  2.         // http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on-a 
  3.         Log.e("handller", "handle"); 
  4.         new RuntimeException("crashed here (native trace should follow after the Java trace)").printStackTrace(); 
  5.         s_instance.startActivity(new Intent(s_instance, CrashHandler.class)); 
  6.     } 
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實現如下:

 

 

  1. publicclass CrashHandler extends Activity 
  2.     publicstaticfinal String TAG = "CrashHandler"
  3.     protectedvoid onCreate(Bundle state) 
  4.     { 
  5.         super.onCreate(state); 
  6.         setTitle(R.string.crash_title); 
  7.         setContentView(R.layout.crashhandler); 
  8.         TextView v = (TextView)findViewById(R.id.crashText); 
  9.         v.setText(MessageFormat.format(getString(R.string.crashed), getString(R.string.app_name))); 
  10.         final Button b = (Button)findViewById(R.id.report), 
  11.               c = (Button)findViewById(R.id.close); 
  12.         b.setOnClickListener(new View.OnClickListener(){ 
  13.             publicvoid onClick(View v){ 
  14.                 final ProgressDialog progress = new ProgressDialog(CrashHandler.this); 
  15.                 progress.setMessage(getString(R.string.getting_log)); 
  16.                 progress.setIndeterminate(true); 
  17.                 progress.setCancelable(false); 
  18.                 progress.show(); 
  19.                 final AsyncTask task = new LogTask(CrashHandler.this, progress).execute(); 
  20.                 b.postDelayed(new Runnable(){ 
  21.                     publicvoid run(){ 
  22.                         if (task.getStatus() == AsyncTask.Status.FINISHED) 
  23.                             return
  24.                         // It's probably one of these devices where some fool broke logcat. 
  25.                         progress.dismiss(); 
  26.                         task.cancel(true); 
  27.                         new AlertDialog.Builder(CrashHandler.this
  28.                             .setMessage(MessageFormat.format(getString(R.string.get_log_failed), getString(R.string.author_email))) 
  29.                             .setCancelable(true
  30.                             .setIcon(android.R.drawable.ic_dialog_alert) 
  31.                             .show(); 
  32.                     }}, 3000); 
  33.             }}); 
  34.         c.setOnClickListener(new View.OnClickListener(){ 
  35.             publicvoid onClick(View v){ 
  36.                 finish(); 
  37.             }}); 
  38.     } 
  39.  
  40.     static String getVersion(Context c) 
  41.     { 
  42.         try
  43.             return c.getPackageManager().getPackageInfo(c.getPackageName(),0).versionName; 
  44.         } catch(Exception e) { 
  45.             return c.getString(R.string.unknown_version); 
  46.         } 
  47.     } 
  48.  
  49. class LogTask extends AsyncTask<Void, Void, Void> 
  50.     Activity activity; 
  51.     String logText; 
  52.     Process process; 
  53.     ProgressDialog progress;  
  54.  
  55.     LogTask(Activity a, ProgressDialog p) { 
  56.         activity = a; 
  57.         progress = p; 
  58.     } 
  59.  
  60.     @Override 
  61.     protected Void doInBackground(Void... v) { 
  62.         try
  63.             Log.e("crash", "doInBackground begin"); 
  64.             process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"}); 
  65.             logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream()); 
  66.             Log.e("crash", "doInBackground end"); 
  67.         } catch (IOException e) { 
  68.             e.printStackTrace(); 
  69.             Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show(); 
  70.         } 
  71.         returnnull
  72.     } 
  73.  
  74.     @Override 
  75.     protectedvoid onCancelled() { 
  76.         Log.e("crash", "onCancelled"); 
  77.         process.destroy(); 
  78.     } 
  79.  
  80.     @Override 
  81.     protectedvoid onPostExecute(Void v) { 
  82.         Log.e("crash", "onPostExecute"); 
  83.         progress.setMessage(activity.getString(R.string.starting_email)); 
  84.         UncaughtExceptionHandler.sendLog(logText, activity); 
  85.         progress.dismiss(); 
  86.         activity.finish(); 
  87.         Log.e("crash", "onPostExecute over"); 
  88.     } 
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到,不多說了。  最后再說下如何解析這個錯誤日志。

 

我們在獲取到的錯誤日志中,可以截取到如下信息:

  1. 12-12 20:41:31.807 24206 24206 I DEBUG   :  
  2. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #00  pc 004931f8  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
  3. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #01  pc 005b3a5e  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
  4. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #02  pc 005aab68  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
  5. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #03  pc 005ad8aa  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
  6. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #04  pc 005924a4  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
  7. 12-12 20:41:31.847 24206 24206 I DEBUG   :          #05  pc 005929b6  /data/data/org.cocos2dx.wing/lib/libhelloworld.so 
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
  1. 004931f8 
004931f8

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

 

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

例如:

  1. $ /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 
$ /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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM