開發 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



沒有留言:
張貼留言