開發 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 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);
}
|
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