日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

android和iOS平臺(tái)的崩潰捕獲和收集

 quasiceo 2013-02-20

android和iOS平臺(tái)的崩潰捕獲和收集

分類: Android開(kāi)發(fā) IOS游戲開(kāi)發(fā) IOS應(yīng)用開(kāi)發(fā) 503人閱讀 評(píng)論(0) 收藏 舉報(bào)
        通過(guò)崩潰捕獲和收集,可以收集到已發(fā)布應(yīng)用(游戲)的異常,以便開(kāi)發(fā)人員發(fā)現(xiàn)和修改bug,對(duì)于提高軟件質(zhì)量有著極大的幫助。本文介紹了iOS和 android平臺(tái)下崩潰捕獲和收集的原理及步驟,不過(guò)如果是個(gè)人開(kāi)發(fā)應(yīng)用或者沒(méi)有特殊限制的話,就不用往下看了,直接把友盟sdk(一個(gè)統(tǒng)計(jì)分析 sdk)加入到工程中就萬(wàn)事大吉了,其中的錯(cuò)誤日志功能完全能夠滿足需求,而且不需要額外準(zhǔn)備接收服務(wù)器。  但是如果你對(duì)其原理更感興趣,或者像我一樣必須要兼容公司現(xiàn)有的bug收集系統(tǒng),那么下面的東西就值得一看了。

       要實(shí)現(xiàn)崩潰捕獲和收集的困難主要有這么幾個(gè):

       1、如何捕獲崩潰(比如c++常見(jiàn)的野指針錯(cuò)誤或是內(nèi)存讀寫越界,當(dāng)發(fā)生這些情況時(shí)程序不是異常退出了嗎,我們?nèi)绾尾东@它呢)

       2、如何獲取堆棧信息(告訴我們崩潰是哪個(gè)函數(shù),甚至是第幾行發(fā)生的,這樣我們才可能重現(xiàn)并修改問(wèn)題)

       3、將錯(cuò)誤日志上傳到指定服務(wù)器(這個(gè)最好辦)


        我們先進(jìn)行一個(gè)簡(jiǎn)單的綜述。會(huì)引發(fā)崩潰的代碼本質(zhì)上就兩類,一個(gè)是c++語(yǔ)言層面的錯(cuò)誤,比如野指針,除零,內(nèi)存訪問(wèn)異常等等;另一類是未捕獲異常 (Uncaught Exception),iOS下面最常見(jiàn)的就是objective-c的NSException(通過(guò)@throw拋出,比如,NSArray訪問(wèn)元素越 界),android下面就是java拋出的異常了。這些異常如果沒(méi)有在最上層try住,那么程序就崩潰了。  無(wú)論是iOS還是android系統(tǒng),其底層都是unix或者是類unix系統(tǒng),對(duì)于第一類語(yǔ)言層面的錯(cuò)誤,可以通過(guò)信號(hào)機(jī)制來(lái)捕獲(signal或者 是sigaction,不要跟qt的信號(hào)插槽弄混了),即任何系統(tǒng)錯(cuò)誤都會(huì)拋出一個(gè)錯(cuò)誤信號(hào),我們可以通過(guò)設(shè)定一個(gè)回調(diào)函數(shù),然后在回調(diào)函數(shù)里面打印并發(fā) 送錯(cuò)誤日志。

      一、iOS平臺(tái)的崩潰捕獲和收集

1、設(shè)置開(kāi)啟崩潰捕獲

  1. static int s_fatal_signals[] = {  
  2.     SIGABRT,  
  3.     SIGBUS,  
  4.     SIGFPE,  
  5.     SIGILL,  
  6.     SIGSEGV,  
  7.     SIGTRAP,  
  8.     SIGTERM,  
  9.     SIGKILL,  
  10. };  
  11.   
  12. static const char* s_fatal_signal_names[] = {  
  13.     "SIGABRT",  
  14.     "SIGBUS",  
  15.     "SIGFPE",  
  16.     "SIGILL",  
  17.     "SIGSEGV",  
  18.     "SIGTRAP",  
  19.     "SIGTERM",  
  20.     "SIGKILL",  
  21. };  
  22.   
  23. static int s_fatal_signal_num = sizeof(s_fatal_signals) / sizeof(s_fatal_signals[0]);  
  24.   
  25. void InitCrashReport()  
  26. {  
  27.         // 1     linux錯(cuò)誤信號(hào)捕獲  
  28.     for (int i = 0; i < s_fatal_signal_num; ++i) {  
  29.         signal(s_fatal_signals[i], SignalHandler);  
  30.     }  
  31.       
  32.         // 2      objective-c未捕獲異常的捕獲  
  33.     NSSetUncaughtExceptionHandler(&HandleException);  
  34. }  

在游戲的最開(kāi)始調(diào)用InitCrashReport()函數(shù)來(lái)開(kāi)啟崩潰捕獲。  注釋1處對(duì)應(yīng)上文所說(shuō)的第一類崩潰,注釋2處對(duì)應(yīng)objective-c(或者說(shuō)是UIKit Framework)拋出但是沒(méi)有被處理的異常。

2、打印堆棧信息

  1. + (NSArray *)backtrace  
  2. {  
  3.     void* callstack[128];  
  4.     int frames = backtrace(callstack, 128);  
  5.     char **strs = backtrace_symbols(callstack, frames);  
  6.       
  7.     int i;  
  8.     NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];  
  9.     for (i = kSkipAddressCount;  
  10.          i < __min(kSkipAddressCount + kReportAddressCount, frames);  
  11.          ++i) {  
  12.         [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];  
  13.     }  
  14.     free(strs);  
  15.       
  16.     return backtrace;  
  17. }  

幸好,蘋果的iOS系統(tǒng)支持backtrace,通過(guò)這個(gè)函數(shù)可以直接打印出程序崩潰的調(diào)用堆棧。優(yōu)點(diǎn)是,什么符號(hào)函數(shù)表都不需要,也不需要保存發(fā)布出去 的對(duì)應(yīng)版本,直接查看崩潰堆棧。缺點(diǎn)是,不能打印出具體哪一行崩潰,很多問(wèn)題知道了是哪個(gè)函數(shù)崩的,但是還是查不出是因?yàn)槭裁幢赖?img doc360img-src='http://image58.360doc.com/DownloadImg/2013/02/2006/30414387_1.gif' alt="大哭" src="http://image58.360doc.com/DownloadImg/2013/02/2006/30414387_1.gif">

3、日志上傳,這個(gè)需要看實(shí)際需求,比如我們公司就是把崩潰信息http post到一個(gè)php服務(wù)器。這里就不多做聲明了。

4、技巧---崩潰后程序保持運(yùn)行狀態(tài)而不退出

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

在崩潰處理函數(shù)上傳完日志信息后,調(diào)用上述代碼,可以重新構(gòu)建程序主循環(huán)。這樣,程序即便崩潰了,依然可以正常運(yùn)行(當(dāng)然,這個(gè)時(shí)候是處于不穩(wěn)定狀態(tài),但是由于手持游戲和應(yīng)用大多是短期操作,不會(huì)有掛機(jī)這種說(shuō)法,所以穩(wěn)定與否就無(wú)關(guān)緊要了)。玩家甚至感受不到崩潰。

這里要在說(shuō)明一個(gè)感念,那就是“可重入(reentrant)”。 簡(jiǎn)單來(lái)說(shuō),當(dāng)我們的崩潰回調(diào)函數(shù)是可重入的時(shí)候,那么再次發(fā)生崩潰的時(shí)候,依然可以正常運(yùn)行這個(gè)新的函數(shù);但是如果是不可重入的,則無(wú)法運(yùn)行(這個(gè)時(shí)候就 徹底死了)。要實(shí)現(xiàn)上面描述的效果,并且還要保證回調(diào)函數(shù)是可重入的幾乎不可能。所以,我測(cè)試的結(jié)果是,objective-c的異常觸發(fā)多少次都可以正 常運(yùn)行。但是如果多次觸發(fā)錯(cuò)誤信號(hào),那么程序就會(huì)卡死。  所以要慎重決定是否要應(yīng)用這個(gè)技巧。


二、android崩潰捕獲和收集

1、android開(kāi)啟崩潰捕獲

      首先是java代碼的崩潰捕獲,這個(gè)可以仿照最下面的完整代碼寫一個(gè)UncaughtExceptionHandler,然后在所有的Activity的onCreate函數(shù)最開(kāi)始調(diào)用

Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler(this));

      這樣,當(dāng)發(fā)生崩潰的時(shí)候,就會(huì)自動(dòng)調(diào)用UncaughtExceptionHandler的public void uncaughtException(Thread thread, Throwable exception)函數(shù),其中的exception包含堆棧信息,我們可以在這個(gè)函數(shù)里面打印我們需要的信息,并且上傳錯(cuò)誤日志

    然后是重中之重,jni的c++代碼如何進(jìn)行崩潰捕獲。

  1. void InitCrashReport()  
  2. {  
  3.     CCLOG("InitCrashReport");  
  4.   
  5.     // Try to catch crashes...  
  6.     struct sigaction handler;  
  7.     memset(&handler, 0, sizeof(struct sigaction));  
  8.   
  9.     handler.sa_sigaction = android_sigaction;  
  10.     handler.sa_flags = SA_RESETHAND;  
  11.   
  12. #define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])  
  13.     CATCHSIG(SIGILL);  
  14.     CATCHSIG(SIGABRT);  
  15.     CATCHSIG(SIGBUS);  
  16.     CATCHSIG(SIGFPE);  
  17.     CATCHSIG(SIGSEGV);  
  18.     CATCHSIG(SIGSTKFLT);  
  19.     CATCHSIG(SIGPIPE);  
  20. }  
通過(guò)singal的設(shè)置,當(dāng)崩潰發(fā)生的時(shí)候就會(huì)調(diào)用android_sigaction函數(shù)。這同樣是linux的信號(hào)機(jī)制。 此處設(shè)置信號(hào)回調(diào)函數(shù)的代碼跟iOS有點(diǎn)不同,這個(gè)只是同一個(gè)功能的兩種不同寫法,沒(méi)有本質(zhì)區(qū)別。有興趣的可以google下兩者的區(qū)別。

2、打印堆棧

      java語(yǔ)法可以直接通過(guò)exception獲取到堆棧信息,但是jni代碼不支持backtrace,那么我們?nèi)绾潍@取堆棧信息呢?    這里有個(gè)我想嘗試的新方法,就是使用google breakpad,貌似它現(xiàn)在完整的跨平臺(tái)了(支持windows, mac, linux, iOS和android等),它自己實(shí)現(xiàn)了一套minidump,在android上面限制會(huì)小很多。  但是這個(gè)庫(kù)有些大,估計(jì)要加到我們的工程中不是一件非常容易的事,所以我們還是使用了簡(jiǎn)潔的“傳統(tǒng)”方案。 思路是,當(dāng)發(fā)生崩潰的時(shí)候,在回調(diào)函數(shù)里面調(diào)用一個(gè)我們?cè)贏ctivity寫好的靜態(tài)函數(shù)。在這個(gè)函數(shù)里面通過(guò)執(zhí)行命令獲取logcat的輸出信息(輸出 信息里面包含了jni的崩潰地址),然后上傳這個(gè)崩潰信息。  當(dāng)我們獲取到崩潰信息后,可以通過(guò)arm-linux-androideabi-addr2line(具體可能不是這個(gè)名字,在android ndk里面搜索*addr2line,找到實(shí)際的程序)解析崩潰信息。

      jni的崩潰回調(diào)函數(shù)如下:

  1. void android_sigaction(int signal, siginfo_t *info, void *reserved)  
  2. {  
  3.     if (!g_env) {  
  4.         return;  
  5.     }  
  6.   
  7.     jclass classID = g_env->FindClass(CLASS_NAME);  
  8.     if (!classID) {  
  9.         return;  
  10.     }  
  11.   
  12.     jmethodID methodID = g_env->GetStaticMethodID(classID, "onNativeCrashed""()V");  
  13.     if (!methodID) {  
  14.         return;  
  15.     }  
  16.   
  17.     g_env->CallStaticVoidMethod(classID, methodID);  
  18.   
  19.     old_sa[signal].sa_handler(signal);  
  20. }  

可以看到,我們僅僅是通過(guò)jni調(diào)用了java的一個(gè)函數(shù),然后所有的處理都是在java層面完成。

java對(duì)應(yīng)的函數(shù)實(shí)現(xiàn)如下:

  1. public static void onNativeCrashed() {  
  2.         // http:///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.     }  

我們開(kāi)啟了一個(gè)新的activity,因?yàn)楫?dāng)jni發(fā)生崩潰的時(shí)候,原始的activity可能已經(jīng)結(jié)束掉了。  這個(gè)新的activity實(shí)現(xiàn)如下:
  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.   
  41.     static String getVersion(Context c)  
  42.     {  
  43.         try {  
  44.             return c.getPackageManager().getPackageInfo(c.getPackageName(),0).versionName;  
  45.         } catch(Exception e) {  
  46.             return c.getString(R.string.unknown_version);  
  47.         }  
  48.     }  
  49. }  
  50.   
  51. class LogTask extends AsyncTask<Void, Void, Void>  
  52. {  
  53.     Activity activity;  
  54.     String logText;  
  55.     Process process;  
  56.     ProgressDialog progress;   
  57.   
  58.     LogTask(Activity a, ProgressDialog p) {  
  59.         activity = a;  
  60.         progress = p;  
  61.     }  
  62.   
  63.     @Override  
  64.     protected Void doInBackground(Void... v) {  
  65.         try {  
  66.             Log.e("crash""doInBackground begin");  
  67.             process = Runtime.getRuntime().exec(new String[]{"logcat","-d","-t","500","-v","threadtime"});  
  68.             logText = UncaughtExceptionHandler.readFromLogcat(process.getInputStream());  
  69.             Log.e("crash""doInBackground end");  
  70.         } catch (IOException e) {  
  71.             e.printStackTrace();  
  72.             Toast.makeText(activity, e.toString(), Toast.LENGTH_LONG).show();  
  73.         }  
  74.         return null;  
  75.     }  
  76.   
  77.     @Override  
  78.     protected void onCancelled() {  
  79.         Log.e("crash""onCancelled");  
  80.         process.destroy();  
  81.     }  
  82.   
  83.     @Override  
  84.     protected void onPostExecute(Void v) {  
  85.         Log.e("crash""onPostExecute");  
  86.         progress.setMessage(activity.getString(R.string.starting_email));  
  87.         UncaughtExceptionHandler.sendLog(logText, activity);  
  88.         progress.dismiss();  
  89.         activity.finish();  
  90.         Log.e("crash""onPostExecute over");  
  91.     }  

最主要的地方是doInBackground函數(shù),這個(gè)函數(shù)通過(guò)logcat獲取了崩潰信息。 不要忘記在AndroidManifest.xml添加讀取LOG的權(quán)限

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

3、獲取到錯(cuò)誤日志后,就可以寫到sd卡(同樣不要忘記添加權(quán)限),或者是上傳。  代碼很容易google到,不多說(shuō)了。  最后再說(shuō)下如何解析這個(gè)錯(cuò)誤日志。

我們?cè)讷@取到的錯(cuò)誤日志中,可以截取到如下信息:

  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  

  1. 004931f8  
這 個(gè)就是我們崩潰函數(shù)的地址,  libhelloworld.so就是崩潰的動(dòng)態(tài)庫(kù)。我們要使用addr2line對(duì)這個(gè)動(dòng)態(tài)庫(kù)進(jìn)行解析(注意要是obj/local目錄下的那個(gè)比較 大的,含有符號(hào)文件的動(dòng)態(tài)庫(kù),不是Libs目錄下比較小的,同時(shí)發(fā)布版本時(shí),這個(gè)動(dòng)態(tài)庫(kù)也要保存好,之后查log都要有對(duì)應(yīng)的動(dòng)態(tài)庫(kù))。命令如下:

arm-linux-androideabi-addr2line.exe -e 動(dòng)態(tài)庫(kù)名稱  崩潰地址

例如:

  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  
得到的結(jié)果就是哪個(gè)cpp文件第幾行崩潰。  如果動(dòng)態(tài)庫(kù)信息不對(duì),返回的就是 ?:0




    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多