iOS你不知道的事--Crash分析
原文作者:Cooci_和谐学习_不急不躁
原文地址:https://www.jianshu.com/p/56f96167a6e9
大家平时在开发过程中,经常会遇到Crash
,那也是在正常不过的事,但是作为一个优秀的iOS开发人员,必将这些用户不良体验降到最低。
- 线下
Crash
,我们直接可以调试,结合stack
信息,不难定位! - 线上
Crash
当然也有一些信息,毕竟苹果爸爸的产品还是做得非常不错的!
通过iPhone的Crash log
也可以分析一些,但是这个是需要用户配合的,因为需要用户在手机 中 设置-> 诊断与用量->勾选 自动发送 ,然后在xcode中 Window->Organizer->Crashes 对应的app
,就是当前app最新一版本的crash log
,并且是解析过的,可以根据crash 栈
等相关信息 ,尤其是程序代码级别的 有超链接,一键可以直接跳转到程序崩溃的相关代码,这样更容易定位bug出处.
为了能够第一时间发现程序问题,应用程序需要实现自己的崩溃日志收集服务,成熟的开源项目很多,如 KSCrash,plcrashreporter,CrashKit 等。追求方便省心,对于保密性要求不高的程序来说,也可以选择各种一条龙Crash统计产品,如 Crashlytics,Hockeyapp ,友盟,Bugly 等等
但是,所有的但是,这不够!因为我们不再是一个简单会用的iOS开发人员,必将走向底层,了解原理,掌握装逼内容和技巧是我们的必修课
首先我们来了解一下Crash
的底层原理
iOS系统自带的 Apple’s Crash Reporter
记录在设备中的Crash日志,Exception Type
项通常会包含两个元素:Mach异常
和 Unix信号
。
Exception Type: EXC_BAD_ACCESS (SIGSEGV) Exception Subtype: KERN_INVALID_ADDRESS at 0x041a6f3
Mach异常是什么?它又是如何与Unix信号建立联系的?
Mach
是一个XNU的微内核核心,Mach
异常是指最底层的内核级异常,被定义在下 。每个thread,task,host
都有一个异常端口数组,Mach
的部分API
暴露给了用户态,用户态的开发者可以直接通过Mach API
设置thread,task,host
的异常端口,来捕获Mach
异常,抓取Crash
事件。
所有Mach
异常都在host
层被ux_exception
转换为相应的Unix信号
,并通过threadsignal
将信号投递到出错的线程。iOS中的 POSIX API
就是通过Mach
之上的 BSD
层实现的。
因此,EXC_BAD_ACCESS (SIGSEGV)
表示的意思是:Mach
层的EXC_BAD_ACCESS异常
,在host
层被转换成SIGSEGV信号
投递到出错的线程。
iOS的异常Crash
- KVO问题
- NSNotification线程问题
- 数组越界
- 野指针
- 后台任务超时
- 内存爆出
- 主线程卡顿超阀值
-
死锁
.... 下面我就拿出最常见的两种`Crash`分析一下
-
Exception
![](//upload-images.jianshu.io/upload_images/2257417-78821e0ecd1deeb7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp)
-
Signal
![](//upload-images.jianshu.io/upload_images/2257417-ff1a3a706473a6dd.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1000/format/webp)
Crash
分析处理
上面我们也知道:既然最终以信号的方式投递到出错的线程,那么就可以通过注册相应函数来捕获信号.到达Hook
的效果
+ (void)installUncaughtSignalExceptionHandler{ NSSetUncaughtExceptionHandler(&LGExceptionHandlers); signal(SIGABRT, LGSignalHandler); }
我们从上面的函数可以Hook到信息,下面我们开始进行包装处理.这里还是面向统一封装,因为等会我们还需要考虑Signal
void LGExceptionHandlers(NSException *exception) { NSLog(@"%s",__func__); NSArray *callStack = [LGUncaughtExceptionHandle lg_backtrace]; NSMutableDictionary *mDict = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo]; [mDict setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey]; [mDict setObject:exception.callStackSymbols forKey:LGUncaughtExceptionHandlerCallStackSymbolsKey]; [mDict setObject:@"LGException" forKey:LGUncaughtExceptionHandlerFileKey]; // exception - myException [[[LGUncaughtExceptionHandle alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:[NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:mDict] waitUntilDone:YES]; }
下面针对封装好的myException
进行处理,在这里要做两件事
- 存储,上传:方便开发人员检查修复
- 处理Crash奔溃,我们也不能眼睁睁看着
BUG
闪退在用户的手机上面,希望“起死回生,回光返照”
- (void)lg_handleException:(NSException *)exception{ // crash 处理 // 存 NSDictionary *userInfo = [exception userInfo]; [self saveCrash:exception file:[userInfo objectForKey:LGUncaughtExceptionHandlerFileKey]]; }
下面是一些封装的一些辅助函数
- 保存奔溃信息或者上传:针对封装数据本地存储,和相应上传服务器!
- (void)saveCrash:(NSException *)exception file:(NSString *)file{ NSArray *stackArray = [[exception userInfo] objectForKey:LGUncaughtExceptionHandlerCallStackSymbolsKey];// 异常的堆栈信息 NSString *reason = [exception reason];// 出现异常的原因 NSString *name = [exception name];// 异常名称 // 或者直接用代码,输入这个崩溃信息,以便在console中进一步分析错误原因 // NSLog(@"crash: %@", exception); NSString * _libPath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) objectAtIndex:0] stringByAppendingPathComponent:file]; if (![[NSFileManager defaultManager] fileExistsAtPath:_libPath]){ [[NSFileManager defaultManager] createDirectoryAtPath:_libPath withIntermediateDirectories:YES attributes:nil error:nil]; } NSDate *dat = [NSDate dateWithTimeIntervalSinceNow:0]; NSTimeInterval a=[dat timeIntervalSince1970]; NSString *timeString = [NSString stringWithFormat:@"%f", a]; NSString * savePath = [_libPath stringByAppendingFormat:@"/error%@.log",timeString]; NSString *exceptionInfo = [NSString stringWithFormat:@"Exception reason:%@\nException name:%@\nException stack:%@",name, reason, stackArray]; BOOL sucess = [exceptionInfo writeToFile:savePath atomically:YES encoding:NSUTF8StringEncoding error:nil]; NSLog(@"保存崩溃日志 sucess:%d,%@",sucess,savePath); }
- 获取函数堆栈信息,这里可以获取响应调用堆栈的符号信息,通过数组回传
+ (NSArray *)lg_backtrace{ void* callstack[128]; int frames = backtrace(callstack, 128);//用于获取当前线程的函数调用堆栈,返回实际获取的指针个数 char **strs = backtrace_symbols(callstack, frames);//从backtrace函数获取的信息转化为一个字符串数组 int i; NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for (i = LGUncaughtExceptionHandlerSkipAddressCount; i < LGUncaughtExceptionHandlerSkipAddressCount+LGUncaughtExceptionHandlerReportAddressCount; i++) { [backtrace addObject:[NSString stringWithUTF8String:strs[i]]]; } free(strs); return backtrace; }
- 获取应用信息,这个函数提供给
Siganl
数据封装
NSString *getAppInfo(){ NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n", [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"], [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"], [UIDevice currentDevice].model, [UIDevice currentDevice].systemName, [UIDevice currentDevice].systemVersion]; // [UIDevice currentDevice].uniqueIdentifier]; NSLog(@"Crash!!!! %@", appInfo); return appInfo; }
做完这些准备,你可以非常清晰的看到程序奔溃,哈哈哈!(好像以前奔溃还不清晰似的),这里说一下:我的意思你非常清晰的知道奔溃之前做了一些什么!
下面是检测我们奔溃之前的沙盒存储的信息:error.log
下面我们来一个骚操作:在监听的信息的时候来了一个Runloop
,我们监听所有的mode
,开启循环(一个相对于我们应用程序自启的Runloop
的平行空间).
SCLAlertView *alert = [[SCLAlertView alloc] initWithNewWindowWidth:300.0f]; [alert addButton:@"奔溃" actionBlock:^{ self.dismissed = YES; }]; [alert showSuccess:exception.name subTitle:exception.reason closeButtonTitle:nil duration:0]; // 本次异常处理 CFRunLoopRef runloop = CFRunLoopGetCurrent(); CFArrayRef allMode = CFRunLoopCopyAllModes(runloop); while (!self.dismissed) { // machO // 后台更新 - log // kill // for (NSString *mode in (__bridge NSArray *)allMode) { CFRunLoopRunInMode((CFStringRef)mode, 0.0001, false); } } CFRelease(allMode);
在这个平行空间
我们开启一个弹框,这个弹框,跟着我们的应用程序保活,并且具备相应的响应能力,到目前为止:此时此刻还有谁!这不就是回光返照
?只要我们的条件成立,那么在相应的这个平行空间
继续做一些我们的工作,程序不死:what is dead may never die,but rises again harder and stronger
signal 函数拦截不到的解决方式
在debug模式下,如果你触发了崩溃,那么应用会直接崩溃到主函数,断点都没用,此时没有任何log信息显示出来,如果你想看log信息的话,你需要在lldb
中,拿SIGABRT
来说吧,敲入pro hand -p true -s false SIGABRT
命令,不然你啥也看不到。
然后断开断点,程序进入监听,下面剩下的操作就是包装异常,操作类似Exception
最后我们需要注意的针对我们的监听回收相应内存:
NSSetUncaughtExceptionHandler(NULL); signal(SIGABRT, SIG_DFL); signal(SIGILL, SIG_DFL); signal(SIGSEGV, SIG_DFL); signal(SIGFPE, SIG_DFL); signal(SIGBUS, SIG_DFL); signal(SIGPIPE, SIG_DFL); if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName]) { kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]); } else { [exception raise]; }
到目前为止,我们响应的Crash
处理已经入门,如果你还想继续探索也是有很多地方比如:
- 我们能否hook系统奔溃,异常的方法
NSSetUncaughtExceptionHandler
,已达到拒绝传递UncaughtExceptionHandler
的效果 - 我们在处理异常的时候,利用
Runloop回光返照
,有没有更加合适的方法 -
Runloop回光返照
我们怎么继续保证应用程序稳定执行
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
为更强大而生的开源关系型数据库来了!阿里云RDS for MySQL 8.0 正式上线!
2019年5月29日15时,阿里云RDS for MySQL 8.0正式上线,使得阿里云成为紧跟社区步伐,发布MySQL最新版本的云厂商。RDS for MySQL 8.0 产品是阿里云推出的 MySQL 系列云产品之一,使用完全兼容 MySQL 8.0 的阿里云 AliSQL 8.0 分支,除了官方在 MySQL 8.0 推出的全新功能外,AliSQL 沉淀了许多在 Alibaba 集团电商业务和云上几十万客户在使用 MySQL 过程中遇到的问题和需求,以此来加固AliSQL, 提升 AliSQL 的性能和稳定性。 据介绍,MySQL 5.7 到 8.0,Oracle 官方跳跃了 Major Version 版本号,随之而来的就是在 MySQL 8.0 上做了许多重大更新,在往企业级数据库的路上大步前行,全新 Data Dicti
- 下一篇
Python爬虫入门教程 49-100 Appium安装+操作51JOB_APP(模拟手机操作之一)手机APP爬虫
爬前准备工作 在开始安装Appium之前,你要先知道Appium是做什么的?Appium 是一个自动化测试开源工具,看到没,做测试用的,它有点类似Selenium,可以自动操作APP实现一系列的操作。 标记重点,可以使用python对Appium编写脚本,实现对App的抓取。 今天就给你写一个100%叫你可以运行起来的入门实例。 下载地址 用稳定的最新版本即可。https://github.com/appium/appium-desktop/releases/tag/v1.10.0 下载之后,双击exe安装即可 出现如下界面,表示安装成功,先不要进行其他的操作,点击下面的 Edit Configurations 注意,在弹出的窗口中,需要配置的ANDROID_HOME和JAVA_HOME 这两个路径都需要安装Android Studio才可以配置
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- CentOS7,8上快速安装Gitea,搭建Git服务器
- CentOS8编译安装MySQL8.0.19
- SpringBoot2全家桶,快速入门学习开发网站教程
- Hadoop3单机部署,实现最简伪集群
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7,CentOS8安装Elasticsearch6.8.6