2. 捕获与报告程序崩溃
iTunes Connet 可以提供crash报告,但是他有很多限制。苹果只会向用户征求一次意见,如果得到用户容许,应用以后就可以上传crash报告了。不过多数用户会拒绝,而且每天只能上传一次报告。iTunesConnet只对App Store中得应用提供错误报告。所以,在开发和测试期间需要使用其他系统管理crash报告。总之,如果iTunes Connet能够满足你得需求,那很好,不过通常是无法满足的。
最好的iTunes Connet 替代品是Quincy Kit(http://quincykit.net)
HockeyApp(http://hockeyapp.net)中就继承了QuincyKit。可以很方便的把Quincy Kit集成到已有的工程中,得到用户容许后,就可以将报告上传到HockeyApp得服务器上,也可以上传到自己的服务器上。目前它只上传crash报告,不上传日志。
错误报告中可能会有一些符号,也可以没有,这取决于错误报告生成方式。只要保留这二进制程序的.dSYM 文件。Xcode就可以非常好的把报告符号化(用方法名取代内存地址)。Xcode通过spotlight查找这些文件,所以要确保这些文件可以被spotlight找到。如果使用了HockeyApp管理crash报告,也可以吧符号化文件上传到HockeyApp。
在XCODE编译项目之后,会在app旁看见一个同名的dSYM文件. 他是一个编译的中转文件,简单说就是debug的symbols包含在这个文件中.他有什么作用? 当release的版本 crash的时候,会有一个日志文件,包含出错的内存地址, 使用symbolicatecrash工具能够把日志和dSYM文件转换成可以阅读的log信息,也就是将内存地址,转换成程序里的函数或变量和所属于的文件名.
3. 错误和NSError
错误分为两种
a 程序错误 程序错误是在调试模式中需要处理异常,在发布模式中则需要记录错误日志。在发布模式中如果出现可能造成数据污染的程序错误,也需要抛出异常。iOS中分配小块内存失败应该是为程序错误。这几乎不可能发生。如果发生了,那么肯定是程序错误。
b 用户错误: 网络连接失败,磁盘空间不足等等
不应该抛出异常,应该返回错误信息,通常使用NSError对象来返回错误信息。NSFileManager 经常会用到NSError。
NSError* error = nil;
if(![NSFileManager defaultManager] copyItemAtPath:srcPath toPath:toPatherror:&error)]) {
[selfhandleError:error];
}
这个方法可以吧一个文件复制到另外一个地方。很明显,有很多原因可能会导致这个方法执行失败。失败时返回NO,同时更新NSError对象,进而吧错误信息返回给调用者。
代码剖析(看起来会是这个样子):
- (BOOL)copyItemAtPath:(NSString *)srcPathtoPath:(NSString *)dstPath error:(NSError **)error NS_AVAILABLE(10_5, 2_0)
{
BOOL success =---------;
if(! success)
{
if(error !=NULL){
* error= [NSError errorWithDomain:-----------]
}
}
return success;
}
error 是指向指针的指针。要对它进行检查,在对error解引用之前需要确保它不为NULL。如果调用者并不关心出错细节,就可以直接传入一个NULL值。还需要对返回值检查,确保所有操作都是成功的。
// Predefined domain for errors from most AppKit andFoundation APIs.
// AppKit和Foundation库中主要的错误域
FOUNDATION_EXPORT NSString *constNSCocoaErrorDomain;
// Other predefined domains;value of "code" will correspond to preexisting values in these domains.其他域
FOUNDATION_EXPORT NSString *const NSPOSIXErrorDomain;
FOUNDATION_EXPORT NSString *const NSOSStatusErrorDomain;
FOUNDATION_EXPORT NSString *const NSMachErrorDomain;
NSError对象中,主要有三个私有变量
错误域(NSInteger): _domain
错误标示(NSString *):_code
错误详细信息(NSDictionary *):_userInfo
通常用_domain和_code一起标识一个错误信息
对应三个方法:
- (NSString *)domain;
- (NSInteger)code;
- (NSDictionary *)userInfo;
NSError中用户信息字典可以用来存储任何信息。这个字典中有很多预定义的键。
// Keysin userInfo, for subsystems wishing to provide their error messages up-front.
FOUNDATION_EXPORT NSString *constNSLocalizedDescriptionKey; // NSString
FOUNDATION_EXPORT NSString *constNSLocalizedFailureReasonErrorKey ; // NSString
FOUNDATION_EXPORT NSString *constNSLocalizedRecoverySuggestionErrorKey; // NSString
FOUNDATION_EXPORT NSString *constNSLocalizedRecoveryOptionsErrorKey ; // NSArray of NSStrings
FOUNDATION_EXPORT NSString *constNSRecoveryAttempterErrorKey; // Instanceof a subclass of NSObject that conforms to the NSErrorRecoveryAttemptinginformal protocol
FOUNDATION_EXPORT NSString *constNSHelpAnchorErrorKey ; // NSString containing a helpanchor
// Other standard keys in userInfo, for various errorcodes
FOUNDATION_EXPORT NSString *constNSStringEncodingErrorKey ; // NSNumbercontaining NSStringEncoding
FOUNDATION_EXPORT NSString *const NSURLErrorKey ; // NSURL
FOUNDATION_EXPORT NSString *const NSFilePathErrorKey ; // NSString
一些关于键的使用:
详细描述键
NSString *const NSLocalizedDescriptionKey;
取方法
- (NSString *)localizedDescription;
失败原因键
NSString *constNSLocalizedFailureReasonErrorKey
取方法
- (NSString *)localizedFailureReason;
恢复建议键
NSString *const NSLocalizedRecoverySuggestionErrorKey;
取方法
- (NSString *)localizedRecoverySuggestion;
恢复选项键
NSString *constNSLocalizedRecoveryOptionsErrorKey
取方法
- (NSArray *)localizedRecoveryOptions;
4. 错误处理块(我没有用过)
块提供了一种非常灵活的错误处理方式。在异步操作中,传入一个错误处理快是非常有用的。也可以把错误处理块用于错误处理。
(如果要想深入了解错误相应与错误恢复,可以看看 Error Handing Programming guide)
通常,块可以很方便的用于错误处理。NSFilePresenter 协议中有这样一个方法:
这是一个使用块进行错误处理的例子。这个方法可能会做一些费时间的操作,因此立即将执行结果通知给调用者是不现实的。这个方法的实现可能类似于如下所示:
- (void)savePresentedItemChangesWithCompletionHandler: (void (^)(NSError*errorOrNil))completionHandler
{
dispatch_queue_t queue =...;
//将请求分发到后台队列然后立即返回
dispatch_async(queue, ^{
//……执行一些操作……
if (completionHandler) {
NSError *error = nil;
if(anErrorOccurred) {
error = [NSError errorWithDomain:...];
}
//在主线程中执行完整的错误处理程序
dispatch_sync(dispatch_get_main_queue(),^{
completionHandler(error);
});
}
});
}
相对于使用委托回调方法进行错误处理,这种模式对于调用者来说更加方便。调用者能够把错误处理代码跟调用代码放在一起,而不必定义类似filePresenter:didFailWithError:这样的委托方法。前面的方法可能会被这样使用:
[presentersavePresentedItemChangesWithCompletionHandler:^(NSError *e) {
if (e)
{
... respond to error ...
}
else{
... cleanup after success ...
}
};
前面的代码并没有把completionHandler存储到实例变量中,这样可以避免出现循环保留。在操作结束之前,可能会短暂地存在循环保留,操作结束之后就没有了。当操作结束并且错误处理被触发之后,循环保留就会立即被清理掉。
5. 日志 恰当的记录日志
Foundation 中有一个独立的日志函数 NSLog,非常不灵活,且效率低下。唯一优点:使用方便。这个函数将日志数输出到控制台,这对release版本而言根本没用,所以绝对不要在release版本中使用NSLog。
#ifdef DEBUG
#define MYLog NSLog
#else
#define MYLog
#endif
这段代码会把release版本中得NSLog 全部移除,但是这样一来就完全没有了日志,所以不可取。你需要一个能够同时在开发环境和生产环境中使用的日志引擎。
注意
1) 开发环境中,日志写入控制台;release版本,应该将日志写入文件。当然debug版本最好是两者都写入。
2) 应该分为多种不同的日志级别(错误,警告,信息,详细)
3) 当某个日志函数级别禁用时,相应日志函数的调用开销要非常小。(不懂)
4) 想控制台或文件写日志时,不可以阻塞调用者的线程
5) 要定期删除日志文件以避免占满磁盘空间
获取日志文件
1) 可以通过网络协议让用户上传日志
2) MFMailComposeViewController将日志以附件的形式通过电子邮件发送
日志文件可能非常大,通常在发送之前进行压缩以减小体积。可以使用Minizip对日志进行压缩。它提供多种压缩方法,比如Objective-Zip和ZipArchive。
TestFlight(http://testflightapp.com)支持把用户日志上传服务器,只需要在代码中使用TFLog()替代NSLog就可以了。Hockey 目前不支持上传日志。
要先经过用户容许才可以发送日志。因为不但牵涉隐私问题,还有可能耗费大量的网络流量和电池流量。通常只有在需要查看问题报告是才有必要发送日志。
Lumberjack log 日志 http://github.com/robbiehanson/CocoaLumberjack
苹果文档: 1.Exception programming Topics
2. Error Handling Programming guide
3. Understanding and analyzing iphone osapplication crash reports