2013年7月5日 星期五

讓 iOS App 優雅的 Crash 吧!


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

沒有留言: