通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人员发现和修改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. 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、打印堆栈信息

  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、技巧---崩溃后程序保持运行状态而不退出

  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++代码如何进行崩溃捕获。

  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的崩溃回调函数如下:

  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对应的函数实现如下:

  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实现如下:

  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的权限

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

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

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

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

    转自:http://www.cnblogs.com/lancidie/archive/2013/04/13/3019349.html 通过崩溃捕获和收集,可以收集到已发布应用(游戏)的异常,以便开发人 ...

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

    前言 对于商务类的app,随着app注册使用人数递增,app的运营者们就会逐渐考虑在应用中开展一些推广活动.大多数活动具备时效性强.运营时间短的特征,一般产品们和运营者们都是通过wap页面快速投放到产 ...

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

    学习笔记:APP切图那点事儿–详细介绍android和ios平台 转载自:http://www.woofeng.cn/articles/168.html   版权归原作者所有 作者:亚茹有李 原文地址 ...

  4. 多媒体开发(7):编译Android与iOS平台的FFmpeg

    编译FFmpeg,一个古老的话题,但小程还是介绍一遍,就当记录.之前介绍怎么给视频添加水印时,就已经提到FFmpeg的编译,并且在编译时指定了滤镜的功能. 但是,在手机盛行的时代,读者可能更需要的是能 ...

  5. 教你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下载lib ...

  6. Android、iOS平台RTMP/RTSP播放器实时音量调节

    介绍移动端RTMP.RTSP播放器实时音量调节之前,我们之前也写过,为什么windows播放端加这样的接口,windows端播放器在多窗口大屏显示的场景下尤其需要,尽管我们老早就有了实时静音接口,相对 ...

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

    IOS点击事件 Click 300毫秒点击延迟 解决办法: 参考:http://cuiqingcai.com/1687.html 可判断设备 if (/(iPhone|iPad|iPod|iOS)/i ...

  8. ReactNative: Android与iOS平台兼容处理

    方法一: 创建不同的文件扩展名:*.android.js*.io.js 方法二: import { Platform } from 'react-native'; if (Platform.OS == ...

  9. Unity在Android和iOS中如何调用Native API

    本文主要是对unity中如何在Android和iOS中调用Native API进行介绍. 首先unity支持在C#中调用C++ dll,这样可以在Android和iOS中提供C++接口在unity中调 ...

随机推荐

  1. poj 2723 2-SAT问题

    思路:二分枚举能开的门的数量,将每次枚举转换成2-SAT问题.这里存在的矛盾是假设有门上a,b两个锁,a锁对应于1号钥匙,而一号钥匙的配对是2号钥匙,b锁对应于3号钥匙,3号的配对是4号钥匙.那么2号 ...

  2. 关于Class.forName("oracle.jdbc.driver.OracleDriver");报ClassNotFoundException 的异常

    关于try { Class.forName("oracle.jdbc.driver.OracleDriver"); }catch(ClassNotFoundException e) ...

  3. C#中thrift 中THttpHandler 传输数据 慢 slow 解决办法

    1. 在用c# 写thrift的服务端,来相应http请求,在用结构体传输时,会遇到一个问题,就是(在用网络)传输数据特别慢, 这是由于在发生数据是用的TStreamTransport 导致每传一个数 ...

  4. ActionBar 的简单使用

    About ActionBar The action bar is one of the most important design elements you can implement for yo ...

  5. Android环境搭建的步骤

    Android 环境搭建步骤 这里简单介绍一下学习Android之后如何搭建环境的问题 一.    在搭建环境之前,首先你要先下载Java JDK(根据系统位数选择下载是64位或32位的),Eclip ...

  6. visual studio 2015预览版系统需求

    visual studio 2015预览版的系统需求跟visual studio 2013的一样. 支持visual studio 2015 preview的操作系统:Windows 8.1(x86 ...

  7. 使用 EF Power Tool Code Frist 生成 Mysql 实体

    原文:使用 EF Power Tool Code Frist 生成 Mysql 实体 1,在要生成的项目上右键   2,   3,   4,   5,  生成后的效果     已知问题: 1,在Mys ...

  8. asp.net C#数据导出Excel实例介绍

    excel导出在C#代码中应用己经很广泛了,我这里就做些总结,供自己和读者学习用. Excel知识点. 一.添加引用和命名空间 添加Microsoft.Office.Interop.Excel引用,它 ...

  9. 第四十二篇、自定义Log打印

    1.在Xcode 8出来之后,需要我们去关闭多余的日志信息打印 2.在开发的过程中,打印调试日志是一项比不可少的工程,但是在iOS 10中NSLog打印日志被屏蔽了,就不得不使用自定义Log 3.去掉 ...

  10. Object-C基础学习笔记(1)

    1.苹果公司将Cocoa.Carbon.QuickTime和OpenGL等技术作为框架集:提供Cocoa组成部分有: (1)Foundation框架(有很多有用的,面向数据的低级类和数据结构): (2 ...