如果你去4S店修車,給小工說你的車哪天怎么樣怎么樣了,小工有可能會立即搬出一台電腦,插上行車電腦把日志打出來,然后告訴你你的車發生過什么故障。汽車尚且如此,何況移動互聯網應用呢。
本文第一篇:經營你的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 ]
0 CoreFoundation 0x340c88d7 __exceptionPreprocess + 186
1 libobjc.A.dylib 0x343181e5 objc_exception_throw + 32
2 CoreFoundation 0x340c87b9 +[NSException raise:format:] + 0
3 CoreFoundation 0x340c87db +[NSException raise:format:] + 34
4 Foundation 0x35a12493 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:modes:] + 998
5 Foundation 0x35a3afb5 -[NSObject(NSThreadPerformAdditions) performSelector:onThread:withObject:waitUntilDone:] + 108
6 MyiOSapplication 0x0022b7e9 -[CinThreadRunLoopActionManager(Protected) performAction:] + 144
13 UIKit 0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] + 144
14 UIKit 0x374b38c1 -[UINavigationController viewWillAppear:] + 288
15 UIKit 0x374b36b5 -[UIViewController _setViewAppearState:isAnimating:] + 144
16 UIKit 0x3750e61b -[UIViewController beginAppearanceTransition:animated:] + 190
17 UIKit 0x3750b415 -[UITabBarController transitionFromViewController:toViewController:transition:shouldSetSelected:] + 184
18 UIKit 0x3750b357 -[UITabBarController transitionFromViewController:toViewController:] + 30
19 UIKit 0x3750ac91 -[UITabBarController _setSelectedViewController:] + 300
20 UIKit 0x3750a9c5 -[UITabBarController setSelectedIndex:] + 240
21 MyiOSapplication 0x0007ef1d +[Utility ResetCurrentTabIndex] + 172
22 MyiOSapplication 0x001a87bd -[UIViewController(statusBar) dismissModalViewControllerAnimatedEx:] + 416
23 MyiOSapplication 0x001793fb -[ImageProcessingViewController save:] + 690
24 CoreFoundation 0x34022435 -[NSObject performSelector:withObject:withObject:] + 52
25 UIKit 0x3748c9eb -[UIApplication sendAction:to:from:forEvent:] + 62
26 UIKit 0x3748c9a7 -[UIApplication sendAction:toTarget:fromSender:forEvent:] + 30
27 UIKit 0x3748c985 -[UIControl sendAction:to:forEvent:] + 44
28 UIKit 0x3748c6f5 -[UIControl(Internal) _sendActionsForEvents:withEvent:] + 492
29 UIKit 0x3748d02d -[UIControl touchesEnded:withEvent:] + 476
30 UIKit 0x3748b50f -[UIWindow _sendTouchesForEvent:] + 318
31 UIKit 0x3748af01 -[UIWindow sendEvent:] + 380
32 UIKit 0x374714ed -[UIApplication sendEvent:] + 356
33 UIKit 0x37470d2d _UIApplicationHandleEvent + 5808
34 GraphicsServices 0x308a3df3 PurpleEventCallback + 882
35 CoreFoundation 0x3409c553 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 38
36 CoreFoundation 0x3409c4f5 __CFRunLoopDoSource1 + 140
37 CoreFoundation 0x3409b343 __CFRunLoopRun + 1370
38 CoreFoundation 0x3401e4dd CFRunLoopRunSpecific + 300
39 CoreFoundation 0x3401e3a5 CFRunLoopRunInMode + 104
40 GraphicsServices 0x308a2fcd GSEventRunModal + 156
41 UIKit 0x3749f743 UIApplicationMain + 1090
42 MyiOSapplication 0x000d4ccb main + 174
43 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線程發生未捕獲異常,都可以先引導用戶發送日志,再把進程崩潰掉。