日志
在开发过程中,众所周知,日志记录调试的关键部分,尤其是当产品发布的时候,有用户feedback一些崩溃问题或者是其他问题时,日志就显得尤其重要,通过分析日志可以很快地找出问题的症结所在并快速解决问题。
恰当的记录用户日志是一门艺术。什么样的信息应该写入日志(通常包括用户行为和错误信息,分开记录),写入日志的信息太少不利于调试,而频繁地记录日志则会影响系统的性能,还会使得日志文件迅速膨胀导致难以查找到需要的信息。对于不同的应用,应该记录的信息是不用的,不过还是有一些通用的规则的。关于日志引擎,有以下几点需要注意:
1、在开发环境中,应该将日志写入控制台;而在生产环境中,应该将日志写入文件。在调试代码的时候,不输出到控制台就无法在XCode中看到日志。当最好的方式是同时写入控制台和日志文件。
2、应该分为多种不同的日志级别(错误、警告、信息、详细)。
3、当某个日志级别被禁用时,相应日志函数的调用开销要非常小。
4、向控制台或者文件写日志的时候,不可以阻塞调用者线程。
5、要定期删除日志文件以避免占满磁盘。
6、日志函数的调用要非常方便,通常使用支持变参的C语法,不建议使用Object-C语法。NSLog的调用凡是非常简单,这一点就值得学习。
在加入一条日志的时候,应该想一下这条日志有什么用,这条语句记录的数据是否已经在其他地方被记录过来了。对于不是肯定会被记录的内容,不要浪费计算机资源。
无需多言,错误信息肯定是要被记录进日志文件的。这里要强调的一点是,断言(NSAssert)也要记录进日志文件中而不是直接让程序崩溃(断言应该位于程序崩溃代码之前)
例子:
这里会导致断言失败,那么即使关闭断言程序依然会崩溃。所以要将代码改为下面这样:
这样就好多了,在 NSAssert 崩溃之前先记录下日志。当然,你可以重写一下 NSAssert ,如:MyNSAssert ,把日志记录代码和 NSAssert 封装在一起使用,这样更加方便,推荐使用。
关于记录敏感信息
记录日志通常会牵扯到隐私问题,要谨慎考虑哪些日志信息是不该被记录进日志的,例如用户的用户名和密码或者是信用卡号和密码等。不要忘了记录日志的目的只是为了在程序出现错误的时候很方便的重现和定位到错误位置,仅此而已。
获取日志文件
如果拿不到日志文件,那记录日志也是白搭。获取日志可以通过网络协议让用户上传日志到服务器。另外要注意一点,日志文件可能会比较大,在上传之前应该进行压缩以减少大小。考虑到用户流量情况,最好是在 WIFI 情况下静默上传日志文件。
何时上传日志
非程序崩溃情况下地日志,最好是选择在用户闲暇时间段且 WIFI 情况下上传;而崩溃情况下的日志,考虑到程序在崩溃的时候会处于奇怪且未知的状态,最好是选择在程序重启的时候(而不是崩溃期间)上传 crash 报告。在程序崩溃期间尽量什么都不要做。
项目中用到的 Bee 框架的 Log 日志写得非常不错,就直接贴出来给大家学习学习:
// // ______ ______ ______ // /\ __ \ /\ ___\ /\ ___// \ \ __< \ \ __\_ \ \ __\_ // \ \_____\ \ \_____\ \ \_____// \/_____/ \/_____/ \/_____/ // // // Copyright (c) 2013-2014, {Bee} open source community // http://www.bee-framework.com // // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // #import "Bee_Precompile.h" #import "Bee_Singleton.h" #pragma mark - typedef enum { BeeLogLevelNone = 0, BeeLogLevelInfo = 100, BeeLogLevelPerf = 100 + 1, BeeLogLevelProgress = 100 + 2, BeeLogLevelWarn = 200, BeeLogLevelError = 300 } BeeLogLevel; #pragma mark - #undef CC #define CC( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelNone format:__VA_ARGS__]; #undef INFO #define INFO( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelInfo format:__VA_ARGS__]; #undef PERF #define PERF( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelPerf format:__VA_ARGS__]; #undef WARN #define WARN( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelWarn format:__VA_ARGS__]; #undef ERROR #define ERROR( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelError format:__VA_ARGS__]; #undef PROGRESS #define PROGRESS( ... ) [[BeeLogger sharedInstance] level:BeeLogLevelProgress format:__VA_ARGS__]; #undef VAR_DUMP #define VAR_DUMP( __obj ) [[BeeLogger sharedInstance] level:BeeLogLevelNone format:[__obj description]]; #undef OBJ_DUMP #define OBJ_DUMP( __obj ) [[BeeLogger sharedInstance] level:BeeLogLevelNone format:[__obj objectToDictionary]]; #undef TODO #define TODO( desc, ... ) #pragma mark - @interface BeeBacklog : NSObject @property (nonatomic, assign) BeeLogLevel level; @property (nonatomic, retain) NSDate * time; @property (nonatomic, retain) NSString * text; @end #pragma mark - @interface BeeLogger : NSObject AS_SINGLETON( BeeLogger ); @property (nonatomic, assign) BOOL enabled; @property (nonatomic, assign) BOOL backlog; @property (nonatomic, retain) NSMutableArray * backlogs; @property (nonatomic, assign) NSUInteger indentTabs; - (void)toggle; - (void)enable; - (void)disable; - (void)indent; - (void)indent:(NSUInteger)tabs; - (void)unindent; - (void)unindent:(NSUInteger)tabs; - (void)level:(BeeLogLevel)level format:(NSString *)format, ...; - (void)level:(BeeLogLevel)level format:(NSString *)format args:(va_list)args; @end #pragma mark - #if __cplusplus extern "C" { #endif void BeeLog( NSString * format, ... ); #if __cplusplus }; #endif
// // ______ ______ ______ // /\ __ \ /\ ___\ /\ ___// \ \ __< \ \ __\_ \ \ __\_ // \ \_____\ \ \_____\ \ \_____// \/_____/ \/_____/ \/_____/ // // // Copyright (c) 2013-2014, {Bee} open source community // http://www.bee-framework.com // // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS // IN THE SOFTWARE. // #import "Bee_Log.h" #import "Bee_UnitTest.h" #import "Bee_Sandbox.h" #import "NSArray+BeeExtension.h" // ---------------------------------- // Source code // ---------------------------------- #pragma mark - #undef MAX_BACKLOG #define MAX_BACKLOG (50) #pragma mark - @implementation BeeBacklog @synthesize level = _level; @synthesize time = _time; @synthesize text = _text; - (id)init { self = [super init]; if ( self ) { self.level = BeeLogLevelNone; self.time = [NSDate date]; self.text = nil; } return self; } - (void)dealloc { self.time = nil; self.text = nil; [super dealloc]; } @end #pragma mark - @interface BeeLogger() { BOOL _enabled; BOOL _backlog; NSMutableArray * _backlogs; NSUInteger _indentTabs; } @end #pragma mark - @implementation BeeLogger DEF_SINGLETON( BeeLogger ); @synthesize enabled = _enabled; @synthesize backlog = _backlog; @synthesize backlogs = _backlogs; @synthesize indentTabs = _indentTabs; - (id)init { self = [super init]; if ( self ) { self.enabled = YES; self.backlog = YES; self.backlogs = [NSMutableArray array]; self.indentTabs = 0; } return self; } - (void)dealloc { self.backlogs = nil; [super dealloc]; } - (void)toggle { _enabled = _enabled ? NO : YES; } - (void)enable { _enabled = YES; } - (void)disable { _enabled = YES; } - (void)indent { _indentTabs += 1; } - (void)indent:(NSUInteger)tabs { _indentTabs += tabs; } - (void)unindent { if ( _indentTabs > 0 ) { _indentTabs -= 1; } } - (void)unindent:(NSUInteger)tabs { if ( _indentTabs < tabs ) { _indentTabs = 0; } else { _indentTabs -= tabs; } } - (void)level:(BeeLogLevel)level format:(NSString *)format, ... { #if (__ON__ == __BEE_LOG__) if ( nil == format || NO == [format isKindOfClass:[NSString class]] ) return; va_list args; va_start( args, format ); [self level:level format:format args:args]; va_end( args ); #endif // #if (__ON__ == __BEE_LOG__) } - (void)level:(BeeLogLevel)level format:(NSString *)format args:(va_list)args { #if (__ON__ == __BEE_LOG__) if ( NO == _enabled ) return; // formatting NSString * prefix = nil; if ( BeeLogLevelInfo == level ) { prefix = @"INFO"; } else if ( BeeLogLevelPerf == level ) { prefix = @"PERF"; } else if ( BeeLogLevelWarn == level ) { prefix = @"WARN"; } else if ( BeeLogLevelError == level ) { prefix = @"ERROR"; } if ( prefix ) { prefix = [NSString stringWithFormat:@"[%@]", prefix]; prefix = [prefix stringByPaddingToLength:8 withString:@" " startingAtIndex:0]; } NSMutableString * tabs = nil; NSMutableString * text = nil; if ( _indentTabs > 0 ) { tabs = [NSMutableString string]; for ( int i = 0; i < _indentTabs; ++i ) { [tabs appendString:@"\t"]; } } text = [NSMutableString string]; if ( prefix && prefix.length ) { [text appendString:prefix]; } if ( tabs && tabs.length ) { [text appendString:tabs]; } if ( BeeLogLevelProgress == level ) { NSString * name = [format stringByPaddingToLength:32 withString:@" " startingAtIndex:0]; NSString * state = va_arg( args, NSString * ); [text appendFormat:@"%@\t\t\t\t[%@]", name, state]; } else { NSString * content = [[[NSString alloc] initWithFormat:(NSString *)format arguments:args] autorelease]; if ( content && content.length ) { [text appendString:content]; } } if ( [text rangeOfString:@"\n"].length ) { [text replaceOccurrencesOfString:@"\n" withString:[NSString stringWithFormat:@"\n%@", tabs ? tabs : @"\t\t"] options:NSCaseInsensitiveSearch range:NSMakeRange( 0, text.length )]; } // print to console fprintf( stderr, [text UTF8String], NULL ); fprintf( stderr, "\n", NULL ); // back log if ( _backlog ) { BeeBacklog * log = [[[BeeBacklog alloc] init] autorelease]; log.level = level; log.text = text; [_backlogs pushTail:log]; [_backlogs keepTail:MAX_BACKLOG]; } #endif // #if (__ON__ == __BEE_LOG__) } @end extern "C" void BeeLog( NSString * format, ... ) { #if (__ON__ == __BEE_LOG__) if ( nil == format || NO == [format isKindOfClass:[NSString class]] ) return; va_list args; va_start( args, format ); [[BeeLogger sharedInstance] level:BeeLogLevelInfo format:format args:args]; va_end( args ); #endif // #if (__ON__ == __BEE_LOG__) } // ---------------------------------- // Unit test // ---------------------------------- #if defined(__BEE_UNITTEST__) && __BEE_UNITTEST__ TEST_CASE( BeeLog ) { TIMES( 3 ) { HERE( "output log", { CC( nil ); CC( @"" ); CC( @"format %@", @"" ); }); HERE( "test info", { INFO( nil ); INFO( nil, nil ); INFO( nil, @"" ); INFO( nil, @"format %@", @"" ); INFO( @"a", nil ); INFO( @"a", @"" ); INFO( @"a", @"format %@", @"" ); }); HERE( "test warn", { WARN( nil ); WARN( nil, nil ); WARN( nil, @"" ); WARN( nil, @"format %@", @"" ); WARN( @"a", nil ); WARN( @"a", @"" ); WARN( @"a", @"format %@", @"" ); }); HERE( "test error", { ERROR( nil ); ERROR( nil, nil ); ERROR( nil, @"" ); ERROR( nil, @"format %@", @"" ); ERROR( @"a", nil ); ERROR( @"a", @"" ); ERROR( @"a", @"format %@", @"" ); }); } } TEST_CASE_END #endif // #if defined(__BEE_UNITTEST__) && __BEE_UNITTEST__
有点可惜的是,Bee 框架的 log 日志并没有直接写入沙盒,我们自己添加一下就好了。强调一点,记得定期清除没用或者已过期的日志文件(可以选择选择先上传到服务器后删掉沙盒中的日志),这点很重要。
日志分析
为了即时拿到用户的崩溃日志并且记录用户的行为,第三方统计会是一个不错的选择。这里就以友盟统计为例:
由上面图片可以看到,日志记录的崩溃信息很是详细,包括错误摘要,版本信息、错误次数和发生时间,最重要的是有记录下 crash 时的堆栈信息,这样找错误就方便很多了。
对于一些比较难懂的错误摘要,如:Application received signal SIGSEGV ,就要用到发生该错误的版本代码和 .DYSM 文件符号化定位到发生错误的位置,具体方法就不赘述了,可以看一下这里,或者是自己Google一下关键词。