iOS调试篇(二)——崩溃捕获篇

1 崩溃信息分类

崩溃信息有的能通过信号捕获到,有的不可以。下图列出了常见的部分崩溃情况:


常见部分崩溃情况分类.png

通过图片可以看出:KVO问题,NSNotification线程问题、数组越界、野指针等崩溃信息是可以通过信号捕获的,但后台任务超时、内存打爆、主线程卡顿超阈值等信息是无法通过信号捕捉的。

2 信号可捕获崩溃日志收集

Signal信号类型:

  • SIGABRT--程序中止命令中止信号
  • SIGALRM--程序超时信号
  • SIGFPE--程序浮点异常信号
  • SIGILL--程序非法指令信号
  • SIGHUP--程序终端中止信号
  • SIGINT--程序键盘中断信号
  • SIGKILL--程序结束接收中止信号
  • SIGTERM--程序kill中止信号
  • SIGSTOP--程序键盘中止信号
  • SIGSEGV--程序无效内存中止信号
  • SIGBUS--程序内存字节未对齐中止信号
  • SIGPIPE--程序Socket发送失败中止信号
    通过信号注册来捕获崩溃信息的代码可参看如下。下面的代码对各种信号都进行了注册,捕获到异常信号后,在处理方法handleSignalException里通过backtrace_symbols方法获取当前堆栈的信息。堆栈信息可暂时保留在本地(App崩溃后内存数据就丢失了),下次启动时上传到崩溃监控服务器。
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);
}

3 信号捕获不到的崩溃信息收集

3.1 后台崩溃

退后台是会把关键业务数据保存在内存中,但保存过程中就出现了崩溃就会丢失数据或损坏关键数据,进而数据损坏又会导致应用不可用。可App退到后台后容易被系统强杀,而且系统强杀抛出的信号由于系统限制还不可捕获。首先看看iOS后台保活的5种方式:

  • Background Mode:只有地图、音乐播放、VoIP(网络电话)等App才可以开启此权限
  • Background Fetch:唤醒时间不稳定,用户可在系统设置关闭这种方式,因此它的使用场景很少
  • Silent Push:会在后台唤起App30秒,优先级很低,会调用application:didReceiveRemoteNotifiacation:fetchCompletionHandler:这个delegate,和普通的remote push notification推送调用的delegate是一样的。
  • PushKit:后天唤醒App后能够保活30秒,主要用于提示VoIP应用体验。
  • Background Task:使用最多的,App退后台后,默认都会使用这种方式,向系统多争取一段时间来完成退后台后还需要一些时间去处理一些任务。当App退到后台后,只有几秒时间可以执行代码,接下来就会被系统挂起。进程挂起后所有线程都会暂停,无论是文件读写还是内存读写都会被暂停。但是数据读写过程无法暂停只能被中断,中断时易出现读写异常且损坏文件,所以系统会主动杀掉App进程。
    Background Task的使用方法如下:
- (void)applicationDidEnterBackground:(UIApplication *)application {
    self.backgroundTaskIdentifier = [application beginBackgroundTaskWithExpirationHandler:^( void) {
        [self yourTask];
    }];
}

这段代码中,yourTask最多执行三分钟,三分钟内运行完成就可以挂起,如果没有执行完成就会被强杀从而造成崩溃,这就是为什么App退后台易出现崩溃的原因。
如何避免后台崩溃呢?我们知道App退后台后执行时间过长就会被强杀,若要避免首先要严格控制后台读写操作。

  1. 比如可判断需要处理的数据的大小,如果数据过大在系统限制时间甚至是延长后台执行时间也处理不完则可以考虑程序下次启动或后台唤醒时再处理。
  2. 采用Background Task方式,设计一个定时器在接近3分钟阈值时判断后台程序是否还在执行,如果还在执行可判断程序即将后台崩溃,进行上报、记录已达到监控的效果。

3.2 主线程卡顿超过阈值

我们所说的卡顿问题,就是在主线程上无法响应用户交互。导致卡顿的几个主要原因:

  • 复杂UI、图文混排绘制量过大
  • 在主线程上做网络同步请求
  • 在主线程做大量的IO操作
  • 运算量过大,CPU持续高占用
  • 死锁和主、子线程抢锁

一般不推荐通过监视FPS来确定是否出现卡顿,一般推荐通过监控主线程RunLoop的状态来判断是否出现卡顿。回忆一下RunLoop可以参看iOS多线程--RunLoop
再次贴出RunLoop运行过程图:

RunLoop运行过程图.png

RunLoop在以下几个状态中切换,当进入睡眠前方法(kCFRunLoopAfterWaiting)和线程唤醒后接收消息时间过长(kCFRunLoopBeforeSources)却不能进入下一步,就可以认为是线程受阻。如果这个线程是主线程,则表现出来就是卡顿。

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry , // 进入 loop
    kCFRunLoopBeforeTimers , // 触发 Timer 回调
    kCFRunLoopBeforeSources , // 触发 Source0 回调
    kCFRunLoopBeforeWaiting , // 等待 mach_port 消息
    kCFRunLoopAfterWaiting ), // 接收 mach_port 消息
    kCFRunLoopExit , // 退出 loop
    kCFRunLoopAllActivities  // loop 所有状态改变
}

实现关键代码如下:

dispatchSemaphore = dispatch_semaphore_create(0); //Dispatch Semaphore保证同步
    //创建一个观察者
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                              kCFRunLoopAllActivities,
                                              YES,
                                              0,
                                              &runLoopObserverCallBack,
                                              &context);
    //将观察者添加到主线程runloop的common模式下的观察中
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopCommonModes);
    
    //创建子线程监控
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //子线程开启一个持续的loop用来进行监控
        while (YES) {
            long semaphoreWait = dispatch_semaphore_wait(dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 20*NSEC_PER_MSEC));
            if (semaphoreWait != 0) {
                if (!runLoopObserver) {
                    timeoutCount = 0;
                    dispatchSemaphore = 0;
                    runLoopActivity = 0;
                    return;
                }
                //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够检测到是否卡顿
                if (runLoopActivity == kCFRunLoopBeforeSources || runLoopActivity == kCFRunLoopAfterWaiting) {
                    //出现三次出结果
//                    if (++timeoutCount < 3) {
//                        continue;
//                    }
                    NSLog(@"monitor trigger");
                    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//                        [SMCallStack callStackWithType:SMCallStackTypeAll];
                    });
                } //end activity
            }// end semaphore wait
            timeoutCount = 0;
        }// end while
    });

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    SMLagMonitor *lagMonitor = (__bridge SMLagMonitor*)info;
    lagMonitor->runLoopActivity = activity;
    
    dispatch_semaphore_t semaphore = lagMonitor->dispatchSemaphore;
    dispatch_semaphore_signal(semaphore);
}

3.3 内存被打爆

iOS系统会开启优先级最高的线程vm_pressure_monitor来监控系统的内存压力情况,并通过一个堆栈来维护所有App的进程。另外,iOS系统还会维护一个内存快照表,用于保存每个进程内存页的消耗情况。
当vm_pressure_monitor发现某App有内存压力了,就发出通知,内存有压力的App就会去执行对应的didReceiveMemoryWarning代理,通过这个代理,你可以获得最后一个编写逻辑代码释放内存的机会。这段代码的执行,就可能会避免你的App被系统强杀。

系统在强杀App前,会做优先级判断。判断依据是:

  • 内核用线程的优先级最高,操作系统优先级其次,App的优先级排在最后。
  • 前台App程序的优先级高于后台运行的App
  • CPU占用多的线程优先级会被降低

一些内存打爆的监控思路:

  1. 在接收到didReceiveMemoryWarning代理时打印出app内存的占用,以及内存的分配情况,了解到是哪些占用了大量内存
  2. hook malloc_logger(这个后续还有待补充更加详细的内容)

4 崩溃信息收集

采集崩溃日志主要需要包含以下信息:

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


    方法调用栈展示图.png

    一些被系统杀掉的情况,可通过异常编码来分析,最常见的异常编码如下:

  1. 0x8badf00d:App在一定时间内无响应被watchdog杀掉(也就是我们所说的主线程被卡)
  2. 0xdeadfa11:用户强制退出(无需关注)
  3. 0xc00010ff:设备运行温度太高而被杀掉(针对CPU进行针对性检查和优化)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,491评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,856评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,745评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,196评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,073评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,112评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,531评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,215评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,485评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,578评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,356评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,215评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,583评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,898评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,497评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,697评论 2 335