iOS进阶之崩溃千奇百怪,如何全面监控?

简介

App 上线后,我们最怕出现的情况就是应用崩溃了。但是,我们线下测试好好的 App,为什么上
线后就发生崩溃了呢?

崩溃的几种情况

下面我们就先看看几个常见的编写代码时的小马虎,是如何让应用崩溃的。

  • 数组

    • 数据越界,在取数据索引时越界,App 会发生崩溃
    • 就是给数组添加了 nil 会
      崩溃。
  • 字典

    • 字典value传的nil也会发成崩溃
  • 多线程

    • 在子线程中进行 UI 更新可能会发生崩溃
    • 多个线程进行数据的读取操作,因为
      处理时机不一致,比如有一个线程在置空数据的同时另一个线程在读取这个数据,可能会出现崩溃情况。
  • 主线程无响应

    • 如果主线程超过系统规定的时间无响应,就会被 Watchdog 杀掉。这时,崩溃
      问题对应的异常编码是 0x8badf00d。
  • 野指针

    • 指针指向一个已删除的对象访问内存区域时,会出现野指针崩溃。

野指针问题是我们需要重点关注的,因为它是导致 App 崩溃的最常见,也是最难定位的一种情况。

崩溃信息的收集

程序崩溃了,你的 App 就不可用了,对用户的伤害也是最大的。因此,每家公司都会非常重视自
家产品的崩溃率,并且会将崩溃率(也就是一段时间内崩溃次数与启动次数之比)作为优先级最高的技术指标,比如千分位是生死线,万分位是达标线等,去衡量一个 App 的高可用性。

一般都是由崩溃监控系统来搜集。同时,崩溃监控系统收集到的堆栈信
息,也为解决崩溃问题提供了最重要的信息。

但是,崩溃信息的收集却并没有那么简单。因为,有些崩溃日志是可以通过信号捕获到的,而很
多崩溃日志却是通过信号捕获不到的。

一、信号可捕获崩溃

  • kvo
  • NSNotification线程问题
  • 数据越界
  • 野指针

二、信号不可捕获崩溃

  • 后台任务超时
  • 内存打爆
  • 主线程卡顿阀值

如何全面监控崩溃信息

  1. 收集崩溃日志最简单的方法,就是打开 Xcode 的菜单选择 Product -> Archive。
  2. 在提交时选上“Upload your app’s symbols to receive symbolicated reports from
    Apple”,以后你就可以直接在 Xcode 的 Archive 里看到符号化后的崩溃日志了。

但是这种查看日志的方式,每次都是纯手工的操作,而且时效性较差。

很多公司的崩
溃日志监控系统,都是通过PLCrashReporter 这样的第三方开源库捕获崩溃日志,然后上传到自
己服务器上进行整体监控的。

没有服务端开发能力,或者对数据不敏感的公司,则会直接使用 Fabric或者Bugly来监控崩溃。

PLCrashReporter、Bugly怎么检测崩溃

PLCrashReporter 和 Bugly 这类工具,是怎么知道 App 什么时候崩溃的?接下
来,我就和你详细分析下。

例如在崩溃日志里,你经常会看到下面这段说明异常信息:

Exception Type: EXC_BAD_ACCESS (SIGSEGV)

它表示的是,EXC_BAD_ACCESS 这个异常会通过 SIGSEGV 信号发现有问题的线程。

虽然信号的种类有很多,但是都可以通过注册 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]];
     }
     NSLog(crashString);
}

上面这段代码对各种信号都进行了注册,捕获到异常信号后,在处理方法 handleSignalException
里通过 backtrace_symbols 方法就能获取到当前的堆栈信息。堆栈信息可以先保存在本地,下次启动时再上传到崩溃监控服务器就可以了。

先将捕获到的堆栈信息保存在本地,是为了实现堆栈信息数据的持久化存储。

一、那么,为什么要实现持久化存储呢?

这是因为,在保存完这些堆栈信息以后,App 就崩溃了,崩溃后内存里的数据也就都没有了。而
将数据保存在本地磁盘中,就可以在 App 下次启动时能够很方便地读取到这些信息。

二、信号捕获不到的崩溃信息怎么收集?

你是不是经常会遇到这么一种情况,App 退到后台后,即使代码逻辑没有问题也很容易出现崩
溃。而且,这些崩溃往往是因为系统强制杀掉了某些进程导致的,而系统强杀抛出的信号还由于
系统限制无法被捕获到。

一般,在退后台时你都会把关键业务数据保存在内存中,如果保存过程中出现了崩溃就会丢失或
损坏关键数据,进而数据损坏又会导致应用不可用。这种关键数据的损坏会给用户带来巨大的损
失。

一、后台容易崩溃的原因是什么?

那就先说说后台保活的5中方式:

  1. Background Mode
    • 使用 Background Mode 方式的话,App Store 在审核时会提高对 App 的要求。通常情况下,
      只有那些地图、音乐播放、VoIP 类的 App 才能通过审核。
  2. Background Fetch
    • Background Fetch 方式的唤醒时间不稳定,而且用户可以在系统里设置关闭这种方式,导致它
      的使用场景很少。
  3. Silent Push
    • Silent Push 是推送的一种,会在后台唤起 App 30 秒。它的优先级很低,会调用
      application:didReceiveRemoteNotifiacation:fetchCompletionHandler: 这个 delegate,和普通
      的 remote push notification 推送调用的 delegate 是一样的。
  4. PushKit
    • PushKit 后台唤醒 App 后能够保活 30 秒。它主要用于提升 VoIP 应用的体验
  5. Background Task
    • Background Task 方式,是使用最多的。App 退后台后,默认都会使用这种方式。

Background Task 方式为什么是使用最多的,它可以解决哪些问题?

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

延长时间的方式:

Background Task 这种方式,就是系统提供了 beginBackgroundTaskWithExpirationHandler
方法来延长后台执行时间,可以解决你退后台后还需要一些时间去处理一些任务的诉求。

代码如下:

 - (void)applicationDidEnterBackground:(UIApplication *)application {
    __block UIBackgroundTaskIdentifier background_task;
    background_task = [application beginBackgroundTaskWithExpirationHandler: ^{
        NSLog(@"task expired...");
        [application endBackgroundTask:background_task];
        background_task = UIBackgroundTaskInvalid;
    }];
} 

在这段代码中,yourTask 任务最多执行 3 分钟,3 分钟内 yourTask 运行完成,你的 App 就会挂
起。

如果 yourTask在3分钟之内没有执行完的话,系统会强制杀掉进程,从而造成崩溃,这就是为什么 App 退后台容易出现崩溃的原因。

后台崩溃造成的影响是未知的。持久化存储的数据出现了问题,就会造成你的 App 无法正常使用。

如何避免后台崩溃呢?

你知道了, App退后台后,如果执行时间过长就会导致被系统杀掉。那么,如果我们要想避免这
种崩溃发生的话,就需要严格控制后台数据的读写操作。

比如,你可以先判断需要处理的数据的
大小,如果数据过大,也就是在后台限制时间内或延长后台执行时间后也处理不完的话,可以考
虑在程序下次启动或后台唤醒时再进行处理。

同时,App 退后台后,这种由于在规定时间内没有处理完而被系统强制杀掉的崩溃,是无法通过
信号被捕获到的。这也说明了,随着团队规模扩大,要想保证 App 高可用的话,后台崩溃的监控
就尤为重要了。

那么,我们又应该怎么去收集退后台后超过保活阈值而导致信号捕获不到的那些崩溃信息呢?

采用 Background Task 方式时,我们可以根据 beginBackgroundTaskWithExpirationHandler 会
让后台保活3分钟这个阈值,先设置一个计时器,在接近3分钟时判断后台程序是否还在执行。
如果还在执行的话,我们就可以判断该程序即将后台崩溃,进行上报、记录,以达到监控的效
果。

一、还有哪些信号捕获不到的崩溃情况?怎样监控其他无法通过信号捕获的崩溃信息?

其他捕获不到的崩溃情况还有很多,主要就是:

  • 内存打爆
  • 主线程卡顿时间超过阈值被watchdog
    杀掉.

监控这两类崩溃的思路和监控后台崩溃类似,我们都先要找到它们的阈值,然后在临近阈
值时还在执行的后台程序,判断为将要崩溃,收集信息并上报。

  • 对于内存打爆信息的收集,你可以采用内存映射(mmap)的方式来保存现场。
  • 主线程卡顿时间超过阈值这种情况,你只要收集当前线程的堆栈信息就可以了。

二、采集到崩溃信息后如何分析并解决崩溃问题呢?

通过上面的内容,我们已经解决了崩溃信息采集的问题。现在,我们需要对这些信息进行分析,
进而解决 App 的崩溃问题。
我们采集到的崩溃日志,主要包含的信息为:

  1. 进程信息
    • 崩溃进程的相关信息,比如崩溃报告唯一标识符、唯一键值、设备标识;
  2. 基本信息
    • 崩溃发生的日期、iOS 版本;
  3. 异常信息
    • 异常类型、异常编码、异常的线程;
  4. 线程回溯
    • 崩溃时的方法调用栈。

通常情况下,我们分析崩溃日志时最先看的是异常信息,分析出问题的是哪个线程,在线程回溯
里找到那个线程;
然后,分析方法调用栈,符号化后的方法调用栈可以完整地看到方法调用的过程,从而知道问题发生在哪个方法的调用上。

如方法调用栈如下图所示:

[图片上传失败...(image-521a96-1601192703753)]

方法调用栈顶,就是最后导致崩溃的方法调用。完整的崩溃日志里,除了线程方法调用栈还有异
常编码。异常编码,就在异常信息里。

一些被系统杀掉的情况,我们可以通过异常编码来分析。
可以网上搜索一下44种异常编码,但常见的就是如下是三种:

  • 0x8badf00d

    • 表示 App 在一定时间内无响应而被 watchdog 杀掉的情况。
  • 0xdeadfa11

    • 表示 App 被用户强制退出。
  • 0xc00010ff

    • 表示 App 因为运行造成设备温度太高而被杀掉。

0x8badf00d 这种情况是出现最多的。当出现被 watchdog 杀掉的情况时,我们就可以把范围控制
在主线程被卡的情况。例如通过runloop原理去监控卡顿,获取堆栈信息。

0xdeadfa11 的情况,是用户的主动行为,我们不用太关注。

0xc00010ff 这种情况,就要对每个线程 CPU 进行针对性的检查和优化.例如:减少App的电量消耗。

除了崩溃日志外,崩溃监控平台还需要对所有采集上来的日志进行统计。我以腾讯的 Bugly 平台
为例,和你一起看一下崩溃监控平台一般都会记录哪些信息,来辅助开发者追溯崩溃问题。

bugly的崩溃分析图.png

除了崩溃率,你还可以在这个平台上能查看次数、用户数等趋势。下图展示的是某一个 App 的崩
溃在不同 iOS 系统、不同 iPhone 设备、App 版本的占比情况。这也是全局大盘观察,从不同维
度来分析。

App崩溃在不通过的系统版本、设备、版本的占比.png

有了全局大盘信息,一旦出现大量崩溃,你就需要明白是哪些方法调用出现了问题,需要根据影
响的用户数量按照从大到小的顺序排列出来,优先解决影响面大的问题。如下图所示:

App崩溃问题列表.png

同时,每个崩溃也都有自己的崩溃趋势图、iOS 系统分布图等信息,来辅助开发者跟踪崩溃修复效果。

有了崩溃的方法调用堆栈后,大部分问题都能够通过方法调用堆栈,来快速地定位到具体是哪个
方法调用出现了问题。有些问题仅仅通过这些堆栈还无法分析出来,这时就需要借助崩溃前用户
相关行为和系统环境状况的日志来进行进一步分析。

小结

学习完今天的这篇文章,我相信你就不再是只能依赖现有工具来解决线上崩溃问题的 iOS 开发者
了。在遇到那些工具无法提供信息的崩溃场景时,你也有了自己动手去收集崩溃信息的能力。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249