备忘录之-iOS Crash调试
1. 符号表
符号表是内存地址和函数名、文件名以及行号等内容的映射表,一般线上出现崩溃的话,我们能看到的大约是一堆一堆的内存地址,单独看这些地址并不能帮助我们很快的定位问题,有了符号表,就能将这些地址映射为我们容易理解的方法名,文件名等信息,这样找起来就很方便了。(如果你用过一些第三方的bug收集框架,比如bugly,在查看崩溃日志的时候,一般会提示你上传符号表,否则你可能只能看到一堆让人头大的内存地址)
要得到符号表,就要有.dSYM文件和crash文件,.dSYM文件是包含了调试信息的目标文件,一般打包或者调试的时候自动生成。
另外要注意的是,我们要给每一个发布版本找到对应的.dSYM文件,因为不同的版本,不同的代码,.dSYM文件也不一样。
下面,我们来找一找.dSYM文件
- 在打包过的历史中查找
window
-> Organizer
打开我们曾打包过的文件目录:
屏幕快照 2019-08-14 上午10.42.54.png 屏幕快照 2019-08-14 上午10.43.00.png
- 直接在Products下面生成的.app中也能找到对应的.dSYM文件
- apple developer My APP已经构建过的,在构建详情里也有下载dSYM的链接。
屏幕快照 2019-08-14 上午10.41.09.png 屏幕快照 2019-08-14 上午10.41.30.png注意:
如果是debug模式,或者有些情况下没有生成.dSYM文件的,我们需要检查下下面几个选项:
2. Crash文件
屏幕快照 2019-08-14 上午10.57.57.png 屏幕快照 2019-08-14 上午10.58.07.png
- 直接从手机导出crash文件
将手机连到电脑上后,在XCode的Window->Devices and Simulators下,找到对应的设备,点右侧的View Device Logs
打开下面的日志列表,在日志列表中找到自己app crash的日志,右键导出即可。
屏幕快照 2019-08-14 上午11.04.14.png
- 手机 设置->隐私->分析->分析数据 ,找到我们的app对应的crash日志,点击进入详情后,右上角有分享按钮,分享即可。
- 在apple developer My App那里获取苹果收集的crash.
- 利用第三方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被覆盖的话,就看不到准确的日志了。