crash report

http://bbs.9ria.com/thread-200126-1-1.html



http://go3k.org/?p=258


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

[cpp]view plaincopyprint?

  1. static int s_fatal_signals[] = {

  2.    SIGABRT,

  3.    SIGBUS,

  4.    SIGFPE,

  5.    SIGILL,

  6.    SIGSEGV,

  7.    SIGTRAP,

  8.        SIGTERM,

  9.        SIGKILL,

  10. };

  11. static const char* s_fatal_signal_names[] = {

  12.        "SIGABRT",

  13.        "SIGBUS",

  14.        "SIGFPE",

  15.        "SIGILL",

  16.        "SIGSEGV",

  17.        "SIGTRAP",

  18.        "SIGTERM",

  19.        "SIGKILL",

  20. };


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


  22. void InitCrashReport()

  23. {

  24.        // 1     linux錯誤信號捕獲

  25.        for (int i = 0; i < s_fatal_signal_num; ++i) {

  26.                signal(s_fatal_signals[i], SignalHandler);

  27.        }


  28.        // 2      objective-c未捕獲異常的捕獲

  29.        NSSetUncaughtExceptionHandler(&HandleException);

  30. }

複製代碼

在遊戲的最開始調用InitCrashReport()函數來開啓崩潰捕獲。  註釋1處對應上文所說的第一類崩潰,註釋2處對應objective-c(或者說是UIKit Framework)拋出但是沒有被處理的異常。
2、打印堆棧信息

[cpp]view plaincopyprint?


  1. + (NSArray *)backtrace

  2. {

  3.        void* callstack[128];

  4.        int frames = backtrace(callstack, 128);

  5.        char **strs = backtrace_symbols(callstack, frames);


  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.        return backtrace;

  15. }

複製代碼

幸好,蘋果的iOS系統支持backtrace,通過這個函數可以直接打印出程序崩潰的調用堆棧。優點是,什麼符號函數表都不需要,也不需要保存發佈出去的對應版本,直接查看崩潰堆棧。缺點是,不能打印出具體哪一行崩潰,很多問題知道了是哪個函數崩的,但是還是查不出是因爲什麼崩的

3、日誌上傳,這個需要看實際需求,比如我們公司就是把崩潰信息http post到一個php服務器。這裏就不多做聲明瞭。
4、技巧---崩潰後程序保持運行狀態而不退出

[cpp]view plaincopyprint?

  1. CFRunLoopRef runLoop = CFRunLoopGetCurrent();

  2.        CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);


  3.        while (!dismissed)

  4.        {

  5.                for (NSString *mode in (__bridge NSArray *)allModes)

  6.                {

  7.                        CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.001, false);

  8.                }

  9.        }


  10.        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++代碼如何進行崩潰捕獲。

[cpp]view plaincopyprint?

  1. void InitCrashReport()


  2. {


  3. CCLOG("InitCrashReport");


  4. // Try

  5. to catch crashes...

  6. struct sigaction handler;


  7. memset(&handler, 0,

  8. sizeof(struct sigaction));



  9. handler.sa_sigaction =

  10. android_sigaction;

  11. handler.sa_flags

  12. = SA_RESETHAND;


  13. #define CATCHSIG(X) sigaction(X, &handler,

  14. &old_sa[X])

  15. CATCHSIG(SIGILL);


  16. CATCHSIG(SIGABRT);

  17. CATCHSIG(SIGBUS);


  18. CATCHSIG(SIGFPE);

  19. CATCHSIG(SIGSEGV);


  20. CATCHSIG(SIGSTKFLT);

  21. CATCHSIG(SIGPIPE);


  22. }

複製代碼


通過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的崩潰回調函數如下:

[cpp]view plaincopyprint?


  1. void android_sigaction(int

  2. signal, siginfo_t *info, void

  3. *reserved)

  4. {


  5. if (!g_env) {

  6. return;

  7. }


  8. jclass classID =

  9. g_env->FindClass(CLASS_NAME);

  10. if (!classID) {

  11. return;

  12. }



  13. jmethodID

  14. methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed", "()V");

  15. if (!methodID) {

  16. return;

  17. }


  18. g_env->CallStaticVoidMethod(classID, methodID);


  19. old_sa[signal].sa_handler(signal);


  20. }

複製代碼


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

java對應的函數實現如下:

[java]view plaincopyprint?

  1. publicstaticvoid

  2. onNativeCrashed() {

  3. //

  4. http://stackoverflow.com/questions/1083154/how-can-i-catch-sigsegv-segmentation-fault-and-get-a-stack-trace-under-jni-on-a


  5. Log.e("handller", "handle");

  6. new RuntimeException("crashed here (native trace should follow after the Java

  7. trace)").printStackTrace();

  8. s_instance.startActivity(new

  9. Intent(s_instance, CrashHandler.class));

  10. }

複製代碼

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


[java]view plaincopyprint?

  1. public class CrashHandler extends Activity

  2. {

  3.    public static final String TAG = "CrashHandler";

  4.    protected void onCreate(Bundle state)

  5.    {

  6.        super.onCreate(state);

  7.        setTitle(R.string.crash_title);

  8.        setContentView(R.layout.crashhandler);

  9.        TextView v = (TextView)findViewById(R.id.crashText);

  10.        v.setText(MessageFormat.format(getString(R.string.crashed), getString(R.string.app_name)));

  11.        final Button b = (Button)findViewById(R.id.report),

  12.              c = (Button)findViewById(R.id.close);

  13.        b.setOnClickListener(new View.OnClickListener(){

  14.            public void onClick(View v){

  15.                final ProgressDialog progress = new ProgressDialog(CrashHandler.this);

  16.                progress.setMessage(getString(R.string.getting_log));

  17.                progress.setIndeterminate(true);

  18.                progress.setCancelable(false);

  19.                progress.show();

  20.                final AsyncTask task = new LogTask(CrashHandler.this, progress).execute();

  21.                b.postDelayed(new Runnable(){

  22.                    public void run(){

  23.                        if (task.getStatus() == AsyncTask.Status.FINISHED)

  24.                            return;

  25.                        // It's probably one of these devices where some fool broke logcat.

  26.                        progress.dismiss();

  27.                        task.cancel(true);

  28.                        new AlertDialog.Builder(CrashHandler.this)

  29.                            .setMessage(MessageFormat.format(getString(R.string.get_log_failed), getString(R.string.author_email)))

  30.                            .setCancelable(true)

  31.                            .setIcon(android.R.drawable.ic_dialog_alert)

  32.                            .show();

  33.                    }}, 3000);

  34.            }});

  35.        c.setOnClickListener(new View.OnClickListener(){

  36.            public void onClick(View v){

  37.                finish();

  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. {

  51.    Activity activity;

  52.    String logText;

  53.    Process process;

  54.    ProgressDialog progress;


  55.    LogTask(Activity a, ProgressDialog p) {

  56.        activity = a;

  57.        progress = p;

  58.    }


  59.    @Override

  60.    protected Void doInVoid... v) {

  61.        try {

  62.                Log.e("crash", "doInBackground begin");

  63.            process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"});

  64.            logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream());

  65.                Log.e("crash", "doInBackground end");

  66.        } catch (IOException e) {

  67.            e.printStackTrace();

  68.            Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show();

  69.        }

  70.        return null;

  71.    }


  72.    @Override

  73.    protected void onCancelled() {

  74.            Log.e("crash", "onCancelled");

  75.        process.destroy();

  76.    }


  77.    @Override

  78.    protected void onPostExecute(Void v) {

  79.            Log.e("crash", "onPostExecute");

  80.        progress.setMessage(activity.getString(R.string.starting_email));

  81.        UncaughtExceptionHandler.sendLog(logText, activity);

  82.        progress.dismiss();

  83.        activity.finish();

  84.        Log.e("crash", "onPostExecute over");

  85.    }

複製代碼


最主要的地方是doInBackground函數,這個函數通過logcat獲取了崩潰信息。 不要忘記在AndroidManifest.xml添加讀取LOG的權限

[html]view plaincopyprint?

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

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

我們在獲取到的錯誤日誌中,可以截取到如下信息:
[plain]view plaincopyprint?

  1. 12-12 20:41:31.807 24206

  2. 24206 I DEBUG :

  3. 12-12

  4. 20:41:31.847 24206 24206 I DEBUG : #00 pc 004931f8

  5. /data/data/org.cocos2dx.wing/lib/libhelloworld.so

  6. 12-12 20:41:31.847 24206 24206 I

  7. DEBUG : #01 pc 005b3a5e /data/data/org.cocos2dx.wing/lib/libhelloworld.so


  8. 12-12

  9. 20:41:31.847 24206 24206 I DEBUG : #02 pc 005aab68

  10. /data/data/org.cocos2dx.wing/lib/libhelloworld.so

  11. 12-12 20:41:31.847 24206 24206 I

  12. DEBUG : #03 pc 005ad8aa /data/data/org.cocos2dx.wing/lib/libhelloworld.so


  13. 12-12

  14. 20:41:31.847 24206 24206 I DEBUG : #04 pc 005924a4

  15. /data/data/org.cocos2dx.wing/lib/libhelloworld.so

  16. 12-12 20:41:31.847 24206 24206 I

  17. DEBUG : #05 pc 005929b6 /data/data/org.cocos2dx.wing/lib/libhelloworld.so

複製代碼

[plain]view plaincopyprint?

  1. 004931f8

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

arm-linux-androideabi-addr2line.exe -e 動態庫名稱  崩潰地址
例如:
[plain]view plaincopyprint?

  1. $

  2. /cygdrive/d/devandroid/android-ndk-r8c-windows/android-ndk-r8c/toolchains/arm-linux-androideabi-4.6/prebuilt/windows/bin/arm-linux-androideabi-addr2line.exe

  3. -e obj/local/armeabi-v7a/libhelloworld.so 004931f8

複製代碼

得到的結果就是哪個cpp文件第幾行崩潰。  如果動態庫信息不對,返回的就是 ?:0


原文來自:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019349.html


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