iOS 如何监控崩溃

3,942 阅读6分钟

APP的崩溃可以分为两类:信号可捕捉崩溃 和 信号不可捕捉崩溃。

信号可捕捉的崩溃

  • 数组越界:取数据时候索引越界,APP发生崩溃。给数组添加nil会崩溃。
  • 多线程问题:多个线程进行数据的存取,可能会崩溃。例如有一个线程在置空数据的同时另一个线程在读取数据。
  • 野指针问题:指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。野指针问题是导致 App 崩溃的最常见,也是最难定位的一种情况。
  • NSNotification线程问题:NSNotification 有很多种线程实现方式,同步、异步、聚合,所以不恰当的线程发送和接收会出现崩溃问题。
  • KVO问题:‘If your app targets iOS 9.0 and later or OS X v10.11 and later, you don't need to unregister an observer in its deallocation method。’ 在9.0之前需要手动remove 观察者,如果没有移除会出现观察者崩溃情况。

信号不可捕捉的崩溃

  • 后台任务超时
  • App超过系统限制的内存大小被杀死
  • 主线程卡顿被杀死

崩溃的信号类型

崩溃是由于Mach异常、Objective-C异常导致的,对于Mach异常,到BSD层会转化为对应的Signal信号。可以通过捕获信号来获取Crash信息。

崩溃报告里面经常会看到:

Exception Type:        EXC_BAD_ACCESS (SIGSEGV)

表示的是EXC_BAD_ACCESS 这个异常会通过 SIGSEGV 信号发现有问题的线程。 信号有很多种类,下面表格列举了些类型及解释:

Exception Type Signal 描述
EXC_BAD_ACCESS SIGSEGV 一般是非法内存访问错误,比如访问了已经释放的野指针,或者C数组越界
SIGBUS 非法地址,意味着指针所对应的地址是有效地址,但总线不能正常使用该指针。通常是未对齐的数据访问所致
EXC_BAD_INSTRUCTION SIGILL 非法指令。 是当一个进程尝试执行一个非法指令时发送给它的信号。可执行程序含有非法指令的原因,一般也就是cpu架构不对,编译时指定的march和实际执行的机器的march不同。这种情况,因为工具链一样,连接脚 本一样,所以可执行程序可以执行,不会发生exec format error。但是会包含一些不兼容的指令。还有另外一种可能,就是程序的执行权限不够,比如在用户态下运行的程序只能执行非特权指令,一旦CPU遇到特权指 令,将产生illegal instruction错误
EXC_ARITHMETIC SIGFPE 当一个进程执行了一个错误的算术操作时发送给它的信号
EXC_EMULATION SIGEMT 一个实现定义的硬件故障
EXC_SOFTWARE SIGSYS 指示一个无效的系统调用。由于某种未知原因,进程执行了一条系统调用指令, 但其指示系统调用类型的参数却是无效的
SIGPIPE 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止
SIGABRT 这种异常类型的崩溃最常见的原因是未捕获的Objective-C/ c++异常和对abort()的调用。 如果应用程序扩展 Extension启动所需时间过久(例如被看门狗 Watch Dog终止),则将使用此异常类型终止应用程序扩展。如果一个扩展在启动时由于挂起而终止,那么生成的崩溃报告的异常子类型将是LAUNCH_HANG。因为扩展没有主函数,所以花在初始化上的任何时间都是在静态构造函数和扩展库中的+load方法中进行的。你应该尽可能多地推迟这项工作
SIGKILL 在系统的请求下,进程被终止。查看终止原因字段以更好地理解终止的原因。 终止原因字段将包含后跟代码的名称空间。以下代码是针对watchOS的: 终止码0xc51bad01表示一个监视应用程序被终止了,因为它在执行后台任务时占用了太多的CPU时间。要解决这个问题,可以优化执行后台任务的代码以提高CPU效率,或者减少在后台运行应用程序时执行的工作量。 终止代码0xc51bad02表示一个监视应用程序被终止,因为它未能在分配的时间内完成一个后台任务。要解决这个问题,减少应用程序在后台运行时执行的工作量。 终止码0xc51bad03表示一个监视应用程序未能在分配的时间内完成一个后台任务,并且系统总体上非常繁忙,应用程序可能没有收到足够多的CPU时间来执行后台任务。虽然一个应用程序可以通过减少它在后台任务中执行的工作量来避免这个问题,但是0xc51bad03并不表示这个应用程序做错了什么。更有可能的是,由于整个系统的负载,该应用程序无法完成其工作。
EXC_BREAKPOINT SIGTRAP 跟踪陷阱,由断点指令或其它trap指令产生,由debugger使用。与异常退出类似,此异常旨在使附加的调试器有机会在进程执行的特定位置中断进程。您可以使用_builtin_trap()函数从您自己的代码中触发此异常。如果没有附加调试器,则终止进程并生成崩溃报告。swift的空指针或类型转换失败也会导致此信号

可以通过注册 signalHandler 来捕获到这些信号。其实现代码,如下所示:

void registerSignalHandler(void) {
    signal(SIGSEGV, handleSignalException);
    signal(SIGFPE, handleSignalException);
    signal(SIGBUS, handleSignalException);
    signal(SIGPIPE, handleSignalException);
    signal(SIGHUP, handleSignalException);
    signal(SIGINT, handleSignalException);
    signal(SIGQUIT, handleSignalException);
    signal(SIGABRT, handleSignalException);
    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 appendFormat:@"%s\n", traceChar[i]];
    }
    //这里写入crashString到你的日志文件
    NSLog(crashString);
}

可捕获的崩溃信息收集

可捕获的崩溃信息可以 通过PLCrashReporter 这样的第三方开源库捕获崩溃日志,然后上传到公司服务器监控,或者可以通过bugly监控崩溃,无需上传到公司的服务器。

Crash收集可以通过捕获Mach异常、或Unix信号两种方式来抓取crash事件

  • Mach异常方式
  • Unix信号
signal(SIGSEGV,signalHandler);

PLCrashReporter等三方库都是采用Mach异常+Unix信号方式,即使在优选捕获Mach异常的情况下,也放弃捕获EXC_CRASH异常,而选择捕获与之对应的SIGABRT信号。

PLCrashReporter将这些崩溃堆栈信息存储后,App就崩溃了,崩溃后内存里的数据也就都没有了,等待下次重启时,就可以取到这些日志,进行上传分析。

不可捕获的崩溃信息收集

Background Task模式

iOS 后台保活的 5 种方式:Background Mode、Background Fetch、Silent Push、PushKit、Background Task。

  • Background Mode,例如音乐播放,VOIP,地图类app
  • Background Fetch 设置一个间隔来每隔一段时间请求网络数据。由于用户可以在设置中关闭这种模式,导致它的使用场景很少
  • Slience Push 是推送的一种,会在后台唤醒30秒,优先级很低,会调用 application:didReceiveRemoteNotifiacation:fetchCompletionHandler: 这个 delegate,和普通的 remote push notification 推送调用的 delegate 是一样的。
  • Push Kit 后台唤醒app后会保活30秒,主要用于VOIP应用提升体验。
  • Background Task 是最常用也是使用最多的模式,会在进入后台时请求3分钟进行额外的操作。

APP退到后台以后,只有几秒钟的时间可以继续执行代码,然后会被系统挂起,所有线程暂停。不管这个线程是文件读写还是内存读写都会被暂停。但是,数据读写过程无法暂停只能被中断,中断时数据读写异常而且容易损坏文件,所以系统会选择主动杀掉 App 进程。

对于 Background Task 你可以使用UIApplication的beginBackgroundTaskWithName:expirationHandler: 或者beginBackgroundTaskWithExpirationHandler:方法来申请一些额外的时间来延长后台执行的时间。使用如下:


- (void)applicationDidEnterBackground:(UIApplication *)application {
    self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^( void) {
        [self yourTask];
    }];
}

任务最多执行3分钟,超过时间了,APP将会被杀死,造成崩溃。这也是app退到后台容易产生崩溃的原因。

对于这种崩溃,我们一般是在申请的3分钟的Task中,开启一个定时器,当快到3分钟时检测app还在运行就意味着App即将被系统杀死,这个时候进行记录日志上传来达到监控的效果。

OOM

OOM,全称Out-Of-Memory,iOS 的 Jetsam 机制造成的一种“另类” Crash,例如系统整体内存使用较高,系统基于优先级杀死优先级较低的 App,或者iOS系统对单个app设置的内存占用上限后,被系统杀死。

可以分析JetsamEvent日志,通过手机的设置->隐私->分析->分析数据中 找到JetsamEvent开头的相关日志信息。查找per-process-limit一项(并不是所有日志都有,可以找有的),用该项的rpages * pageSize即可得到OOM的阈值

{
    "uuid" : "092ff2cc-0290-3310-a375-cc69c192b94d",
    "states" : [
      "daemon",
      "idle"
    ],
    "killDelta" : 6781,
    "lifetimeMax" : 8241,
    "age" : 4581135570382,
    "purgeable" : 485,
    "fds" : 50,
    "genCount" : 0,
    "coalition" : 832,
    "rpages" : 7736,
    "reason" : "per-process-limit",
    "pid" : 8326,
    "idleDelta" : 174877955757,
    "name" : "photoanalysisd",
    "cpuTime" : 111.697557
  },

"rpages" : 7736 表示内存页数为 7736 ,再从文件中找到pageSize字段

  "largestZoneSize" : 15192064,
  "pageSize" : 4096,
  "uncompressed" : 77779,
  "zoneMapSize" : 70713344,

计算当前app内存限额 7736*4096/1024/1024 = 30MB