Job

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

2019-04-30  本文已影响0人  Claire_wu

1 崩溃信息分类

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


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

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

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

Signal信号类型:

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种方式:

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

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

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

3.2 主线程卡顿超过阈值

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

一般不推荐通过监视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前,会做优先级判断。判断依据是:

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

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

4 崩溃信息收集

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

  1. 0x8badf00d:App在一定时间内无响应被watchdog杀掉(也就是我们所说的主线程被卡)
  2. 0xdeadfa11:用户强制退出(无需关注)
  3. 0xc00010ff:设备运行温度太高而被杀掉(针对CPU进行针对性检查和优化)
上一篇下一篇

猜你喜欢

热点阅读