前言
崩溃是让发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很重要。调试阶段是比较容易找到出问题的地方的,但是已经上线的app并分析崩溃报告就比较麻烦了。最终,我们可以通过iOS崩溃日志在大多数情况下,你能从中了解到关于闪退的详尽、有用的信息。线上崩溃可以通过 iTunesConnect 中心的Cash收集,也可以通过第三方Cash收集工具,亦或自己在工程中手动收集崩溃日志上传到服务器中,本文做个小结,希望对初入者能有些帮助。
崩溃
崩溃是由于程序抛出异常,系统异常结束的一种现象。我们可以先了解一下异常 NSException,这对于我们理解崩溃有帮助。NSException掌控着程序的生命,程序的崩溃就是NSException来控制的。其实主要的出发点是让开发者认识到哪里的代码有问题。
** NSException**
其实控制台输出的日志信息就是NSException产生的,一旦程序抛出异常,程序就会崩溃,控制台就会有这些崩溃日志。
下面代码就会让你的程序崩溃(下面代码出自别人的文章,文末有原文出处)
//异常的名称
NSString *exceptionName = @"自定义异常";
//异常的原因
NSString *exceptionReason = @"我长得太帅了,所以程序崩溃了";
//异常的信息
NSDictionary *exceptionUserInfo = nil;
NSException *exception = [NSException exceptionWithName:exceptionName reason:exceptionReason userInfo:exceptionUserInfo];
NSString *aboutMe = @"太帅了";
if ([aboutMe isEqualToString:@"太帅了"])
{ //抛异常
@throw exception;
}
崩溃截图如下
NSException的实用技巧
1、 若自己封装一套SDK,若要提示哪里出错,那么就可以使用NSException。就像上面NSException的基本用法中的代码一样。
-
2、可以用来捕获异常,防止程序的崩溃。当你意识到某段代码可能存在崩溃的危险,那么你就可以通过捕获异常来防止程序的崩溃。代码如下
@try { //如果@try中的代码会导致程序崩溃,就会来到@catch //将一个nil插入到可变数组中,这行代码肯定有问题 [arrayM addObject:nilStr]; } @catch (NSException *exception) { //如果@try中的代码有问题(导致崩溃),就会来到@catch //在这里你可以进行相应的处理操作 //如果你要抛出异常(让程序崩溃),就写上 @throw exception } @finally { //@finally中的代码是一定会执行的 //你可以在这里进行一些相应的操作 }
崩溃日志
关于修复崩溃的Bug,如果你凭借自己的经验,有时候可能会遇到问题卡住,我想最快的方式就是通过分析崩溃日志来解决崩溃。
什么是崩溃日志,从哪里能得它
iOS设备上的应用闪退时,操作系统会生成一个崩溃报告,也叫崩溃日志,保存在设备上。
崩溃日志上有很多有用的信息,包括应用是什么情况下闪退的。通常,上面有每个正在执行线程的完整堆栈跟踪信息,所以你能从中了解到闪退发生时各线程都在做什么,并分辨出闪退发生在哪个线程上。
有几种方法可以从设备上获取崩溃日志。
-
xcode中查看崩溃信息
xcode->Window->Organizer->Crashes
-
通过Xcode查看设备崩溃信息
除了上面的系统分析工具来进行分析,如果是我们自己直接使用手机连接崩溃或者崩溃之后连接手机,选择window-> devices -> 选择自己的手机 -> view device logs 就可以查看我们的崩溃信息了。
-
使用第三方软件:itools等
如果你平时不用iTunes,而是使用itools这类第三方的软件对iPhone设备进行管理,也是没问题的。
打开itools,在你的设备下,找到“高级功能”,点击“崩溃日志”,然后将需要的日志导出到电脑里面就可以了! 应用提交到App Store后,你也能从 iTunes Connect 获取到用户的崩溃日志. 登录到 iTunes Connect 上, 选择 Manage Your Applications, 点击相应的应用, 点击应用图标下面的 View Details 按钮, 然后点击右栏Links部分的 Crash Reports 。
什么时候不会产生崩溃日志
以下情况不会有崩溃信息产生:
- 内存访问错误(不是野指针错误)
- 低内存,当程序内存使用过多会造成系统低内存的问题,系统会将程序内存回收
- 因为某种原因触发看门狗机制
一般Xcode不输出Crash日志有一下几个可能:
NSSetUncaughtExceptionHandler() 可能被重写了,(比如你引用了一些第三方库, 它的SDK里面可能包含了把Crash的日志上传到服务器, 这样这个日志可能被重写了, 就不打印本地的崩溃信息了) 尽量把它放在didFinishLaunchingWithOptions 最后面的一行代码块里.
还一种崩溃的情况是 EXC_BAD_ACCESS ,EXC_BAD_ACCESS异常的本意是指访问不到内存中这个地址的值,可能是由于些变量已经被回收了,亦可能是由于使用栈内存的基本类型的数据赋值给了id类型的变量。当遇到这种错误, 控制一般不会给你很多关于崩溃的信息, 这种崩溃你开启僵尸对象模式即可, 不过记住你在正式发布的时候记得把这个勾取消, 不然会造成内存泄漏。*
解析崩溃日志
.dSYM 文件
.dSYM 文件称为符号表,是指在Xcode项目编译后,在编译生成的二进制文件.app的同级目录下生成的同名的.dSYM文件。
.dSYM文件其实是一个目录,在子目录中包含了一个16进制的保存函数地址映射信息的中转文件,所有Debug的symbols都在这个文件中(包括文件名、函数名、行号等),所以也称之为调试符号信息文件。符号表就是用来符号化 crash log(崩溃日志)。crash log中有一些方法16进制的内存地址等,通过符号表就能找到对应的能够直观看到的方法名之类。
符号集是我们对ipa文件进行打包之后,和.app文件同级的后缀名为.dSYM的文件,这个文件必须使用Xcode进行打包才有。每一个.dSYM文件都有一个UUID,和.app文件中的UUID对应,代表着是一个应用。而.dSYM文件中每一条崩溃信息也有一个单独的UUID,用来和程序的UUID进行校对。这些UUID一致时才可以解析出当前APP的崩溃信息.
我们在Archive的时候会生成.xcarchive文件,然后显示包内容就能够在里面找到.dsYM文件和.app文件。
所以 为了更好的分析崩溃原因,在每次上架APP的时候,应该保留对应的app文件和dsym文件。
当获得一份crash日志时,我们需要将初始展示的十六进制地址等原始信息映射为源代码级别的方法名称和代码行数,使其对开发人员可读。这个过程称为符号化解析。要成功地符号化解析一份crash日志,我们需要有对应的应用程序二进制文件以及符号(.dSYM)文件。
当程序崩溃的时候,我们可以获得到崩溃的错误堆栈,但是这个错误堆栈都是0x开头的16进制地址,需要我们使用Xcode自带的symbolicatecrash工具来将.Crash和.dSYM文件进行符号化,就可以得到详细崩溃的信息。
Symbolicatecrash
Symbolicatecrash是Xcode自带的一个分析工具,可以通过机器上的崩溃日志和应用的.dSYM文件定位发生崩溃的位置,把crash日志中的地址替换成代码相应位置。
使用效果:
分析前:
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 CoreFoundation 0x3723b870 0x37180000 + 768112
1 CoreFoundation 0x37196648 0x37180000 + 91720
2 CoreFoundation 0x37181e90 0x37180000 + 7824
3 CoreFoundation 0x3718bb74 0x37180000 + 47988
4 CoreFoundation 0x3718ba8e 0x37180000 + 47758
5 UIKit 0x30f0f866 0x30f0a000 + 22630
分析后:
0 CoreFoundation 0x3723b870 ___forwarding___ + 136
1 CoreFoundation 0x37196648 _CF_forwarding_prep_0 + 40
2 CoreFoundation 0x37181e90 CFRetain + 76
3 CoreFoundation 0x3718bb74 +[__NSArrayI __new::] + 48
4 CoreFoundation 0x3718ba8e -[__NSPlaceholderArray initWithObjects:count:] + 294
5 UIKit 0x30f0f866 -[UIView(Hierarchy) _makeSubtreePerformSelector:withObject:withObject:copySublayers:] + 70
在这个路径下你可以得到系统自带的Symbolicatecrash,把它拷贝到指定的文件下
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources
获取.dSYM文件
选中archive的版本右击,选择Show in Finder就可以选中archived 文件然后显示包内容,就可以找到dSYM文件了。
解析步骤
- 我在解析崩溃信息的时候,首先在桌面上建立一个Crash文件夹,然后将.Crash、app、.dSYM、symbolicatecrash放在这个文件夹中。
注意:这里的 .crash 必须是真机安装的打包的那个 sometwo 产生的崩溃日志才行,运行其他的版本产生的崩溃日志,以下的解析会失败。
如何把这个打包的应用安装到测试机上呢?注意这里的应用不是 ipa文件,而且这个手机也可以没被加入到当前的开发者账号中。
手机连上 itunes,在itunes中打开 手机的应用, 文件->添加到资料库 把桌面是上的那个应用添加进入,再同步更新到测试机器中即可。
如果你一直解析失败,那么可能你的 .Crash、app、.dSYM、的UUID不一致,通过终端工具可以查看 app、 .dSYM文件的UUID:
cd到文件夹
dwarfdump --uuid Sometwo.app/Sometwo
dwarfdump --uuid Sometwo.app.dSYM
三者一致才能还原符号表。
由上图可以看出三折的UUID是不一致的,所以会一直解析失败,无法符号化 .Crash文件。
在终端中输入以下命令, iOS002 换成你自己的用户名称
- cd /Users/iOS002/Desktop/Cash/
- export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
- ./symbolicatecrash /Users/iOS002/Desktop/Cash/SomeTwo.crash /Users/iOS002/Desktop/Cash/SomeTwo.app.dSYM > Control_symbol.crash
一切正常的话这样就完成了一个崩溃日志的解析工作。解析完成后会生成一个新的.Crash文件,这个文件中就是崩溃详细信息。图中红色标注的部分就是我们代码崩溃的部分。
收集崩溃日志
获取崩溃信息方式
在iOS中获取崩溃信息的方式有很多,比较常见的是使用友盟、云测、百度、Crashlytics等第三方分析工具,或者自己收集崩溃信息并上传公司服务器。下面列举一些我们常用的崩溃分析方式:
- 自己实现应用内崩溃收集,并上传服务器。
- 使用友盟、云测、百度、Crashlytics等第三方崩溃统计工具。
自己收集崩溃信息
苹果给我们提供了异常处理的类,NSException类。这个类可以创建一个异常对象,也可以通过这个类获取一个异常对象。这个类中我们最常用的还是一个获取崩溃信息的C函数,我们可以通过这个函数在程序发生异常的时候收集这个异常。然后把收集到的崩溃信息发送到自己的服务器。
我们也可以通过下面方法获取崩溃统计的函数指针:
NSUncaughtExceptionHandler *handler = NSGetUncaughtExceptionHandler();
NSSetUncaughtExceptionHandler (&UncaughtExceptionHandler);
// 收集崩溃信息的调用方法
void UncaughtExceptionHandler(NSException *exception) {
NSArray *arr = [exceptioncallStackSymbols];//得到当前调用栈信息
NSString *reason = [exceptionreason];//非常重要,就是崩溃的原因
NSString *name = [exceptionname];//异常类型
NSLog(@"exception type : %@ \n crash reason : %@ \n call stack info : %@", name, reason, arr);
}
获取到了崩溃的发送给开发者有以下两种方式:
将崩溃信息持久化在本地,下次程序启动时,将崩溃信息作为日志发送给开发者。
-
通过邮件发送给开发者。不过此种方式需要得到用户的许可,因为iOS不能后台发送短信或者邮件,会弹出发送邮件的界面,只有用户点击了发送才可发送。不过,此种方式最符合苹果的以用户至上的原则。
发送邮件代码: NSString *crashLogInfo = [NSString stringWithFormat:@"exception type : %@ \n crash reason : %@ \n callstack info : %@", name, reason, arr]; NSString*urlStr = [NSString stringWithFormat:@"mailto://[email protected]?subject=bug报告&body=感谢您的配合! 错误详情:%@",crashLogInfo]; NSURL *url =[NSURL URLWithString:[urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; [[UIApplication sharedApplication] openURL:url];
我们是否也可以在程序崩溃时,将崩溃信息写入本地,APP再次启动时,将崩溃信息上传到我们的服务器。这里就要用到apple的一个函数:NSSetUncaughtExceptionHandler。上代码:
application didFinishLaunchingWithOptions中调用
[self catchCrashLogs];
- (void)catchCrashLogs{
NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
}
void UncaughtExceptionHandler(NSException *exception){
if (exception ==nil)return;
NSArray *array = [exception callStackSymbols];
NSString *reason = [exception reason];
NSString *name = [exception name];
NSDictionary *dict = @{@"appException":@{@"exceptioncallStachSymbols":array,@"exceptionreason":reason,@"exceptionname":name}};
if([SDFileToolClass writeCrashFileOnDocumentsException:dict]){
NSLog(@"Crash logs write ok!");
}
}
//写入缓存中: 以下提供三个API,分别是:写入,获取,清空
NSString * const SDCrashFileDirectory = @"SDMapHomeCrashFileDirectory"; //你的项目中自定义文件夹名
+ (NSString *)sd_getCachesPath{
return [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
}
+ (BOOL)writeCrashFileOnDocumentsException:(NSDictionary *)exception{
NSString *time = [[NSDate date] formattedDateWithFormat:@"yyyyMMddHHmmss" locale:[NSLocale currentLocale]];
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
NSString *crashname = [NSString stringWithFormat:@"%@_%@Crashlog.plist",time,infoDictionary[@"CFBundleName"]];
NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
NSFileManager *manager = [NSFileManager defaultManager];
//设备信息
NSMutableDictionary *deviceInfos = [NSMutableDictionary dictionary];
[deviceInfos setObject:[infoDictionary objectForKey:@"DTPlatformVersion"] forKey:@"DTPlatformVersion"];
[deviceInfos setObject:[infoDictionary objectForKey:@"CFBundleShortVersionString"] forKey:@"CFBundleShortVersionString"];
[deviceInfos setObject:[infoDictionary objectForKey:@"UIRequiredDeviceCapabilities"] forKey:@"UIRequiredDeviceCapabilities"];
BOOL isSuccess = [manager createDirectoryAtPath:crashPath withIntermediateDirectories:YES attributes:nil error:nil];
if (isSuccess) {
NSLog(@"文件夹创建成功");
NSString *filepath = [crashPath stringByAppendingPathComponent:crashname];
NSMutableDictionary *logs = [NSMutableDictionary dictionaryWithContentsOfFile:filepath];
if (!logs) {
logs = [[NSMutableDictionary alloc] init];
}
//日志信息
NSDictionary *infos = @{@"Exception":exception,@"DeviceInfo":deviceInfos};
[logs setObject:infos forKey:[NSString stringWithFormat:@"%@_crashLogs",infoDictionary[@"CFBundleName"]]];
BOOL writeOK = [logs writeToFile:filepath atomically:YES];
NSLog(@"write result = %d,filePath = %@",writeOK,filepath);
return writeOK;
}else{
return NO;
}
}
+ (nullable NSArray *)sd_getCrashLogs{
NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
NSFileManager *manager = [NSFileManager defaultManager];
NSArray *array = [manager contentsOfDirectoryAtPath:crashPath error:nil];
NSMutableArray *result = [NSMutableArray array];
if (array.count == 0) return nil;
for (NSString *name in array) {
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:[crashPath stringByAppendingPathComponent:name]];
[result addObject:dict];
}
return result;
}
+ (BOOL)sd_clearCrashLogs{
NSString *crashPath = [[self sd_getCachesPath] stringByAppendingPathComponent:SDCrashFileDirectory];
NSFileManager *manager = [NSFileManager defaultManager];
if (![manager fileExistsAtPath:crashPath]) return YES; //如果不存在,则默认为删除成功
NSArray *contents = [manager contentsOfDirectoryAtPath:crashPath error:NULL];
if (contents.count == 0) return YES;
NSEnumerator *enums = [contents objectEnumerator];
NSString *filename;
BOOL success = YES;
while (filename = [enums nextObject]) {
if(![manager removeItemAtPath:[crashPath stringByAppendingPathComponent:filename] error:NULL]){
success = NO;
break;
}
}
return success;
}
使用工具Crashlytics统计Crash
市场上有多种移动应用Crash收集工具, 如友盟,MTJ等。在iOS中, 收集Crash主要通过两种方式, 一种是信号量机制,因为crash通常会发出信号量,标明某某应用崩溃了, 另一种方式是每一个应用都有一个crash handle, 即崩溃钩子, 每当程序崩溃时, 都会执行这个回调。信号量比起崩溃句柄的区别有点像ios开发中的通知和delegate。 信号量抛出后,可以被多个捕获crash的工具获取到,然后取当前的堆栈信息, 再利用该堆栈信息与原app的dsym文件进行比对, 就可以找到崩溃的代码行。
理论上讲, 这个信号量机制优秀于crash句柄, 因为这样的话,可以有多个收集工具并行收集, 前提是,每个收集工具收集后,继续抛出这个异常,而不是截断这个异常,当截断后后续的其它工具就收集不到这个异常了, 会导致其它工具收集不全的问题。 而友盟正是这样做的.
利用crash 句柄这种方式使得crash信息只能被一个收集工具所收集到,因为句柄只有一个。如果一个应用中有多个收集工具都设置了这个句柄, 这里就得看谁最后设置这个句柄, 谁就有效。
上面是收集crash的方式说明, 现在说说Crashlytics这个工具。 原理和上面的一样。 不一样的是, 这个工具被twitter收购, 既然有这么一根大树, 那就保证了这个工具的稳定性。 所以建议使用, 目前是免费的。
使用步骤基本上可以分为如下:
- 注册,
- 收到邀请信, 然后一步步按其说明完成注册。
- 根据其提示,下载一个mac app配合进行使用。
- 当有崩溃发生时,会给注册的邮件发送崩溃统计,方便查看。
在crash信息收集时, 如果正在进行debug调试,是收集不到信息的。
使用Crashlytics的好处:
- Crashlytics不会漏掉任何应用崩溃信息(就这两个字让我决定使用crashlytics)
- Crashlytics可以象Bug管理工具那样,管理这些崩溃日志, 可以根据频率及影响用户量来自动设置优先级
- 可以每天和每周将崩溃信息汇总发送到邮箱中。
具体使用,可以参照这篇文章Crashlytics
小结
有关应用Crash的处理工作任重而道远,后续会持续更新,先写这些吧。
本文参考文章:
关于崩溃日志解读很详细很棒的的一篇文章
iOS被开发者遗忘在角落的NSException-其实它很强大
iOS崩溃调试(收集不同用户的崩溃信息)
模拟器打印不出来 malloc stock的信息,需要真机。
1.unrecognized seletor。错误:这种情况很简单,给一个对象发送了一条它不认识的消息。比如说你的.h中声明了某一个方法,但是.m中却没有实现,而且你没有对异常消息处理(消息转发)就会造成这种现象。解决办法:首先排查自己的某一些方法是否实现,其次看一下哪些对象接收了它不该接收的消息。
2.index 1 beyond NSArraMu [0,0]数组越界:数组越界这个不多说。
3.NSNul length 这个异常以可以归类为第一种,也是给某一个对象发送了不识别的消息。常见原因有:给UILabel对象设置了text,此时的text内容为空字符串null,然后你在取text的length的时候就会抛出异常。
4.EXC_BAD_ACCESS异常:这种大多数是对象提前释放,访问了野指针的错误。解决办法:排查所有声明为weak对象的使用,是否在没有持有的情况下再次访问了该对象(该对象已经被释放),第二在MRC情况下,排查一下所以已经release的对象(声明一点,MRC中全局变量最好在dealloc方法中进行释放),第三排查一下所有block,是否block被正常赋值等。
5.崩溃在main函数。这种情况最苦逼也是最难找到bug所在,这种情况下,用@try @catch将main函数包裹起来,这样会抛出异常堆栈信息等,或者通过添加全局breakPoint来追踪bug。(扯淡)
@try @catch 是最后的大招