開發 iOS App 最害怕的一件事就屬上架審核了!
不僅要讓 Apple 恣意的玩弄 App 所有的功能以外,更令人尷尬的事情是 - Bug 總會在這時候爭先恐後的冒出來,造成 crash reject(註1:教育訓練定理)。
如果剛好這些 Bug 們生性害羞靦腆,躲過了審查而順利的上架了,這時候卻換成使用者三不五時反應說程式一直當機,一直閃退(註2:使用者天生是讓系統崩潰的天才),但是你卻想破頭也不知道原因是什麼,這時候如果有個能將當下的錯誤訊息回報給你,至少有個方向可以去追查。
註1:教育訓練定理。教育訓練等正式場合,系統的 Bug 就有如脫韁的野馬一般,總在令人意想不到的情形下大肆奔放。
註2:使用者天生是讓系統崩潰的天才。不解釋......
今天要介紹的是,如何讓 App 崩潰的時候,能夠崩的優雅,崩的面不改色,崩的理所當然。
首先要了解的是,一般在 iOS 中系統遇到了 Exception 時,我們可以用 try...catch 來捕捉這些例外情況,但是會造成 App 直接 crash 的狀況時,不論再怎麼包 try...catch 就是無法將錯誤例外抓出來,這是因為系統對於這一類的錯誤不是拋出 Exception 訊息,而是 Signal 訊號。
所以如果想要捕捉這些錯誤例外,就必須自己手動捕捉。
UncaughtExceptionHandler.h
#import <Foundation/Foundation.h> #import <MessageUI/MFMailComposeViewController.h> @interface UncaughtExceptionHandler : NSObject<MFMailComposeViewControllerDelegate> { BOOL dismissed; } @end void HandleException(NSException *exception); void SignalHandler(int signal); void InstallUncaughtExceptionHandler(void);
UncaughtExceptionHandler.m
#import "UncaughtExceptionHandler.h" #include <libkern/OSAtomic.h> #include <execinfo.h> #import <MessageUI/MFMailComposeViewController.h> #import "AppDelegate.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); NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; for(int 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; } else if(anIndex == 1) { NSBundle *bundle = [NSBundle mainBundle]; NSDictionary *info = [bundle infoDictionary]; NSString *productName = [info objectForKey:@"CFBundleDisplayName"]; // 開啟寄信的 view MFMailComposeViewController *mailView = [[MFMailComposeViewController alloc] init]; mailView.mailComposeDelegate = self; [mailView setEditing:NO animated:YES]; [mailView setEditing:NO]; [mailView setToRecipients:@[@"tericky@gmail.com"]]; [mailView setSubject:[NSString stringWithFormat:@"[%@] - 錯誤回報",productName]]; [mailView setMessageBody:anAlertView.message isHTML:NO]; if(mailView) { AppDelegate *appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate; [appDelegate.window.rootViewController presentViewController:mailView animated:YES completion:^{}]; } } } - (void)validateAndSaveCriticalApplicationData { } -(void)mailComposeController:(MFMailComposeViewController *)controller didFinishWithResult:(MFMailComposeResult)result error:(NSError *)error { if(result == MFMailComposeResultSent) { } AppDelegate *appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate; [appDelegate.window.rootViewController dismissViewControllerAnimated:YES completion:^{}]; } - (void)handleException:(NSException *)exception { [self validateAndSaveCriticalApplicationData]; UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"喔喔!程式發生了異常!" message:[NSString stringWithFormat:@"為了讓 App 可以更好,請將錯誤訊息回報給開發人員!\n\n"@"異常原因如下:\n%@\n%@",exception.reason,[exception.userInfo objectForKey:UncaughtExceptionHandlerAddressesKey]] delegate:self cancelButtonTitle:@"退出" otherButtonTitles:@"回報", nil]; [alert show]; CFRunLoopRef runLoop = CFRunLoopGetCurrent(); CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop); while(!dismissed) { for(NSString *mode in (__bridge NSArray *)allModes) { // 0.1 是 run loop 的時間,越短畫面捕捉使用者觸碰反應越靈敏,但是會讓 cpu 非常忙碌,結果就造成凍結。 CFRunLoopRunInMode((__bridge CFStringRef)mode, 0.1, 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]; exit(0); } } @end void HandleException(NSException *exception) { int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount); if(exceptionCount > UncaughtExceptionMaximum) { return; } NSArray *callStack = [UncaughtExceptionHandler backtrace]; NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:exception.userInfo]; [userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey]; [[[UncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:[NSException exceptionWithName:exception.name reason:exception.reason userInfo:userInfo] waitUntilDone:YES]; } void SignalHandler(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]; NSException *exception = [NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName reason:[NSString stringWithFormat:@"Signal %d 被觸發",signal] userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithInt:signal] forKey:UncaughtExceptionHandlerSignalKey]]; [[[UncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(handleException:) withObject:exception waitUntilDone:YES]; } void InstallUncaughtExceptionHandler(void) { NSSetUncaughtExceptionHandler(&HandleException); signal(SIGABRT, SignalHandler); signal(SIGILL, SignalHandler); signal(SIGSEGV, SignalHandler); signal(SIGFPE, SignalHandler); signal(SIGBUS, SignalHandler); signal(SIGPIPE, SignalHandler); }完成了之後,在AppDelegate.m中
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { InstallUncaughtExceptionHandler(); self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[MainViewController alloc] init]; [self.window makeKeyAndVisible]; return YES; }
這樣就完成了捕捉了,以下是測試效果
可喜可賀!可喜可賀!
範例檔下載:ElegantDeath.zip
沒有留言:
張貼留言