备忘录之-iOS Crash调试

2019-08-13  本文已影响0人  忧郁的小码仔

1. 符号表

符号表是内存地址和函数名、文件名以及行号等内容的映射表,一般线上出现崩溃的话,我们能看到的大约是一堆一堆的内存地址,单独看这些地址并不能帮助我们很快的定位问题,有了符号表,就能将这些地址映射为我们容易理解的方法名,文件名等信息,这样找起来就很方便了。(如果你用过一些第三方的bug收集框架,比如bugly,在查看崩溃日志的时候,一般会提示你上传符号表,否则你可能只能看到一堆让人头大的内存地址)

要得到符号表,就要有.dSYM文件和crash文件,.dSYM文件是包含了调试信息的目标文件,一般打包或者调试的时候自动生成。
另外要注意的是,我们要给每一个发布版本找到对应的.dSYM文件,因为不同的版本,不同的代码,.dSYM文件也不一样。

下面,我们来找一找.dSYM文件

  1. 在打包过的历史中查找

window -> Organizer打开我们曾打包过的文件目录:

右键在文件中显示 找到对应文件,显示包内容 .dSYM文件
  1. 直接在Products下面生成的.app中也能找到对应的.dSYM文件
屏幕快照 2019-08-14 上午10.42.54.png 屏幕快照 2019-08-14 上午10.43.00.png
  1. apple developer My APP已经构建过的,在构建详情里也有下载dSYM的链接。

注意:
如果是debug模式,或者有些情况下没有生成.dSYM文件的,我们需要检查下下面几个选项:

屏幕快照 2019-08-14 上午10.41.09.png 屏幕快照 2019-08-14 上午10.41.30.png

2. Crash文件

  1. 直接从手机导出crash文件
    将手机连到电脑上后,在XCode的Window->Devices and Simulators下,找到对应的设备,点右侧的View Device Logs打开下面的日志列表,在日志列表中找到自己app crash的日志,右键导出即可。
屏幕快照 2019-08-14 上午10.57.57.png 屏幕快照 2019-08-14 上午10.58.07.png
  1. 手机 设置->隐私->分析->分析数据 ,找到我们的app对应的crash日志,点击进入详情后,右上角有分享按钮,分享即可。
屏幕快照 2019-08-14 上午11.04.14.png
  1. 在apple developer My App那里获取苹果收集的crash.
  2. 利用第三方app crash分析平台,比如Bugly.

3. 文件校验,以及crash文件符号化

在符号化crash文件之前,我们要先校验crash文件,.dSYM文件的UUID,只有UUID一致才表明crash文件和.dSYM文件是匹配的,才能正确的符号化crash文件。

比如,下面是我们的crash文件和.dSYM文件:


屏幕快照 2019-08-14 下午1.10.46.png

获取crash文件的UUID:

grep "[AppName] arm64" t.crash

比如,我的app叫PlayWithCrash:


屏幕快照 2019-08-14 下午1.13.17.png

获取.dSYM文件的UUID:

dwarfdump --uuid [.dSYM文件名]

比如:


屏幕快照 2019-08-14 下午1.14.03.png

这里crash文件的UUID是连续的小写字母,.dSYM文件的UUID是类似XXX-XXX-XXX这样的字符。

确认两者的UUID一致之后,我们开始将crash文件符号化,这里需要用到XCode的一个脚本工具symbolicatecrash,我们要用它将crash文件符号化,执行下面命令来获取symbolicatecrash文件的目录:

find /Applications/Xcode.app -name symbolicatecrash -type f
屏幕快照 2019-08-14 下午1.18.21.png
最后这条SharedFrameworks目录就是我们需要的路径,到该路径下将symbolicatecrash文件拷贝到.crash和.dSYM同一个文件夹下面,然后执行下面两条命令:
export DEVELOPER_DIR=/Applications/XCode.app/Contents/Developer
./symbolicatecrash ./mycrash.crash ./PlayWithCrash.app.dSYM > symbolResult.crash

这样,就生成了一个新的符号化后的crash文件,来对比下符号化前后的变化:


result.png

最大的变化是,符号化之前,报异常的地方是一堆内存地址,符号化之后,我们看到的不仅有内存地址,更有这些内存地址对应的方法。

4. 崩溃日志上传

关于崩溃日志收集,有很多第三方的服务可以集成使用,这里,我们选择自己将崩溃日志上传到自己的服务器。

这里要用到下面这个方法:

NSSetUncaughtExceptionHandler(...)

这里我们传递一个C函数的地址进去,该方法用于设置最顶层的错误处理方法,可以用来在应用崩溃退出之前做一些处理。(我们可以在这里将崩溃的日志记录下来,下次应用登陆的时候将上次的日志上传给后台即可)

void UncaughtExceptionHandler(NSException *exception) {
    NSArray *symbols = [exception callStackSymbols]; // 包含调用堆栈符号的数组
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    
    LastException *lastException = [[LastException alloc] init];
    lastException.symbols = symbols;
    lastException.reason = reason;
    lastException.name = name;

    NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"lastException.archiver"];
    BOOL result = [NSKeyedArchiver archiveRootObject:lastException toFile:filePath];
    if (result) {
        NSLog(@"归档成功");
    } else {
        NSLog(@"归档失败");
    }
}


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"lastException.archiver"];
    LastException *lastException = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    if (lastException) {

        NSString *pfxPath = [[NSBundle mainBundle] pathForResource:@"mymeizi" ofType:@"cer"];
        NSData *pfxData = [NSData dataWithContentsOfFile:pfxPath];

        // AFSSLPinningModeCertificate 和
        // AFSSLPinningModePublicKey 只能用于安全的访问链接,及浏览器地址栏https安全表示是绿色那种
        // 如果不是从可信任机构颁发的,而是自签名证书,就用AFSSLPinningModeNone模式
        AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
        // 默认不允许过期或者自签名证书的,如果是自签名证书,这里要设置为YES
        securityPolicy.allowInvalidCertificates = YES;
        // 默认是要验证请求的域名和证书中的域名是否完全一致,即使是子域名也不行,这里我们可以在证书中使用通配符域名
        // 这里,我们用于测试服务器,直接使用IP地址,所以把它关掉即可。
        securityPolicy.validatesDomainName = NO;
        if (pfxData) {
            securityPolicy.pinnedCertificates = [[NSSet alloc] initWithObjects:pfxData, nil];
        }


        AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
        [manager setSecurityPolicy:securityPolicy];
        manager.requestSerializer = [[AFJSONRequestSerializer alloc] init];
        manager.responseSerializer = [[AFJSONResponseSerializer alloc] init];

        NSMutableDictionary *params = [[NSMutableDictionary alloc] init];
        [params setValue:lastException.symbols forKey:@"symbols"];
        [params setValue:lastException.reason forKey:@"reason"];
        [params setValue:lastException.name forKey:@"name"];


        [manager POST:@"https://192.168.1.57:443/uploadException" parameters:params progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
            
            NSFileManager *fileManager = [NSFileManager defaultManager];
            if ([fileManager fileExistsAtPath:filePath]) {
                NSError *error;
                [fileManager removeItemAtPath:filePath error:&error];
                if (error) {
                    NSLog(@"删除失败");
                } else {
                    NSLog(@"删除成功");
                }
            }
            
        } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
            NSLog(@"上传失败");
        }];
    }


    NSSetUncaughtExceptionHandler(&UncaughtExceptionHandler);
    
    return YES;
}

这里,我们将最后截获的Exception先归档,UncaughtExceptionHandler函数这里不能发送网络请求,我们只能先归档,然后再app launch的时候检查有没有异常,有的话就上传,上传后删除即可。

另外,如果使用第三方收集框架的话,最好只使用一个,如果NSSetUncaughtExceptionHandler被覆盖的话,就看不到准确的日志了。

上一篇下一篇

猜你喜欢

热点阅读