iOS崩溃捕捉和分析

作者: 深山问 | 来源:发表于2016-04-05 09:20 被阅读6158次

    主题: 如何捕捉发布版本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文件。

    六、参考链接:

    相关文章

      网友评论

      • RobinZhao:请问一下可以在APP崩溃的时候不让APP崩溃而是用一个友好页面提示给用户?
        RobinZhao:@大东哥哥哥 这个方法NSLog可以打印,但是不能不能做UI操作,展示不了UI
        深山问:@大东哥哥哥 弹窗让用户手动去强制关闭App
        深山问:首先奔溃是不可逆的,不能跳过奔溃然后正常执行。可以考虑在`UncaughtExceptionHandler` 设置sleep一个无限长的时间,假装发生“主线程卡顿”,痰喘让用户手动去强制关闭App

      本文标题:iOS崩溃捕捉和分析

      本文链接:https://www.haomeiwen.com/subject/mveylttx.html