如何讓iOS應(yīng)用從容地崩潰
雖然大家都不愿意看到程序崩潰,但可能崩潰是每個應(yīng)用必須面對的現(xiàn)實,既然崩潰已經(jīng)發(fā)生,無法阻擋了,那我們就讓它崩也崩得淡定點吧。
iOS SDK中提供了一個現(xiàn)成的函數(shù) NSSetUncaughtExceptionHandler 用來做異常處理,但功能非常有限,而引起崩潰的大多數(shù)原因如:內(nèi)存訪問錯誤,重復(fù)釋放等錯誤就無能為力了,因為這種錯誤它拋出的是Signal,所以必須 要專門做Signal處理。首先定義一個UncaughtExceptionHandler類,.h頭文件的代碼如下:
- #import <UIKit/UIKit.h>
- @interface UncaughtExceptionHandler : NSObject
- {
- BOOL dismissed;
- }
- @end
- void InstallUncaughtExceptionHandler();
- 然后在.mm文件實現(xiàn)InstallUncaughtExceptionHandler(),如下:
- void InstallUncaughtExceptionHandler()
- {
- signal(SIGABRT, MySignalHandler);
- signal(SIGILL, MySignalHandler);
- signal(SIGSEGV, MySignalHandler);
- signal(SIGFPE, MySignalHandler);
- signal(SIGBUS, MySignalHandler);
- signal(SIGPIPE, MySignalHandler);
- }
這樣,當應(yīng)用發(fā)生錯誤而產(chǎn)生上述Signal后,就將會進入我們自定義的回調(diào)函數(shù)MySignalHandler。為了得到崩潰時的現(xiàn)場信息,還可以加入一些獲取CallTrace及設(shè)備信息的代碼,.mm文件的完整代碼如下:
- #import "UncaughtExceptionHandler.h"
- #include <libkern/OSAtomic.h>
- #include <execinfo.h>
- NSString * const UncaughtExceptionHandlerSignalExceptionName = @"UncaughtExceptionHandlerSignalExceptionName";
- NSString * const UncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
- NSString * const UncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey";
- volatile int32_t UncaughtExceptionCount = 0;
- const int32_t UncaughtExceptionMaximum = 10;
- const NSInteger UncaughtExceptionHandlerSkipAddressCount = 4;
- const NSInteger UncaughtExceptionHandlerReportAddressCount = 5;
- @implementation UncaughtExceptionHandler
- + (NSArray *)backtrace
- {
- void* callstack[128];
- int frames = backtrace(callstack, 128);
- char **strs = backtrace_symbols(callstack, frames);
- int i;
- NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
- for (
- i = UncaughtExceptionHandlerSkipAddressCount;
- i < UncaughtExceptionHandlerSkipAddressCount +
- UncaughtExceptionHandlerReportAddressCount;
- i++)
- {
- [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
- }
- free(strs);
- return backtrace;
- }
- - (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex
- {
- if (anIndex == 0)
- {
- dismissed = YES;
- }
- }
- - (void)handleException:(NSException *)exception
- {
- UIAlertView *alert =
- [[[UIAlertView alloc]
- initWithTitle:NSLocalizedString(@"Unhandled exception", nil)
- message:[NSString stringWithFormat:NSLocalizedString(
- @"You can try to continue but the application may be unstable.\n"
- @"%@\n%@", nil),
- [exception reason],
- [[exception userInfo] objectForKey:UncaughtExceptionHandlerAddressesKey]]
- delegate:self
- cancelButtonTitle:NSLocalizedString(@"Quit", nil)
- otherButtonTitles:NSLocalizedString(@"Continue", nil), nil]
- autorelease];
- [alert show];
- CFRunLoopRef runLoop = CFRunLoopGetCurrent();
- CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
- while (!dismissed)
- {
- for (NSString *mode in (NSArray *)allModes)
- {
- CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
- }
- }
- CFRelease(allModes);
- NSSetUncaughtExceptionHandler(NULL);
- signal(SIGABRT, SIG_DFL);
- signal(SIGILL, SIG_DFL);
- signal(SIGSEGV, SIG_DFL);
- signal(SIGFPE, SIG_DFL);
- signal(SIGBUS, SIG_DFL);
- signal(SIGPIPE, SIG_DFL);
- if ([[exception name] isEqual:UncaughtExceptionHandlerSignalExceptionName])
- {
- kill(getpid(), [[[exception userInfo] objectForKey:UncaughtExceptionHandlerSignalKey] intValue]);
- }
- else
- {
- [exception raise];
- }
- }
- @end
- NSString* getAppInfo()
- {
- NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\nUDID : %@\n",
- [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
- [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
- [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
- [UIDevice currentDevice].model,
- [UIDevice currentDevice].systemName,
- [UIDevice currentDevice].systemVersion,
- [UIDevice currentDevice].uniqueIdentifier];
- NSLog(@"Crash!!!! %@", appInfo);
- return appInfo;
- }
- void MySignalHandler(int signal)
- {
- int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
- if (exceptionCount > UncaughtExceptionMaximum)
- {
- return;
- }
- NSMutableDictionary *userInfo =
- [NSMutableDictionary
- dictionaryWithObject:[NSNumber numberWithInt:signal]
- forKey:UncaughtExceptionHandlerSignalKey];
- NSArray *callStack = [UncaughtExceptionHandler backtrace];
- [userInfo
- setObject:callStack
- forKey:UncaughtExceptionHandlerAddressesKey];
- [[[[UncaughtExceptionHandler alloc] init] autorelease]
- performSelectorOnMainThread:@selector(handleException:)
- withObject:
- [NSException
- exceptionWithName:UncaughtExceptionHandlerSignalExceptionName
- reason:
- [NSString stringWithFormat:
- NSLocalizedString(@"Signal %d was raised.\n"
- @"%@", nil),
- signal, getAppInfo()]
- userInfo:
- [NSDictionary
- dictionaryWithObject:[NSNumber numberWithInt:signal]
- forKey:UncaughtExceptionHandlerSignalKey]]
- waitUntilDone:YES];
- }
- void InstallUncaughtExceptionHandler()
- {
- signal(SIGABRT, MySignalHandler);
- signal(SIGILL, MySignalHandler);
- signal(SIGSEGV, MySignalHandler);
- signal(SIGFPE, MySignalHandler);
- signal(SIGBUS, MySignalHandler);
- signal(SIGPIPE, MySignalHandler);
- }
在應(yīng)用自身的 didFinishLaunchingWithOptions 前,加入一個函數(shù):
- - (void)installUncaughtExceptionHandler
- {
- InstallUncaughtExceptionHandler();
- }
最后,在 didFinishLaunchingWithOptions 中加入這一句代碼就行了:
- [self InstallUncaughtExceptionHandler];
現(xiàn)在,基本上所有崩潰都能Hold住了。崩潰時將會顯示出如下的對話框:
這樣在崩潰時還能從容地彈出對話框,比起閃退來,用戶也不會覺得那么不爽。然后在下次啟動時還可以通過郵件來發(fā)送Crash文件到郵箱,這就看各個應(yīng)用的需求了。