北京注册网站,网站什么情况要更新,计算机应用软件开发流程图,vi设计基本要素经营你的iOS应用日志#xff08;一#xff09;#xff1a;开始编写日志组件 对于那些做后端开发的工程师来说#xff0c;看LOG解Bug应该是理所当然的事#xff0c;但我接触到的移动应用开发的工程师里面#xff0c;很多人并没有这个意识#xff0c;查Bug时总是一遍一遍的…经营你的iOS应用日志一开始编写日志组件 对于那些做后端开发的工程师来说看LOG解Bug应该是理所当然的事但我接触到的移动应用开发的工程师里面很多人并没有这个意识查Bug时总是一遍一遍的试图重现试图调试特别是对一些不太容易重现的Bug经常焦头烂额。而且iOS的异常机制比较复杂Objective-C的语言驾驭也需要一定的功力做出来的应用有时候挺容易产生崩溃闪退。一遍一遍的用XCode取应用崩溃记录、解析符号通常不胜其烦有时还对着解析出来的调用栈发呆因为程序当时的内部状态常常难以看明白只能去猜测。好了先从一个自制的日志组件开始吧。我们需要一个专门的后台线程去输出日志线程根函数如下- ( void ) threadProc{ do { NSAutoreleasePool* pool [ [ NSAutoreleasePool alloc ] init ]; for ( int i 0; i 20; i ) { [ _signal lock ]; while ( [ _queue count ] 0 ) // NSMutableArray* _queue其它线程将日志加入_queue日志线程负责输出到文件和控制台 [ _signal wait ]; // NSCondition* _signal NSArray* items [ NSArray arrayWithArray: _queue ]; [ _queue removeAllObjects ]; [ _signal unlock ]; if ( [ items count ] 0 [ self checkFileCreated ] /* 检查日志文件是否已创建 */ ) [ self logToFile: items ]; // 输出到文件以及控制台 } // 每20次输出日志执行一次NSAutoreleasePool的release // 保证既不太频繁也不太滞后 [ pool release ]; } while ( YES );}再上记录日志的入口函数。注意Objective-C作为一门动态语言要以动态语言的思维去使用比如习惯去用NSDictionary而不是自己定义一个数据类。好处很多后面再说。void writeCinLog( const char* function, // 记录日志所在的函数名称 CinLogLevel level, // 日志级别Debug、Info、Warn、Error NSString* format, // 日志内容格式化字符串 ... ) // 格式化字符串的参数{ CinLoggerManager* manager instanceOfLoggerManager(); // CinLoggerManager是单件的日志管理器 if ( manager.mLogLevel level || ! format ) // 先检查当前程序设置的日志输出级别。如果这条日志不需要输出就不用做字符串格式化 return; va_list args; va_start( args, format ); NSString* str [ [ NSString alloc ] initWithFormat: format arguments: args ]; va_end( args ); NSThread* currentThread [ NSThread currentThread ]; NSString* threadName [ currentThread name ]; NSString* functionName [ NSString stringWithUTF8String: function ]; if ( ! threadName ) threadName ; if ( ! functionName ) functionName ; if ( ! str ) str ; // NSDictionary中加入所有需要记录到日志中的信息 NSDictionary* entry [ [ NSDictionary alloc ] initWithObjectsAndKeys: LogEntry, Type, str, Message, // 日志内容 [ NSDate date ], Date, // 日志生成时间 [ NSNumber numberWithUnsignedInteger: level ], Level, // 本条日志级别 threadName, ThreadName, // 本条日志所在的线程名称 functionName, FunctionName, // 本条日志所在的函数名称 nil ]; [ str release ]; [ manager appendLogEntry: entry ]; [ entry release ];}appendLogEntry实现如下- ( void ) appendLogEntry: ( NSDictionary* )entry{ [ _signal lock ]; [ _queue addObject: entry ]; [ _signal signal ]; [ _signal unlock ];}日志文件的管理也是必须考虑的。我现在日志文件的文件名形如“03月27日 09:57:25 (0).txt”其中前面是本次程序启动的时间括号内默认是0。如果同一次的运行进程输出的日志文件超过1M就创建新文件“03月27日 09:57:25 (1).txt”。这样文件不会太大也有利于在时间点上与测试报上的Bug对应起来。另外为了调用writeCinLog时能将当前所在的函数名传进来我们需要借助宏使用__FUNCTION__预定义宏在编译期将函数名转换为字符串#define FeLogDebug(format,...) writeCinLog(__FUNCTION__,CinLogLevelDebug,format,##__VA_ARGS__)#define FeLogInfo(format,...) writeCinLog(__FUNCTION__,CinLogLevelInfo,format,##__VA_ARGS__)#define FeLogWarn(format,...) writeCinLog(__FUNCTION__,CinLogLevelWarning,format,##__VA_ARGS__)#define FeLogError(format,...) writeCinLog(__FUNCTION__,CinLogLevelError,format,##__VA_ARGS__) 这样如果在didFinishLaunchingWithOptions函数中写一句日志FeLogInfo( 应用已经启动成功了 ); 输出的日志可能是这样的- 03-27 10:44:59 INFO - [UI] -[myAppDelegate application:didFinishLaunchingWithOptions:] 应用已经启动成功了 其中前面是时间INFO是日志级别UI是线程名称myAppDelegate是记录日志的类的名称application:didFinishLaunchingWithOptions:是所在的函数名称。还有其它可利用的预定义宏比如__FILE__、__LINE__能将代码文件名和行号也加入到日志中就看有没有必要了。 经营你的iOS应用日志二异常日志 言归正传。开发iOS应用解决Crash问题始终是一个难题。Crash分为两种一种是由EXC_BAD_ACCESS引起的原因是访问了不属于本进程的内存地址有可能是访问已被释放的内存另一种是未被捕获的Objective-C异常NSException导致程序向自身发送了SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常我们是有办法将它记录下来的如果日志记录得当能够解决绝大部分崩溃的问题。这里对于UI线程与后台线程分别说明。先看UI线程。iOS SDK提供了NSSetUncaughtExceptionHandler函数用法如NSSetUncaughtExceptionHandler( handleRootException ); 这样在UI线程发生未捕获异常后进程崩溃之前handleRootException会被执行。这个函数实现如下static void handleRootException( NSException* exception ){ NSString* name [ exception name ]; NSString* reason [ exception reason ]; NSArray* symbols [ exception callStackSymbols ]; // 异常发生时的调用栈 NSMutableString* strSymbols [ [ NSMutableString alloc ] init ]; // 将调用栈拼成输出日志的字符串 for ( NSString* item in symbols ) { [ strSymbols appendString: item ]; [ strSymbols appendString: \r\n ]; } // 写日志级别为ERROR writeCinLog( __FUNCTION__, CinLogLevelError, [ Uncaught Exception ]\r\nName: %, Reason: %\r\n[ Fe Symbols Start ]\r\n%[ Fe Symbols End ], name, reason, strSymbols ); [ strSymbols release ]; // 这儿必须Hold住当前线程等待日志线程将日志成功输出当前线程再继续运行 blockingFlushLogs( __FUNCTION__ ); // 写一个文件记录此时此刻发生了异常。这个挺有用的哦 NSDictionary* dict [ NSDictionary dictionaryWithObjectsAndKeys: currentCinLogFileName(), LogFile, // 当前日志文件名称 currentCinLogFileFullPath(), LogFileFullPath, // 当前日志文件全路径 [ NSDate date ], TimeStamp, // 异常发生的时刻 nil ]; NSString* path [ NSString stringWithFormat: %/Documents/, NSHomeDirectory() ]; NSString* lastExceptionLog [ NSString stringWithFormat: %LastExceptionLog.txt, path ]; [ dict writeToFile: lastExceptionLog atomically: YES ]; }而我们的日志组件必须实现blockingFlushLogs函数确保进程在日志完全写入文件后再退出。这个实现应该很简单吧。当应用下次启动时我们可以检查如果有LastExceptionLog.txt则弹窗引导测试人员将日志发过来。如果iPhone上面配置了EMail帐户可以很简单的调用MFMailComposeViewController将日志文件作为附件发送当然也可以想其它办法。记得正式发布的版本要将它条件编译去掉哦。 其中文件中的最后一条ERROR即为导致崩溃的异常而从ERROR之前的日志可以看出当前程序的运行情况。ERROR如下- 03-20 17:21:43 ERROR - [UI] -[CinUIRunLoopActionManager(Protected) handleRootException:][ Uncaught Exception ]Name: NSDestinationInvalidException, Reason: *** -[CinThreadRunLoopActionManager performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform[ Fe Symbols Start ] CoreFoundation 0x340c88d7 __exceptionPreprocess 186 libobjc.A.dylib 0x343181e5 objc_exception_throw 32 CoreFoundation 0x340c87b9 [NSException raise:format:] 0 CoreFoundation 0x340c87db [NSException raise:format:] 34 Foundation 0x35a12493 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] 998 Foundation 0x35a3afb5 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:] 108 MyiOSapplication 0x0022b7e9 -[CinThreadRunLoopActionManager(Protected) performAction:] 144 UIKit 0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] 144 UIKit 0x374b38c1 -[UINavigationController viewWillAppear:] 288 UIKit 0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] 144 UIKit 0x3750e61b -[UIViewController beginAppearanceTransition:animated:] 190 UIKit 0x3750b415 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] 184 UIKit 0x3750b357 -[UITabBarController transitionFromViewController:toViewController:] 30 UIKit 0x3750ac91 -[UITabBarController _setSelectedViewController:] 300 UIKit 0x3750a9c5 -[UITabBarController setSelectedIndex:] 240 MyiOSapplication 0x0007ef1d [Utility ResetCurrentTabIndex] 172 MyiOSapplication 0x001a87bd -[UIViewController(statusBar) dismissModalViewControllerAnimatedEx:] 416 MyiOSapplication 0x001793fb -[ImageProcessingViewController save:] 690 CoreFoundation 0x34022435 -[NSObject performSelector:withObject:withObject:] 52 UIKit 0x3748c9eb -[UIApplication sendAction:to:from:forEvent:] 62 UIKit 0x3748c9a7 -[UIApplication sendAction:toTarget:fromSender:forEvent:] 30 UIKit 0x3748c985 -[UIControl sendAction:to:forEvent:] 44 UIKit 0x3748c6f5 -[UIControl(Internal) _sendActionsForEvents:withEvent:] 492 UIKit 0x3748d02d -[UIControl touchesEnded:withEvent:] 476 UIKit 0x3748b50f -[UIWindow _sendTouchesForEvent:] 318 UIKit 0x3748af01 -[UIWindow sendEvent:] 380 UIKit 0x374714ed -[UIApplication sendEvent:] 356 UIKit 0x37470d2d _UIApplicationHandleEvent 5808 GraphicsServices 0x308a3df3 PurpleEventCallback 882 CoreFoundation 0x3409c553 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ 38 CoreFoundation 0x3409c4f5 __CFRunLoopDoSource1 140 CoreFoundation 0x3409b343 __CFRunLoopRun 1370 CoreFoundation 0x3401e4dd CFRunLoopRunSpecific 300 CoreFoundation 0x3401e3a5 CFRunLoopRunInMode 104 GraphicsServices 0x308a2fcd GSEventRunModal 156 UIKit 0x3749f743 UIApplicationMain 1090 MyiOSapplication 0x000d4ccb main 174 MyiOSapplication 0x000039c8 start 40[ Fe Symbols End ] 可以看到即使我们没有编译时生成的符号文件也能够打印出调用栈上的每个函数的名称只是没有文件名和行号。那么除了UI线程之外自己创建的后台线程呢运行NSRunLoop的后台线程的线程函数应该如下- ( void ) threadProc: ( NSString* )threadName{ NSThread* current [ NSThread currentThread ]; [ current setName: threadName ]; NSAutoreleasePool *pool [ [ NSAutoreleasePool alloc ] init ]; // 一个没有实际作用的NSTimer确保NSRunLoop不退出。不知道有没有更好的办法啊 _dummyTimer [ [ NSTimer timerWithTimeInterval: 10.0 target: self selector: selector( dummyTimerProc: ) userInfo: nil repeats: YES ] retain ]; NSRunLoop *r [ NSRunLoop currentRunLoop ]; [ r addTimer: _dummyTimer forMode: NSDefaultRunLoopMode ]; try { // 启动后台线程的NSRunLoop [ r run ]; } catch ( NSException *exception ) { [ self handleRootException: exception ]; // 一旦在线程根上捕捉到未知异常记录异常后本线程退出 } finally { [ _dummyTimer invalidate ]; [ _dummyTimer release ]; [ pool release ]; }}后台线程的handleRootException与UI线程基本一致。不过为了测试人员更加方便其实只要不是UI线程发生未捕获异常都可以先引导用户发送日志再把进程崩溃掉。转载于:https://www.cnblogs.com/codingking/archive/2013/01/17/2865061.html