一次阿里云日志接口导致崩溃问题的经历

简要过程

  • 在工程中引入日志系统,使用了第三方库CocoaLumberjack,网络发送采用了阿里的开源库AliyunLogObjc

  • 自测没什么问题,日志也能发送到阿里云后台

  • 在版本上线日,爆出了崩溃的问题。并且现象比较奇怪,用XCode在模拟器上和真机上跑都没有问题,但是用Jekins打包之后安装到手机上,就崩溃,Release模式和debug模式都崩溃。

  • 这期的改动(非业务)主要是集成了日志系统。所以我把日志系统卸了,然后再试,崩溃现象就没有了。

  • 从崩溃的手机导出崩溃日志,符号化之后,可以看出,应该是日志系统的问题,并且问题出在阿里云日志发送函数上面。

Last Exception Backtrace:
0   CoreFoundation                  0x1904bafd8 __exceptionPreprocess + 124
1   libobjc.A.dylib                 0x18ef1c538 objc_exception_throw + 56
2   CoreFoundation                  0x1904baf20 +[NSException raise:format:] + 116
3   xxxxxxxxx                       0x100c291fc UmengSignalHandler + 128
4   libsystem_platform.dylib        0x18f57930c _sigtramp + 36
5   xxxxxxxxx                       0x100418148 -[NSString(Crypto) SHA1WithSecret:] (NSString+Crypto.m:21)
6   xxxxxxxxx                       0x100418148 -[NSString(Crypto) SHA1WithSecret:] (NSString+Crypto.m:21)
7   xxxxxxxxx                       0x1003fc6e8 -[LogClient GetHttpHeadersFrom:url:body:bodyZipped:] (LogClient.m:101)
8   xxxxxxxxx                       0x1003fbc28 __39-[LogClient PostLog:logStoreName:call:]_block_invoke (LogClient.m:63)
9   libdispatch.dylib               0x18f3729e0 _dispatch_call_block_and_release + 24
10  libdispatch.dylib               0x18f3729a0 _dispatch_client_callout + 16
11  libdispatch.dylib               0x18f382bac _dispatch_root_queue_drain + 888
12  libdispatch.dylib               0x18f3827d0 _dispatch_worker_thread3 + 124
13  libsystem_pthread.dylib         0x18f57b1d0 _pthread_wqthread + 1096
14  libsystem_pthread.dylib         0x18f57ad7c start_wqthread + 4

崩溃日志符号化

  • 遇到崩溃,一种方法是登录友盟后台,查崩溃日志,然后将里面的地址用一个工具,(名字叫DSYMTools),可以看到有意义的信息。不过这种方式比较原始,不是很方便

  • 如果是调试过程中遇到崩溃,不需要这么麻烦,将XCode的“僵尸对象”那个选项勾上,一般都能够定位到。

  • 如果是测试机上发生的崩溃,可以把崩溃日志导出,后缀名是.ips可以直接改后缀为.crash

  • 如果不符号化,那么崩溃日志基本没用,一堆地址,看不出什么东西。

  • 崩溃日志符号化,需要symbolicatecrash工具、xxx.appxxx.app.dSYM这几个要素

  • 然后记住几句命令行,比如:

./symbolicatecrash /Users/xxxx/Desktop/crash/InOrder.crash /Users/xxxx/Desktop/crash/InOrder.app.dSYM > Control_symbol.crash

新生成的Control_symbol.crash就是符号化过的,里面的信息对于定位崩溃发生点还是有帮助的。

简要分析

崩溃报告中,以下几行跟应用有关,其他的基本上是系统库。这些函数,来自阿里云提供的日志上传接口

5   xxxxxxxxx                       0x100418148 -[NSString(Crypto) SHA1WithSecret:] (NSString+Crypto.m:21)
6   xxxxxxxxx                       0x100418148 -[NSString(Crypto) SHA1WithSecret:] (NSString+Crypto.m:21)
7   xxxxxxxxx                       0x1003fc6e8 -[LogClient GetHttpHeadersFrom:url:body:bodyZipped:] (LogClient.m:101)
8   xxxxxxxxx                       0x1003fbc28 __39-[LogClient PostLog:logStoreName:call:]_block_invoke (LogClient.m:63)
  • LogClient.m:63内容如下:调用函数GetHttpHeadersFrom
NSDictionary* httpHeaders = [self GetHttpHeadersFrom:name url:httpUrl body:httpPostBody bodyZipped:httpPostBodyZipped];
  • LogClient.m:101内容如下:在函数GetHttpHeadersFrom中调用函数SHA1WithSecret
NSString *sign = [signString SHA1WithSecret:_mAccessKeySecret];
  • NSString+Crypto.m:21内容如下:
-(NSString *)SHA1WithSecret:(NSString *)secret {
    const char *cKey   = [secret cStringUsingEncoding:NSUTF8StringEncoding];
    const char *cData  = [self cStringUsingEncoding:NSUTF8StringEncoding];
    
    unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH];
    
    // 这里就是第21行
    CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
    
    NSData *HMAC   = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
    
    NSString *hash = [HMAC base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    
    return hash;
}
  • 问题1,这里只是进行SH1加密,怎么会导致崩溃?而且这种写法也能在网上找到相关的文章,看不出什么问题。
    Objective-C sample code for HMAC-SHA1 [closed]这个和这里一模一样
    iOS 使用HMAC这个基本也差不多,只是用了NSData数据类型参与运算。

  • 问题2,-[NSString(Crypto) SHA1WithSecret:] (NSString+Crypto.m:21)调用了两次,代码中明明只有一个地方调用。

  • 问题3,用XCode安装或者打包都不会崩溃。用Jekins打包,用iTools安装的包才会崩溃。打包只是调用命令行啊,怎么会有差别?

  • 问题4,Jekins打包,iTools安装,崩溃之后将手机连上XCode,看不到崩溃日志。这种情况也是少见的。什么原因?
    比如下面两篇文章就有图文并茂的介绍
    ios crash的原因与抓取crash日志的方法
    Xcode崩溃日志分析工具symbolicatecrash用法

  • 实际试验结果:改用第二种NSData数据类型参与运算,用Jekins打包后的程序不崩溃了。 原因不清楚,实在太奇怪了。这种情况目前当个例来看,其他地方应该不会遇到。

-(NSString *)SHA1WithSecret:(NSString *)secret {
    // 这块代码在网上也能找到出处,比如https://stackoverflow.com/questions/756492/objective-c-sample-code-for-hmac-sha1
    // 不过实际使用中,用XCode安装没有问题,但是用Jekins打包会导致崩溃。原因暂时不清楚
//    const char *cKey   = [secret cStringUsingEncoding:NSUTF8StringEncoding];
//    const char *cData  = [self cStringUsingEncoding:NSUTF8StringEncoding];
    
//    unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH];
    
//    CCHmac(kCCHmacAlgSHA1, cKey, strlen(cKey), cData, strlen(cData), cHMAC);
    
    // 这种写法在网上也能找到出处,比如http://blog.csdn.net/u011604049/article/details/55044483
    // 实际试验了一下,这种写法用Jekins打出的包不崩溃。
    NSData *datas = [self dataUsingEncoding:NSUTF8StringEncoding];
    size_t dataLength = datas.length;
    NSData *keys = [secret dataUsingEncoding:NSUTF8StringEncoding];
    size_t keyLength = keys.length;
    unsigned char cHMAC[CC_SHA1_DIGEST_LENGTH];
    
    CCHmac(kCCHmacAlgSHA1, [keys bytes], keyLength, [datas bytes], dataLength, cHMAC);
    
    NSData *HMAC   = [[NSData alloc] initWithBytes:cHMAC length:sizeof(cHMAC)];
    
    NSString *hash = [HMAC base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed];
    
    return hash;
}

搜集崩溃信息

  • IOS SDK中提供了一个现成的函数 NSSetUncaughtExceptionHandler 用来做异常处理。

  • 上面的崩溃日志中包含这么一句xxxxxxxxx 0x100c291fc UmengSignalHandler + 128估计友盟也是通过这个API进行日志搜集的吧。

  • 崩溃信息抓到之后,一般的做法是调用邮件程序,让用户给开发者发邮件,比如全面的理解和分析IOS的崩溃日志中列举的例子

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [[AFNetworkReachabilityManager sharedManager] startMonitoring];
    appDelegate = self;
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    // Override point for customization after application launch.
    self.window.backgroundColor = [UIColor whiteColor];
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];
    _notification = notification;

    NSSetUncaughtExceptionHandler(&caughtExceptionHandler);
    /*Changes the top-level error handler.
    Sets the top-level error-handling function where you can perform last-minute logging before the program terminates
    */
    return YES;
}

void caughtExceptionHandler(NSException *exception){
    /**
     *  获取异常崩溃信息
     */
    NSArray *callStack = [exception callStackSymbols];
    NSString *reason = [exception reason];
    NSString *name = [exception name];
    NSString *content = [NSString stringWithFormat:@"========异常错误报告========\\nname:%@\\nreason:\\n%@\\ncallStackSymbols:\\n%@",name,reason,[callStack componentsJoinedByString:@"\\n"]];

    //把异常崩溃信息发送至开发者邮件
    NSMutableString *mailUrl = [NSMutableString string];
    [mailUrl appendString:@"mailto:[email protected]"];
    [mailUrl appendString:@"?subject=程序异常崩溃信息,请配合发送异常报告,谢谢合作!"];
    [mailUrl appendFormat:@"&body=%@", content];
    // 打开地址
    NSString *mailPath = [mailUrl stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    [[UIApplication sharedApplication] openURL:[NSURL URLWithString:mailPath]];
}
  • 也可以考虑存本地文件。产线版本没什么作用,拿不到用户的手机。开发和测试版本还是有用的,可以导出测试机上的日志文件,帮助分析。IOS崩溃 异常处理(NSSetUncaughtExceptionHandler)

  • 这个函数功能有限,可以考虑添加一些自定义的功能。
    IOS异常捕获 拒绝闪退 让应用从容的崩溃 UncaughtExceptionHandler

小插曲

  • 改了阿里云接口的SHA1加密函数的接口之后,我自己测试了好多次,终于没有崩溃了。

  • 让运维帮忙看日志,那些自己生成的日志也能在阿里云后台看到了。

  • 将用来测试的自己生成的日志的代码删除,一个定时器,1秒产生的log,内容就是当前的时间。1秒钟1条,很快就能达到50条,这样就能触发向阿里云发送日志的动作。

[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    XXXLog(@"测试log[NSDate date]: %@", [NSDate date]);
}];
  • 第二天,测试跟我说,今天早上的包不崩溃了,但是用我昨天打的包(测试代码还没有删除),就会崩溃。而且程序都没起来,闪一下就崩溃了。

  • 我有点不大相信,不过看了一下确实崩溃了。不过我昨天确实验证过了。我拿来了我昨天验证用的手机。不管是今天早上打的包还是昨天打的包,我的手机都不崩溃。

  • 用测试的手机连上XCode,跑得也很正常。(这个时候测试代码已经删除)。并且这次的崩溃也是Jekins打包崩溃,XCode联调不崩溃,说明不了任何问题。

  • 查看崩溃报告,通过XCode链接,或者通过iTools,都看不到崩溃日志。

  • 接着想到用NSSetUncaughtExceptionHandler函数自己抓崩溃,将崩溃信息存到手机本地,比如Document/exception.txt。不过,比较运气不好的事,崩溃的那个手机用我的MAC上的iTools看不到沙盒文件。我的那台不崩溃的手机,反而能看到沙盒文件。

  • 后来想到iTools有个实时日志输出功能,从日志中可以看到scheduledTimerWithTimeInterval函数是不存在的selector

  • 看到这里明白了,崩溃的手机没有这个API,看来是系统版本兼容性问题,我用的那台手机版本是10.3的,崩溃那两台,系统是8.x,9.x

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
  • 可以看到,这个定时器函数是10.0引入的。今天早上的版本,删除了测试代码,所以没有发生崩溃。昨天的版本,测试代码还在,所以出现了有点手机不崩溃,有的手机崩溃的奇怪现象。

  • 一开始以为不可思议的事情,找到原因之后,又那么理所当然。为了保险起见,早上加的抓崩溃日志保存在本地的代码我也删除了。因为正式版本,放一个崩溃日志在用户手机上,一点用也没有。

测试代码一定要删除,多余的代码一定要删除。

要注意系统API的版本问题,考虑系统兼容性

  • 事情到这里基本可以结束了。客户端直接调用阿里云接口直接将日志发送到阿里云并不是一个好的做法。这次的问题就是阿里云接口不稳定造成的。更好的做法是客户端先将日志发送到自己的后台,达到一定量之后,后台统一再发送阿里云备份。原因很简单:iOS客户端要发一遍,那么安卓端呢?H5端呢?

参考文章

具体的步骤,下面的参考文章已经写得很详细,照着做就好了。

全面的理解和分析IOS的崩溃日志

iOS .ips崩溃报告文件分析

iOS应用崩溃日志分析

iOS调试之 crash log分析

你可能感兴趣的:(一次阿里云日志接口导致崩溃问题的经历)