上一篇見iOS常見Crash案例總結
iOS Crash捕獲
iOS端的crash分為兩類,一類是NSException異常,另外一類是Signal信號異常。這兩類異常我們都可以通過注冊相關函數來捕獲。
Crash種類
1、對于unrecognized selector sent to instance找不到方法iOS系統拋出異常崩潰,給NSObject添加一個分類,實現消息轉發的幾個方法
當無實現的方法時消息轉發流程.jpeg
利用runtime消息轉發處理未實現的方法crash,代碼如下:
#import <objc/runtime.h>
static NSString *_errorFunctionName;
void dynamicMethodIMP(id self,SEL _cmd) {}
static inline void change_method(Class _originalClass ,SEL _originalSel,Class _newClass ,SEL _newSel) {
Method methodOriginal = class_getInstanceMethod(_originalClass, _originalSel);
Method methodNew = class_getInstanceMethod(_newClass, _newSel);
method_exchangeImplementations(methodOriginal, methodNew);
}
@implementation NSObject(UnRecognized)
+ (void)load {
change_method([self class], @selector(methodSignatureForSelector:), [self class], @selector(ws_methodSignatureForSelector:));
change_method([self class], @selector(forwardInvocation:), [self class], @selector(ws_forwardInvocation:));
}
- (NSMethodSignature *)ws_methodSignatureForSelector:(SEL)aSelector {
if (![self respondsToSelector:aSelector]) {
_errorFunctionName = NSStringFromSelector(aSelector);
NSMethodSignature *methodSignature = [self ws_methodSignatureForSelector:aSelector];
if (class_addMethod([self class], aSelector, (IMP)dynamicMethodIMP, method_getTypeEncoding(class_getInstanceMethod([self class], @selector(methodSignatureForSelector:))))) {
NSLog(@"添加臨時方法成功!");
}
if (!methodSignature) {
methodSignature = [self ws_methodSignatureForSelector:aSelector];
}
return methodSignature;
}else{
return [self ws_methodSignatureForSelector:aSelector];
}
}
- (void)ws_forwardInvocation:(NSInvocation *)anInvocation{
SEL selector = [anInvocation selector];
if ([self respondsToSelector:selector]) {
[anInvocation invokeWithTarget:self];
}else{
[self ws_forwardInvocation:anInvocation];
}
}
@end
2、對于signal產生的crash,創建一個crash捕獲的單例,里面進行信號捕捉處理,只需要在AppDelegate中進行初始化
#import "WSCrashHandler.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
#include <sys/signal.h>
NSString * const kSignalExceptionName = @"kSignalExceptionName";
NSString * const kSignalKey = @"kSignalKey";
NSString * const kCaughtExceptionStackInfoKey = @"kCaughtExceptionStackInfoKey";
static void HandleException(NSException *exception);
static void SignalHandler(int signal);
@interface WSCrashHandler ()
@property (nonatomic, assign) BOOL dismissed;
@end
@implementation WSCrashHandler
+ (instancetype)shared {
static WSCrashHandler *sharedHelper = nil;
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
sharedHelper = [[WSCrashHandler alloc] init];
});
return sharedHelper;
}
- (instancetype)init {
self = [super init];
if (self) {
// 1.捕獲一些異常導致的崩潰
NSSetUncaughtExceptionHandler(&HandleException);
// 2.捕獲非異常情況,通過signal傳遞出來的崩潰
//signal是一個函數,有2個參數,第一個是int類型,第二個參數是一個函數指針
//添加想要監聽的signal類型,當發出相應類型的signal時,會回調SignalHandler方法
signal(SIGABRT, SignalHandler);// SIGABRT--程序中止命令中止信號
signal(SIGILL, SignalHandler);//SIGILL--程序非法指令信號
signal(SIGSEGV, SignalHandler);//SIGSEGV--程序無效內存中止信號
signal(SIGFPE, SignalHandler);//SIGFPE--程序浮點異常信號
signal(SIGBUS, SignalHandler);//SIGBUS--程序內存字節未對齊中止信號
signal(SIGPIPE, SignalHandler);//SIGPIPE--程序Socket發送失敗中止信號
}
return self;
}
//NSException異常是OC代碼導致的crash
void HandleException(NSException *exception) {
NSString *message = [NSString stringWithFormat:@"崩潰原因如下:\n%@\n%@",
[exception reason],
[[exception userInfo] objectForKey:kCaughtExceptionStackInfoKey]];
NSLog(@"%@",message);
// 獲取NSException異常的堆棧信息
NSArray *callStack = [exception callStackSymbols];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:callStack forKey:kCaughtExceptionStackInfoKey];
WSCrashHandler *crashObject = [WSCrashHandler shared];
NSException *customException = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
//signal信號拋出的異常處理
void SignalHandler(int signal) {
NSArray *callStack = [WSCrashHandler backtrace];
NSLog(@"signal信號捕獲崩潰,堆棧信息:%@",callStack);
WSCrashHandler *crashObject = [WSCrashHandler shared];
NSException *customException = [NSException exceptionWithName:kSignalExceptionName
reason:[NSString stringWithFormat:NSLocalizedString(@"Signal %d was raised.", nil),signal]
userInfo:@{kSignalKey:[NSNumber numberWithInt:signal]}];
[crashObject performSelectorOnMainThread:@selector(handleException:) withObject:customException waitUntilDone:YES];
}
- (void)handleException:(NSException *)exception {
// 打印或彈出框
// TODO :
#ifdef DEBUG
NSString *message = [NSString stringWithFormat:@"抱歉,APP發生了異常,點擊屏幕繼續并自動復制錯誤信息到剪切板。\n\n異常報告:\n異常名稱:%@\n異常原因:%@\n", [exception name], [exception reason]];
NSLog(@"%@",message);
[self showCrashToastWithMessage:message];
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!self.dismissed) {
for (NSString *mode in (__bridge NSArray *)allModes) {
//為阻止線程退出,使用 CFRunLoopRunInMode(model, 0.001, false)等待系統消息,false表示RunLoop沒有超時時間
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
#endif
// 本地保存exception異常信息并上傳服務器
// TODO :
// 下面等同于清空之前設置的
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:kSignalExceptionName]) {
kill(getpid(), [[[exception userInfo] objectForKey:kSignalKey] intValue]);
} else {
[exception raise];
}
}
//該函數用來獲取當前線程調用堆棧的信息,并且轉化為字符串數組。
+ (NSArray *)backtrace {
void *callStack[128];//堆棧方法數組
int frames = backtrace(callStack, 128);//獲取錯誤堆棧方法指針數組,返回數目
char **strs = backtrace_symbols(callStack, frames);//符號化
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames]; //函數調用信息依照順序存在NSMutableArray backtrace
for (int i = 0; i < frames; i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
- (void)showCrashToastWithMessage:(NSString *)message {
UILabel *crashLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 64, [UIApplication sharedApplication].keyWindow.bounds.size.width, [UIApplication sharedApplication].keyWindow.bounds.size.height - 64)];
crashLabel.textColor = [UIColor redColor];
crashLabel.font = [UIFont systemFontOfSize:15];
crashLabel.text = message;
crashLabel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.7];
crashLabel.numberOfLines = 0;
[[UIApplication sharedApplication].keyWindow addSubview:crashLabel];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(crashToastTapAction:)];
crashLabel.userInteractionEnabled = YES;
[crashLabel addGestureRecognizer:tap];
}
- (void)crashToastTapAction:(UITapGestureRecognizer *)tap {
UILabel *crashLabel = (UILabel *)tap.view;
[UIPasteboard generalPasteboard].string = crashLabel.text;
self.dismissed = YES;
}
@end
3、實例測試:
關鍵點:SignalHandler不要在debug環境下測試。因為系統的debug會優先去攔截。先在run的切換Release狀態,運行安裝后,再手動點擊啟動app去測試。
搞成relese模式.png
實例測試代碼如下:
#import "ViewController.h"
@interface ViewController ()
@end
typedef struct Test {
int a;
int b;
}Test;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor whiteColor];
// Do any additional setup after loading the view.
UIButton *crashExcButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 44, self.view.bounds.size.width, 50)];
crashExcButton.backgroundColor = [UIColor redColor];
[crashExcButton setTitle:@"Exception" forState:UIControlStateNormal];
[crashExcButton addTarget:self action:@selector(crashExcClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:crashExcButton];
UIButton *crashSignalEGVButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 60+44, self.view.bounds.size.width, 50)];
crashSignalEGVButton.backgroundColor = [UIColor redColor];
[crashSignalEGVButton setTitle:@"Signal(EGV)" forState:UIControlStateNormal];
[crashSignalEGVButton addTarget:self action:@selector(crashSignalEGVClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:crashSignalEGVButton];
UIButton *crashSignalBRTButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 120+44, self.view.bounds.size.width, 50)];
crashSignalBRTButton.backgroundColor = [UIColor redColor];
[crashSignalBRTButton setTitle:@"Signal(ABRT)" forState:UIControlStateNormal];
[crashSignalBRTButton addTarget:self action:@selector(crashSignalBRTClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:crashSignalBRTButton];
UIButton *crashSignalBUSButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 180+44, self.view.bounds.size.width, 50)];
crashSignalBUSButton.backgroundColor = [UIColor redColor];
[crashSignalBUSButton setTitle:@"Signal(BUS)" forState:UIControlStateNormal];
[crashSignalBUSButton addTarget:self action:@selector(crashSignalBUSClick) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:crashSignalBUSButton];
}
- (void)crashSignalEGVClick {
UIView *view = [[UIView alloc] init];
[view performSelector:NSSelectorFromString(@"release")];//導致SIGSEGV的錯誤,一般會導致進程流產
view.backgroundColor = [UIColor whiteColor];
}
- (void)crashSignalBRTClick {
Test *pTest = {1,2};
free(pTest);//導致SIGABRT的錯誤,因為內存中根本就沒有這個空間,哪來的free,就在棧中的對象而已
pTest->a = 5;
}
- (void)crashSignalBUSClick {
//SIGBUS,內存地址未對齊
//EXC_BAD_ACCESS(code=1,address=0x1000dba58)
char *s = "hello world";
*s = 'H';
}
- (void)crashExcClick {
[self performSelector:@selector(aaaa)];
}
@end