开发iOS应用,解决Crash问题始终是一个难题。Crash分为两种,一种是由EXC_BAD_ACCESS引起的,原因是访问了不属于本进程的内存地址,有可能是访问已被释放的内存;另一种是未被捕获的Objective-C异常(N***ception),导致程序向自身发送了SIGABRT信号而崩溃。其实对于未捕获的Objective-C异常,我们是有办法将它记录下来的,如果日志记录得当,能够解决绝大部分崩溃的问题。这里对于UI线程与后台线程分别说明。
先看UI线程。iOS SDK提供了NSSetUncaughtExceptionHandler函数,用法如:
NSSetUncaughtExceptionHandler( handleRootException );
这样在UI线程发生未捕获异常后,进程崩溃之前,handleRootException会被执行。这个函数实现如下
static void handleRootException( N***ception* 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 ]
0 CoreFoundation
0x340c88d7 __exceptionPreprocess +
1861 libobjc.A.dylib
0x343181e5 objc_exception_throw +
322 CoreFoundation
0x340c87b9 +[N***ception raise:format:] +
03 CoreFoundation
0x340c87db +[N***ception raise:format:] +
344 Foundation
0x35a12493 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] +
9985 Foundation
0x35a3afb5 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:] +
1086 MyiOSapplication
0x0022b7e9 -[CinThreadRunLoopActionManager(Protected) performAction:] +
14413 UIKit
0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] +
14414 UIKit
0x374b38c1 -[UINavigationController viewWillAppear:] +
28815 UIKit
0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] +
14416 UIKit
0x3750e61b -[UIViewController beginAppearanceTransition:animated:] +
19017 UIKit
0x3750b415 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] +
18418 UIKit
0x3750b357 -[UITabBarController transitionFromViewController:toViewController:] +
3019 UIKit
0x3750ac91 -[UITabBarController _setSelectedViewController:] +
30020 UIKit
0x3750a9c5 -[UITabBarController setSelectedIndex:] +
24021 MyiOSapplication
0x0007ef1d +[Utility ResetCurrentTabIndex] +
17222 MyiOSapplication
0x001a87bd -[UIViewController(statusBar) dismissModalViewControllerAnimatedEx:] +
41623 MyiOSapplication
0x001793fb -[ImageProcessingViewController save:] +
69024 CoreFoundation
0x34022435 -[NSObject performSelector:withObject:withObject:] +
5225 UIKit
0x3748c9eb -[UIApplication sendAction:to:from:forEvent:] +
6226 UIKit
0x3748c9a7 -[UIApplication sendAction:toTarget:fromSender:forEvent:] +
3027 UIKit
0x3748c985 -[UIControl sendAction:to:forEvent:] +
4428 UIKit
0x3748c6f5 -[UIControl(Internal) _sendActionsForEvents:withEvent:] +
49229 UIKit
0x3748d02d -[UIControl touchesEnded:withEvent:] +
47630 UIKit
0x3748b50f -[UIWindow _sendTouchesForEvent:] +
31831 UIKit
0x3748af01 -[UIWindow sendEvent:] +
38032 UIKit
0x374714ed -[UIApplication sendEvent:] +
35633 UIKit
0x37470d2d _UIApplicationHandleEvent +
580834 GraphicsServices
0x308a3df3 PurpleEventCallback +
88235 CoreFoundation
0x3409c553 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ +
3836 CoreFoundation
0x3409c4f5 __CFRunLoopDoSource1 +
14037 CoreFoundation
0x3409b343 __CFRunLoopRun +
137038 CoreFoundation
0x3401e4dd CFRunLoopRunSpecific +
30039 CoreFoundation
0x3401e3a5 CFRunLoopRunInMode +
10440 GraphicsServices
0x308a2fcd GSEventRunModal +
15641 UIKit
0x3749f743 UIApplicationMain +
109042 MyiOSapplication
0x000d4ccb main +
17443 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 ( N***ception *exception ) {
[ self handleRootException: exception ];
// 一旦在线程根上捕捉到未知异常,记录异常后本线程退出
}
@finally {
[ _dummyTimer invalidate ];
[ _dummyTimer release ];
[ pool release ];
}
}
后台线程的handleRootException与UI线程基本一致。不过为了测试人员更加方便,其实只要不是UI线程发生未捕获异常,都可以先引导用户发送日志,再把进程崩溃掉。
阅读(1332) | 评论(0) | 转发(0) |