经典好文: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、设置开启崩溃捕获

Cpp代码  

  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代码  

  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代码  

  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代码  

  1. void InitCrashReport()
  2. {
  3. CCLOG("InitCrashReport");
  4. // Try to catch crashes...
  5. struct sigaction handler;
  6. memset(&handler, 0, sizeof(struct sigaction));
  7. handler.sa_sigaction = android_sigaction;
  8. handler.sa_flags = SA_RESETHAND;
  9. #define CATCHSIG(X) sigaction(X, &handler, &old_sa[X])
  10. CATCHSIG(SIGILL);
  11. CATCHSIG(SIGABRT);
  12. CATCHSIG(SIGBUS);
  13. CATCHSIG(SIGFPE);
  14. CATCHSIG(SIGSEGV);
  15. CATCHSIG(SIGSTKFLT);
  16. CATCHSIG(SIGPIPE);
  17. }

通过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代码  

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

可以看到,我们仅仅是通过jni调用了java的一个函数,然后所有的处理都是在java层面完成。

java对应的函数实现如下:

Java代码  

  1. public static void 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. }

我们开启了一个新的activity,因为当jni发生崩溃的时候,原始的activity可能已经结束掉了。 这个新的activity实现如下:

Java代码  

  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 doInBackground(Void... 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代码  

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

3、获取到错误日志后,就可以写到sd卡(同样不要忘记添加权限),或者是上传。 代码很容易google到,不多说了。 最后再说下如何解析这个错误日志。

我们在获取到的错误日志中,可以截取到如下信息:

Plain代码  

  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

Plain代码  

  1. 004931f8

这个就是我们崩溃函数的地址, libhelloworld.so就是崩溃的动态库。我们要使用addr2line对这个动态库进行解析(注意要是obj/local目录下的那个比较大的,含有符号文件的动态库,不是Libs目录下比较小的,同时发布版本时,这个动态库也要保存好,之后查log都要有对应的动态库)。命令如下:

arm-linux-androideabi-addr2line.exe -e 动态库名称 崩溃地址

例如:

Plain代码  

  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

得到的结果就是哪个cpp文件第几行崩溃。 如果动态库信息不对,返回的就是 ?:0

时间: 2024-10-09 13:24:13

经典好文:android和iOS平台的崩溃捕获和收集的相关文章

android和iOS平台的崩溃捕获和收集

转自:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019349.html 通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人员发现和修改bug,对于提高软件质量有着极大的帮助.本文介绍了iOS和android平台下崩溃捕获和收集的原理及步骤,不过如果是个人开发应用或者没有特殊限制的话,就不用往下看了,直接把友盟sdk(一个统计分析sdk)加入到工程中就万事大吉了,其中的错误日志功能完全能够满足需求,而且不需要额外准备接收

JSBridge(Android和IOS平台)的设计和实现

前言 对于商务类的app,随着app注册使用人数递增,app的运营者们就会逐渐考虑在应用中开展一些推广活动.大多数活动具备时效性强.运营时间短的特征,一般产品们和运营者们都是通过wap页面快速投放到产品的活动模块.Wap页面可以声文并茂地介绍活动,但活动的最终目标是通过获取特权.跳转进入本地功能模块,最后达成交易.如何建立wap页面和本地Native页面的深度交互,这就需要用到本文介绍的JSBridge. 此外一些平台类的产品,如大家每天都在使用的微信.支付宝.手机qq等,无一例外都在使用集成J

cocos2d-x打开网页android与ios平台

cocos2d-x打开一个网页,特别简单 转载请注明地址白白:http://blog.csdn.net/u010229677 iOS中 <span style="font-size:18px;">void FileOperation::linkToFull() { [[UIApplication sharedApplication]openURL:[NSURL RLWithString:@"http://www.baidu.com"]]; }</s

基于Android、iOS平台的移动端车牌识别技术及实现车牌识别过程

近年来,随着移动行业的爆发式发展,手机配置不断提高,基于手机平台的信息采集.图像处理.数据传输等方面的研究也成为了热点,这使得基于手机平台上的车牌识别成为可能.传统的车牌识别系统一般都基于固定的桌面平台.图像采集不灵活,特别是对于交通管理部门来说,对违章车辆车牌的自动登记非常不便,因此基于Android.iOS平台的移动端车牌识别技术出现了. 那么如何实现车牌识别的呢,下面简单说说: 首先对现存的车牌识别算法进行了研究,在诸多算法中寻找到一种适合在Android.iOS平台上运行的算法.先通过智

学习笔记:APP切图那点事儿–详细介绍android和ios平台

学习笔记:APP切图那点事儿–详细介绍android和ios平台 转载自:http://www.woofeng.cn/articles/168.html   版权归原作者所有 作者:亚茹有李 原文地址:http://blog.boocss.com/app-android-ios/ 一.android版 在做android版本设计的时候,尺寸有很多种,这时我们要以一种尺寸为基准,那这个基准尺寸是480px*800px,设计图完成之后就开始切图了 需要注意的: A:android主要有3种屏,即:

一种支持本地离线扫描识别的移动端车牌识别技术,基于Android、iOS平台

随着社会的发展,城市中的汽车越来越多.城市由于汽车的增加造成的拥挤给人们的生活带来了极大的不便,这种不便迫使人们去寻找高技术有效手段去解决这种不便.很多的大型停车场收费系统管理存在着排队时间长.管理成本高.劳动强度大等各种弊端,顺应时代发展的一些占路停车场和小型露天停车场也应运而生,然而这些停车场收费透明度低.资金流失和车辆失窃也给车主和管理者造成了较大的困扰,因此需要一些较为快捷有效的管理系统去解决这些问题.此时,一种基于移动端车牌识别的停车收费方法应运而生,车辆通过停车场出入口时,停车场端系

JS对于Android和IOS平台的点击响应的适配

综述 最近做项目的时候发现了一个非常奇怪的问题,就是对于click事件的响应.经过测试发现,对于IOS平台,直接监听click事件可能是没有响应的,而在Android和PC上则完全没有问题.所以通过获取设备信息实现了不同平台的不同监听. IOS监听 对于IOS设备,只监听click方法可能是没有响应的.解决方法就是监听 “touchend click”事件. 而对于Android和PC,则只监听click事件即可. 平台检测 我们利用userAgent来检测平台 1 2 3 4 5 6 7 8

教你pomeloclient包libpomelo增加cocos2d-x 3.0工程(Windows、Android、IOS平台)

Windows平台 操作系统:Windows7(64-bit) VS版本号:2013 Cocos2d-x版本号:3.0 project路径:E:\cocos2d-prj\ 1.从github下载libpomelo代码 E:\cocos2d-prj\cocos2d\external> git clone https://github.com/NetEase/libpomelo.git 2.创建libpomelo的VSproject E:\cocos2d-prj\cocos2d\external>

一种基于Android、iOS平台的移动端名片识别SDK,商务得力助手

移动端名片识别应用背景: 这些年,随着移动互联的发展,APP应用成爆发式的增长,在很多APP中都涉及到对名片信息的录入,如移动CRM.移动端OA,移动访客系统等:如果手动输入名片信息,速度慢,易出错,用户体验非常差.为了提高在移动终端上输入名片信息的速度和准确性,移动端名片识别技术出现了,以满足各个行业对名片信息自动录入的需求,只需在APP中集成移动端名片识别,用户便可通过手机拍照,自动录入识别名片信息.名片识别是指名片经过手机拍照识别,导入手机通讯录后,利用软件SyncML标准同步至云端,便可