源码

iOS崩溃与日志分析


在iOS开发中经常需要靠记录日志来调试应用程序、解决崩溃问题等,整理常用的日志输出和崩溃日志分析。

最新更新:2018-11-30

基于CocoaLumberjack 的 Swift使用封装库

一、崩溃的捕获

1、崩溃日志产生原因

1、应用中有Bug。
2、Watchdog 超时机制
3、用户强制退出
4、低内存终止
5、其他违反系统规则的操作,大部分是内存问题
发生崩溃,系统会生成一份崩溃日志在本地,或者上传 ITC

2、崩溃的类型(异常、信号错误)

异常类

NSRangeException等 NSException类

信号错误类

信号中断(SGIABRT)、非法指令信号(SIGILL)、总线错误信号(SIGBUS)、段错误信号(SIGSEGV)、访问一个已经释放的对象(EXC_BAD_ACCESS)

3、捕获异常崩溃信息

只能捕获一些异常崩溃,如 unrecognized selector、NSRangeException beyond bounds越界等Exception属错误

Appdelegate

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

测试

数组越界错误

NSMutableArray *array = [NSMutableArray array];
NSLog(@"%@",array[1]);

4、捕获信号错误崩溃信息

信号类型崩溃捕获,测试的时候如果测试Signal类型的崩溃,不要在xcode下的debug模式进行测试。因为系统的debug会优先去拦截。应该build好应用之后直接点击运行app进行测试。

1、什么是信号

在计算机科学中,信号(英语:Signals)是Unix、类Unix以及其他POSIX兼容的操作系统中进程间通讯的一种有限制的方式。它是一种异步的通知机制,用来提醒进程一个事件已经发生。当一个信号发送给一个进程,操作系统中断了进程正常的控制流程,此时,任何非原子操作都将被中断。如果进程定义了信号的处理函数,那么它将被执行,否则就执行默认的处理函数。

在iOS中就是未被捕获的Objective-C异常(NSException),导致程序向自身发送了SIGABRT信号而崩溃。

SIGABRT–程序中止命令中止信号
SIGALRM–程序超时信号
SIGFPE–程序浮点异常信号
SIGILL–程序非法指令信号
SIGHUP–程序终端中止信号
SIGINT–程序键盘中断信号
SIGKILL–程序结束接收中止信号
SIGTERM–程序kill中止信号
SIGSTOP–程序键盘中止信号
SIGSEGV–程序无效内存中止信号
SIGBUS–程序内存字节未对齐中止信号
SIGPIPE–程序Socket发送失败中止信号

更多信号

2、捕获方法

Appdelegate 的 didFinishLaunchingWithOptions 中 添加

signal(SIGHUP, SignalHandler);
signal(SIGINT, SignalHandler);
signal(SIGQUIT, SignalHandler);

signal(SIGABRT, SignalHandler);
signal(SIGILL, SignalHandler);
signal(SIGSEGV, SignalHandler);
signal(SIGFPE, SignalHandler);
signal(SIGBUS, SignalHandler);
signal(SIGPIPE, SignalHandler);

方法实现如下

void SignalHandler(int signal){
    
    int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
    if (exceptionCount > UncaughtExceptionMaximum)
    {
        return;
    }
    
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for (
         i = UncaughtExceptionHandlerSkipAddressCount;
         i < UncaughtExceptionHandlerSkipAddressCount +
         UncaughtExceptionHandlerReportAddressCount;
         i++)
    {
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
    NSLog(@"%@",backtrace);
    NSLog(@"%@", [NSString stringWithFormat:
                  NSLocalizedString(@"Signal %d was raised.", nil),
                  signal]);
}

3、测试

UIView *tempView = [[UIView alloc]init];
[tempView release];

//对象已经被释放,内存不合法,此块内存地址又没被覆盖,所以此内存内垃圾内存,所以调用方法的时候会导致SIGSEGV的错误
[tempView setNeedsDisplay];

或者说 我在堆内存中找栈内存地址

id x_id = [self performSelector:@selector(createNum)];

- (int)createNum {
    return 10;
}

这种情况也是会导致SIGSEGV的错误的

如果在内存中释放不存在的空间,就会导致SIGABRT错误

Test * test = {1, 2};

free(test);


内存地址不对齐会导致SIGBUS错误

char *s = "hello world";
*s = 'H';

4、问题

信号捕获后 app卡死了

大部分这类型的错误会报错 EXC_BAD_ACCESS ,而这种错误都是发生在内存问题,例如

1、访问数据为空、数据类型不对

2、操作了不该操作的对象,野指针

5、EXC_BAD_ACCESS错误的调试

1、xcode可以用僵尸模式打印出对象 然后通过对象查找对应的代码位置

1、Edit Scheme - Diagnositics - Memory Management 勾选 Zombie Objects 和 Malloc Stack

2、会打印出 
cyuyan[7756:17601127] *** -[UIViewController respondsToSelector:]: message sent to deallocated instance 0x7fe71240d390

这句开启僵尸模式后打出来的输出,包含了我们需要的 进程pid、崩溃地址,终端通过下面命令查看堆栈日志来找到崩溃代码

3、查找日志
sudo malloc_history 7756 0x7fe71240d390

2、覆写一个object的respondsToSelector方法

在 other c flags中加入-D FOR_DEBUG(记住请只在Debug Configuration下加入此标记)。这样当你程序崩溃时,Xcode的console上就会准确地记录了最后运行的object的方法。重写一个object的respondsToSelector方法,打印报错前的

#ifdef _FOR_DEBUG_  
-(BOOL) respondsToSelector:(SEL)aSelector {  
    printf("SELECTOR: %s/n", [NSStringFromSelector(aSelector) UTF8String]);  
    return [super respondsToSelector:aSelector];  
}  
#endif

3、通过instruments的Zombies

引申:怎么定位到野指针的地方。如果还没定位到,这个对象被提前释放了,怎么知道该对象在什么地方释放的

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

0

6、可能崩溃的一些场景

1、野指针

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

1

image.png

2、内存处理不当、内存泄露

3、主线程UI长时间卡死,系统强制销毁app

死锁?

4、多线程切换访问引起的crash

多线程抢写数据库?

5、和服务端约定的数据结构变更导致操作数据类型问题、或者操作空

二、崩溃日志的获取

1、iOS设备可以直接查看

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

2

2、链接设备到电脑 Itunes同步后,日志会保存在电脑上

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

3

3、xcode获取

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

4

4、线上的崩溃,没有设备

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

5

image.png

三、崩溃日志的解析

1、崩溃日志的实例

在Appdelegate 的 didFinishLaunchingWithOptions 中 添加
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);

方法实现如下
void UncaughtExceptionHandler(NSException *exception) {
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========/nname:%@/nreason:/n%@/ncallStackSymbols:/n%@",name,reason,[callStack componentsJoinedByString:@"/n"]];
    
    //将崩溃信息持久化在本地,下次程序启动时、或者后台,将崩溃信息作为日志发送给开发者。
    [[NSUserDefaults standardUserDefaults] setObject:content forKey:@"ExceptionContent"];
}

6

(0)

本文由 投稿者 创作,文章地址:https://blog.isoyu.com/archives/iosbengkuiyurizhifenxi.html
采用知识共享署名4.0 国际许可协议进行许可。除注明转载/出处外,均为本站原创或翻译,转载前请务必署名。最后编辑时间为:12 月 4, 2018 at 10:55 下午

热评文章

发表回复

[必填]

我是人?

提交后请等待三秒以免造成未提交成功和重复