iOS崩溃捕捉和分析

主题: 如何捕捉发布版本ipa的崩溃, 并定位崩溃代码

一、 崩溃日志

  • 1 什么是崩溃日志
    iOS设备上的应用闪退时, 操作系统会声称一个崩溃日志, 保存在设备上。
路径是:  设置 -> 隐私 ->诊断与用量 ->诊断与用量数据。在这里可以看到设备上所有的设备崩溃日志.
在“诊断与用量”界面,建议用户选择自动发送,这样可以每天自动发送诊断和用量数据到itunes,来帮助开发者分析崩溃.
  • 2如何获取崩溃日志
    2.1 连接设备获取崩溃日志
    设备与电脑上的ITunes Store同步后, 会将崩溃日志保存在电脑上,崩溃日志保存在以下位置:
Mac OS X:   ~/Library/Logs/CrashReporter/MobileDevice/
可以看到所有和该电脑同步过的设备的崩溃日志(.crash文件)
iOS设备上的崩溃日志

2.2 通过Xcode获取崩溃日志
打开Xcode, 菜单栏上选择Window ->Devices,选中设备,点击View Device Logs -> All logs可以看到所有的崩溃日志。
选中某一个崩溃日志,点击Export Log可导出崩溃日志(.crash文件)


Xcode 查看崩溃日志

2.3 通过iTunes Connect获取使用者上传的崩溃日志
登录iTunes Connect, 选中APP, 点击可供销售的APP(即当前最新版本), 在最下面选中额外信息下的崩溃报告, 可以看到所有iOS版本下的崩溃报告。


iTunes Connect 崩溃日志

二、iOS 崩溃日志分析

首先来看一份崩溃日志


iOS崩溃日志

(1)Incident Identifier: 是崩溃报告的唯一标识符。
(2)CrashReporter Key: 是与设备标识相对应的唯一键值。虽然它不是真正的设备标识符,但也是一个非常有用的情报:如果你看到100个崩溃日志的CrashReporter Key值都是相同的,或者只有少数几个不同的CrashReport值,说明这不是一个普遍的问题,只发生在一个或少数几个设备上。
(3)Hardware Model: 标识设备类型。 如果很多崩溃日志都是来自相同的设备类型,说明应用只在某特定类型的设备上有问题。上面的日志里,崩溃日志产生的设备是iPhone 6(但是显示的是iPhone7,2? 暂时不清楚原因)。
(4)Process 是应用名称。中括号里面的数字是闪退时应用的进程ID。
(5)Version: App版本号
最重要的两部分
(1)Exception Type:EXC_CRASH (SIGABRT)
(2)Last Exception Backtrace(即发生崩溃的原因,也是我们要研究的重点)

Xcode会自动符号化代码, 翻译成明文, 如下:

Crash Logs

可以看到发生崩溃的代码位于[SCHomePageVC viewDidLoad]方法中第408行。
崩溃的代码是[NSArrayM insertObject:atIndex:]。
找到该行代码,可以看到崩溃日志中所描述的崩溃发生的位置,代码都和时机代码一致。


崩溃的代码

崩溃的原因是: The object to add to the array's content. This value must not be nil.

三、如何通过.crash文件反编译得到明文的crash文件

步骤如下:
  • Step1: 在桌面上创建一个空的文件夹, 我将其命名为 DebugTest , 然后将三个文件放入该文件夹 "MyApp.app" , "MyApp.app.dSYM", "MyApp_2016_4_1.crash"。
  • Step2 : 打开Applications文件夹,找到 symbolicatecrash 文件, Xcode和Xcode以上,文件位置
//终端中输入以下命令:
cd /Applications/Xcode.app/Contents/SharedFrameworks/DTDeviceKitBase.framework/Versions/A/Resources

然后你会发现symbolicatecrash文件,长这个样子,将其拷贝到DebugTest文件夹中

symbolicatecrash

到这一步,你的DebugTest目录机构应该是这样
(1MyAPP.app
(2)MyApp.app.dSYM
(3)MyApp_2016_4_1.crash
(4)symbolicatecrash

  • Step3: 在终端中输入以下3条命令
//第一条命令(其中Yourname 应该是你的用户名)
 cd /Users/Yourname/Desktop/DebugTest
// 第二条命令
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
//第三条命令(二选一)
(Xcode6.3和之前版本输入以下命令)
./symbolicatecrash -A -v MYApp_2016-4-1.crash MyApp.app.dSYM
(Xcode6.4和之前版本输入以下命令)
./symbolicatecrash  -v MYApp_2016-4-1.crash MyApp.app.dSYM

然后用控制台打开你的MyApp_2016_4_1.crash文件, 你就会看到编译后的crash文件, 同Xcode看到的崩溃日志一致。通过查看崩溃日志,可以轻易的找到崩溃原因并修正。

Crash Logs

四、如何在程序崩溃时手动捕捉到崩溃

   当我们debug的时候, 发生崩溃后可以在控制台上看到崩溃的堆栈信息和崩溃日志。上面三种方法都是我们获取.crash文件后解析的办法, 那么如果用户不发送崩溃日志到iTunes Connect时,我们如何获取崩溃信息呢?(尽可能的获取崩溃信息有助于热修复时定位代码)。当然,友盟支持搜集崩溃日志,那我们是否也可以在程序崩溃时,将崩溃信息写入本地,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;
}

Well done!

五、结论: 为了更好的分析崩溃原因,在每次上架APP的时候,应该保留对应的app文件和dsym文件。

六、参考链接:

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

推荐阅读更多精彩内容