现在的位置: 首页 > 综合 > 正文

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

2013年07月23日 ⁄ 综合 ⁄ 共 5964字 ⁄ 字号 评论关闭

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

抱歉!评论已关闭.