App上线后,我们最怕出现的情况就是崩溃了,但是线下我们测试好好的App,为什么上线后就发生崩溃了呢?这些崩溃日志信息是怎么采集的?能够采集的全吗?采集后又要怎么分析,解决呢?
App上线后,是很脆弱的,导致其崩溃的问题,不仅包括编写代码时的各种马虎,还包括那些被系统强杀的疑难杂症.下面我们先看常见的几个编写代码时的小马虎,是如何让应用崩溃的.
1.数组越界:在取数据索引时越界,App会发生崩溃,还有一种情况,就是给数据添加一个nil元素会崩溃
2.多线程问题:在子线程中进行UI更新可能会发生崩溃,多个线程进行数据的读取操作,因为处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况
3.主线程为响应:如果主线程超过系统规定的时间无响应,会被Watchdog杀掉,这时,崩溃问题对应的异常编码是0x8badf00d.
4.野指针:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃,野指针问题是需要我们重点关注的,因为它是导致App崩溃的最常见,也是最难定位的一种情况,
一般崩溃都是由崩溃监控系统收集的,收集的是堆栈信息,这些堆栈信息也是解决崩溃问题的重要依据
崩溃信息的收集并没有那么简单,因为,有的崩溃日志是可以通过信号捕捉到的,而很多崩溃日志是通过信号捕捉不到的,下图可视
从这张图片中可以看出KVO问题,通知线程问题,数据越界,野指针等崩溃信息,可以通过信号捕捉,但是,像后台任务超时,内存被打爆,主线程卡顿超阈值等信息,是无法通过信号捕捉到的,接下来我们就看两种不同的崩溃情况怎么捕捉
信号可捕捉的崩溃日志收集
收集崩溃日志最简单的方法就是打开Xcode的菜单选择Product -> Archive.然后在提交时选上"Upload your app`s symbols to receive symbolicated reports from Apple",以后你就可以直接在Xcode的Archive里看到符号化的崩溃日志了.
这种查看日志的方式,每次都是纯手工操作,时效性较差,目前很多公司的崩溃日志监控系统,都是通过PLCarshReporter这样的三方开源库捕捉崩溃日志的,然后上传到自己服务器上进行整体监控,而没有服务端开发能力或者对数据不敏感的公司,这会使用Bugly这样的三方提供的完整的监控捕捉崩溃SDK进行集成统计.
那么为什么PLCrashReporter和Bugly这类工具,是怎么知道App是什么时候崩溃呢?
在崩溃日志里,我们会经常看到下面这段说明
Type: EXC_BAD_ACCESS (SIGSEGV)
它表示的事,EXC_BAD_ACCESS这个异常会通过SIGSEGV信号发现有问题的线程,虽然信号的种类有很多,但是都可以通过注册signalHandler来捕捉,起实现代码如下
void registerSignalHandler(void){
signal(SIGSEGV, handleSignalException);//试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据.
signal(SIGFPE, handleSignalException);//在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。
signal(SIGBUS, handleSignalException);//非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。
signal(SIGPIPE, handleSignalException);//管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止
signal(SIGHUP, handleSignalException);//本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录, wget也 能继续下载。此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。
signal(SIGINT, handleSignalException);//程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进 程组终止进程。
signal(SIGQUIT, handleSignalException);//和SIGINT类似, 但由QUIT字符(通常是Ctrl-)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。
signal(SIGABRT, handleSignalException);//调用abort函数生成的信号。
signal(SIGILL, handleSignalException);//执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。
}
void handleSignalException(int signal){
NSMutableString * crashString = [[NSMutableString alloc]init];
void * callStack[128];
int i, frames = backtrace(callStack,128);
char ** traceChar = backtrace_symbols(callStack,frames);
for (i = 0; i < frames; ++i) {
[crashString appendString:@"%s\n",traceChar[i]];
}
NSLog(crashString);
}
上面这段代码会各种信号都进行了注册,捕捉到异常信号后,在处理方法handleSignalException里通过backtrace_symbols方法可以获取到当前的堆栈信息,堆栈信息先保存到本地,下次启动时在上传到奔溃监控服务器就可以了
信号不可捕捉的崩溃日志收集
我们可能会遇到App退到后台后,即使代码逻辑没有问题也容易出现崩溃,而且这些崩溃往往是因为系统强杀掉了某些进程导致的,而系统强杀抛出的信号还由于系统限制无法被捕捉到.
那么,后台容易崩溃的原因是什么呢?如何避免后台崩溃?怎么去收集后台信号捕捉不到的那些崩溃信息呢?还有那些信号捕捉不到的崩溃情况?怎么去监控其他无法通过信号捕捉的崩溃信息?
下面我们就带着这五个问题来了解信号不可捕捉的崩溃
后台容易崩溃的原因是什么?
首先我们知道iOS后台包活的5种方式:Background Mode,Background Fetch,Silent Push,PushKit,Background Task
1.使用Background Mode方式的话,AppStore在审核时会提高对App的要求,通常情况下,只有那些地图,音乐播放,VoIP类的App才能通过审核
2.Background Fetch 方式的唤醒时间不稳定,而且用户可以在系统设置关闭这种方式,导致他的使用场景很少
3.Silent Push是推送的一种,会在后台唤起App 30秒.会调起application:didReceiveRemoteNotifiacation
这个delegate和普通的remote pushnotification推送调用的delegate是一样的
4.PushKit后台唤醒App后能够包活30秒,他主要用于提升VoIp应用的体验
5.Background Task方式,是使用最多的,App退到后台后,默认都会使用这种方式
接下来看一下使用最多的Background Task方式:
在程序退到后台后,只有几秒钟的时间可以执行代码,接下来会被系统挂起,进程挂起后所有的线程都会暂停,不管这个线程是文件读写还是内存读写都会被暂停,但是,数据读写过程无法暂停只能被中断,中断时数据读写异常而且容易损坏文件,所以系统会选择主动杀掉进程.
Background Task就是系统提供了baginBackgroundTaskWithExpirationHandler
方法来延长后台执行时间,可以解决退到后台还需要一些时间去处理一些任务的诉求.
Background Task方式的使用方法,如下代码
- (void)applicationDidEnterBackground:(UIApplication *)application {
self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^{
[self youTask];
}];
}
这段代码中,youTask任务最多执行3分钟,3分钟内youTask运行完成,你的App就会挂起,如果youTask在三分钟内没有执行完的话,就会被系统强行杀掉进程,造成崩溃.
如何避免后台崩溃呢?
如果我们想要避免这种崩溃发生的话,就需要严格控制后台数据的读写操作,比如,可以先判断需要处理的数据大小,如果数据过大,也就是在后台限制时间内或延长后台执行时间后也处理不完的话,可以考虑在程序下次启动或后台唤醒时在进行处理
怎么去收集后台信号捕捉不到的那些崩溃信息呢?
采用Background Task方式时,我们可以根据beginBackgroundTaskWithExpirationHandler会让后台包活三分钟,先设置一个定时器,在接近三分钟的时候判断后台程序是否还在执行,如果还在执行的话,我们就可以判断该程序即将后台崩溃,进行上报记录,已达到监控的效果
还有那些信号捕捉不到的崩溃情况?怎么去监控其他无法通过信号捕捉的崩溃信息?
其他捕捉不到的崩溃情况还有很多,主要就是内存打爆和主线程卡顿时间超过阈值被watchdog杀掉,其实监控这两种崩溃的思路和监控后台崩溃类似,我们要先找到他们的阈值,然后在临近阈值时还在执行的后台程序,判断为将要崩溃,收集信息并上报
注:以上文章为学习戴铭(iOS开发课程)