讓 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
?
1
2
3
4
5
6
7
8
9
10
11
12
#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
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
#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:@[@ "[email protected]" ]];
   [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
?
1
2
3
4
5
6
7
8
9
10
11
- ( 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

你可能感兴趣的:(讓 iOS App 優雅的 Crash 吧!)