61.常见的Exception Type
1 EXC_BAD_ACCESS
在开发过程中,总是会遇到各种Exception,在此总结一些常见的Exception。
NSInvalidArgumentException
错误类型 NSInvalidArgumentException
log输出 unrecognized selector sent to instance xxxx
错误释义 给实体对象发送了不认识的消息,即对象调用方法出错
错误基本原因 Objective-C的方法调用其实是基于消息传递机制,并且是动态编译。因此在编译阶段不会进行类和方法的绑定,而是在运行时执行绑定操作。当类的方法没有实现或对象
场景1 给对象发送消息时:
[self performSelector:@selector(testException:) withObject:@"string"];
-(void)testException {
NSLog(@"%s",__func__);
}
错误分析:给对象发消息的时候,该消息是带形参的,而实现的方法却不带形参,因此产生了异常。
场景2 在集合中:
NSMutableArray *array = [NSMutableArray array];
[array addObject:nil];
错误分析:当企图给集合插入nil值的时候,实际上调用的是[__NSArrayM insertObject: atIndex: ],根据CFArray的源码推断,在该方法里面,对插入的对象进行了断言操作(仅仅是猜想,未能找到CFMutableArray的源码)。
NSRangeException
错误类型 NSRangeException
log输出 1.index 2 beyond bounds [0 … 1]
2.index x beyond bounds for …
错误释义 1.索引序号2超出0~1区间
2.索引序号超出…
常见场景
场景1 插入或获取对象:
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"one"];
[array addObject:@"two"];
[array objectAtIndex:2];
错误分析:当试图从数组中获得一个对象的时候,索引大于自身容量,因而引起该异常。同样在插入的时候,不正确的索引值也会引发这个问题。
crash文件
当运行的APP发生crash的时候,如果代码里面增加对应的handler或者有第三方的crash SDK,他们会采集相关的运行堆栈,发送到对应的服务器上,然后通过开发者上传的dsym文件进行解析,得到符号化的堆栈信息,我们可以通过分析这个知道crash的原因。另外,当发生crash的时候,相应的设备上也会生成一个crash文件。我们可以通过Xcode导出crash文件。
Incident Identifier: B9C9CA64-DB6D-4EA7-AE86-A7BD841E2A50
CrashReporter Key: 6b0e0da01ba5183a74d6fb37e0ebd2b46540e89d
Hardware Model: iPhone7,2
Process: AlipayWallet [221]
Path: /private/var/containers/Bundle/Application/8A3BF1BC-06B9-481F-8B33-5F51CCA1D764/AlipayWallet.app/AlipayWallet
Identifier: com.alipay.iphoneclient
Version: 10.1.5.102407 (10.1.5)
Code Type: ARM-64 (Native)
Role: Non UI
Parent Process: launchd [1]
Coalition: com.alipay.iphoneclient [359]
Date/Time: 2017-10-28 18:27:52.9845 +0800
Launch Time: 2017-10-28 18:27:26.7691 +0800
OS Version: iPhone OS 11.0.3 (15A432)
Baseband Version: 6.17.00
Report Version: 104
Exception Type: EXC_CRASH (SIGKILL)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Termination Reason: Namespace SPRINGBOARD, Code 0x8badf00d
Termination Description: SPRINGBOARD, process-exit watchdog transgression: com.alipay.iphoneclient exhausted real (wall clock) time allowance of 5.00 seconds | | Elapsed total CPU time (seconds): 10.020 (user 10.020, system 0.000), 100% CPU | Elapsed application CPU time (seconds): 1.194, 12% CPU |
Triggered by Thread: 0
Filtered syslog:
None found
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 CoreFoundation 0x00000001818a057c CFAllocatorAllocate + 0
1 CoreFoundation 0x00000001818a09bc _CFRuntimeCreateInstance + 348
2 CoreFoundation 0x00000001818a1610 CFBasicHashCreate + 108
3 CoreFoundation 0x0000000181961c44 __CFDictionaryCreateTransfer + 156
4 CoreFoundation 0x00000001819462b0 __CFBinaryPlistCreateObjectFiltered + 5952
5 CoreFoundation 0x0000000181945ed0 __CFBinaryPlistCreateObjectFiltered + 4960
6 CoreFoundation 0x00000001819469e4 __CFTryParseBinaryPlist + 200
7 CoreFoundation 0x00000001818a0d54 _CFPropertyListCreateWithData + 192
8 CoreFoundation 0x00000001818ec178 _CFPropertyListCreateFromXMLData + 116
9 Foundation 0x00000001822fa68c +[NSPropertyListSerialization propertyListFromData:mutabilityOption:format:errorDescription:] + 52
10 AlipayWallet 0x00000001023f2058 0x100480000 + 32972888
11 AlipayWallet 0x00000001023f0ad4 0x100480000 + 32967380
header
Incident Identifier: 事件标识符,每个crash文件对应一个唯一的标识符。
CrashReporter Key: 匿名设备标识符。
Hardware Model:设备型号;
Process: 进程名;
Identifier:app Identifier;
Exception Type: 异常类型;
Exception Codes: 异常代码;
Termination Reason:进程被结束的原因
线程堆栈信息
下面是各个线程的堆栈信息,有部分是符号化的,有的是没有符号化的,一般系统动态库相关的都已经符号化的,没有符号化的是运行APP里面的符号,我们可以通过atos或者symbolicatecrash命令逐行解析。
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0 CoreFoundation 0x00000001818a057c CFAllocatorAllocate + 0
.....
9 Foundation 0x00000001822fa68c +[NSPropertyListSerialization propertyListFromData:mutabilityOption:format:errorDescription:] + 52
10 AlipayWallet 0x00000001023f2058 0x100480000 + 32972888
11 AlipayWallet 0x00000001023f0ad4 0x100480000 + 32967380
第一行的数字表示对应线程的调用栈顺序;
第二行表示对应的镜像文件名称,如系统的foundation等等;
第三行表示当前行对应的符号地址;
第四行: 如果是已经符号化过的,则表示对应的符号,没有符号化的是镜像的起始地址+文件偏移地址,这2个值相加实际上就是第三行的地址。
Exception
从上面的分析我们可以知道在crash文件的头部包含的Exception相关信息。下面我们来看看我们常见的几种Exception类型:
当你的iOS应用崩溃的时候,我们需要去分析异常日志以定位根本原因。崩溃可能是 “低内存崩溃 Low Memory Crash” 或者 “普通异常崩溃”。当碰到“异常”时,更好的理解“不同类型的异常”能够真正帮助我们快速定位问题所在。
在这篇文章中,我们将研究 iOS 应用可能碰到的不同类型的“异常”,例如EXC_CRASH、EXC_BAD_ACCESS、EXC_RESOURCE、00000020 等。
崩溃日志中的“异常”
“异常”这个词在“崩溃日志”语境下更多与“Mach 异常”(以“EXC_为前缀”)和 “UNIX 信号”(如: SIGSEGV, SIGBUS等)相关。在某些情况下(应该是有对应的dSYM符号文件时)系统会通过映射将底层的 Mach 异常 翻译为 UNIX 信号。这就是为什么你能log中看到有用 “EXC_CRASH(SIGABRT)” 及 “EXC_BAC_ACCESS(SIGSEGV)” 作为 异常类型(Exception Type)。
对于某些异常,还会附带一个关联的 处理器定制异常码(processor-specific Exception Code) 或者 异常子类型(Exception Subtype),用以包含更多问题相关信息。举例来说, “EXC_BAC_ACCESS” 类型异常可能有一行如“KERN_INVALID_ADDRESS at 0x80000010”作为“异常码”; “EXC_RESOURCE” 可能有一行"WAKEUPS"作为"异常子类别"
UNIX 信号
iOS开发者常见的 UNIX 信号 如下:
UNIX 信号 注释
SIGSEGV 访问无效的内存地址。地址存在,但是应用程序无法访问。
SIGABRT 程序崩溃。由 C函数 abort() 初始化。通常意味着系统检测到某些事务出错,例如 assert() 或者 NSAssert() 校验失败。
SIGBUS 访问无效的内存地址。地址不存在,或对齐无效。(The address does not exist, or the alignment is invalid.)
SIGTRAP 调试器相关
SIGILL 尝试执行非法的、有缺陷、未知的或者需要权限的指令。
Mach 异常
Mach 异常 描述 注释
EXC_BAD_ACCESS 错误内存访问 访问“错误”内存地址。“错误”可能指“地址不存在”或者“应用没有权限访问”。因此通常与 SIGBUS 及 SIGSEGV 相关联。
EXC_CRASH 异常跳出 通常与 SIGABRT 相关联,意思是由于检测到代码抛出的未捕获异常而使应用程序异常退出。
EXC_BREAKPOINT 跟踪/断点捕获 通用与 SIGTRAP 相关联。可以由你自己的代码或者 NSExceptions 抛出时触发。
EXC_GUARD 违反了受保护资源的防护(Violated Guarded Resource Protection) 由违背受保护资源防护触发,例如‘某些文件描述符’。
EXC_BAD_INSTRUCTION 非法指令 通常与特定非法或未定义指令/操作数相关。
EXC_RESOURCE 资源限制 应用由于达到资源消耗限制而退出。
00000020 十六进制异常类型 非 ‘OS Kernel’ 异常。
异常
EXC_BAD_ACCESS(错误内存访问)
“EXC_BAD_ACCESS” 是APP崩溃时最常见的异常之一。不幸的是,调试起来却不容易。
一般有两种可能性:
访问某些尚未初始化的对象。(SIGBUS)
访问已经被 ARC 释放(导致地址变为不可访问)的对象。如果是这个情况,你通常可以在崩溃日志中的 “Backtrace” 顶部附近看到 objc_release。
示例如:
Exception Type: EXC_BAD_ACCESS (SIGSEGV)
Exception Codes: KERN_INVALID_ADDRESS at 0x6d783f44
…
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x00000011
“EXC_BAD_ACCESS”也有关联的“异常码”以帮助提供额外信息。举例来说,KERN_PROTECTION_FAILURE 表示内存有效,但是不允许当前形式的访问,KERN_INVALID_ADDRESS 意思是地址当前无效。
为了辅助调试 “EXC_BAD_ACCESS” 类型异常,你可以勾选 Xcode 中的 “Enable Zombie Objects” 后再尝试。
EXC_CRASH(异常跳出)
相较于 “EXC_BAD_ACCESS”,“EXC_CRASH" 更容易遇到。它通常发生在对象接收到未实现的消息时,如 Xcode 调试器中显示的 “unrecognized selector sent to instance 0x6a33840”。
UncaughtExceptionHandler.h
#import
#import
@interface UncaughtExceptionHandler : NSObject
/*!
* 异常的处理方法
*
* @param install 是否开启捕获异常
* @param showAlert 是否在发生异常时弹出alertView
*/
+ (void)installUncaughtExceptionHandler:(BOOL)install showAlert:(BOOL)showAlert;
@end
UncaughtExceptionHandler.m
#import "UncaughtExceptionHandler.h"
#include
#include
NSString * const UncaughtExceptionHandlerSignalExceptionName = @"UncaughtExceptionHandlerSignalExceptionName";
NSString * const UncaughtExceptionHandlerSignalKey = @"UncaughtExceptionHandlerSignalKey";
NSString * const UncaughtExceptionHandlerAddressesKey = @"UncaughtExceptionHandlerAddressesKey";
volatile int32_t UncaughtExceptionCount = 0;
const int32_t UncaughtExceptionMaximum = 10;
static BOOL showAlertView = nil;
void HandleException(NSException *exception);
void SignalHandler(int signal);
NSString* getAppInfo();
@interface UncaughtExceptionHandler()
@property (assign, nonatomic) BOOL dismissed;
@end
@implementation UncaughtExceptionHandler
+ (void)installUncaughtExceptionHandler:(BOOL)install showAlert:(BOOL)showAlert {
if (install && showAlert) {
[[self alloc] alertView:showAlert];
}
NSSetUncaughtExceptionHandler(install ? HandleException : NULL);
signal(SIGABRT, install ? SignalHandler : SIG_DFL);
signal(SIGILL, install ? SignalHandler : SIG_DFL);
signal(SIGSEGV, install ? SignalHandler : SIG_DFL);
signal(SIGFPE, install ? SignalHandler : SIG_DFL);
signal(SIGBUS, install ? SignalHandler : SIG_DFL);
signal(SIGPIPE, install ? SignalHandler : SIG_DFL);
}
- (void)alertView:(BOOL)show {
showAlertView = show;
}
//获取调用堆栈
+ (NSArray *)backtrace {
//指针列表
void* callstack[128];
//backtrace用来获取当前线程的调用堆栈,获取的信息存放在这里的callstack中
//128用来指定当前的buffer中可以保存多少个void*元素
//返回值是实际获取的指针个数
int frames = backtrace(callstack, 128);
//backtrace_symbols将从backtrace函数获取的信息转化为一个字符串数组
//返回一个指向字符串数组的指针
//每个字符串包含了一个相对于callstack中对应元素的可打印信息,包括函数名、偏移地址、实际返回地址
char **strs = backtrace_symbols(callstack, frames);
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
for (i = 0; i < frames; i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
return backtrace;
}
//点击退出
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
- (void)alertView:(UIAlertView *)anAlertView clickedButtonAtIndex:(NSInteger)anIndex {
#pragma clang diagnostic pop
if (anIndex == 0) {
self.dismissed = YES;
}
}
//处理报错信息
- (void)validateAndSaveCriticalApplicationData:(NSException *)exception {
NSString *exceptionInfo = [NSString stringWithFormat:@"\n--------Log Exception---------\nappInfo :\n%@\n\nexception name :%@\nexception reason :%@\nexception userInfo :%@\ncallStackSymbols :%@\n\n--------End Log Exception-----", getAppInfo(),exception.name, exception.reason, exception.userInfo ? : @"no user info", [exception callStackSymbols]];
NSLog(@"%@", exceptionInfo);
// [exceptionInfo writeToFile:[NSString stringWithFormat:@"%@/Documents/error.log",NSHomeDirectory()] atomically:YES encoding:NSUTF8StringEncoding error:nil];
}
- (void)handleException:(NSException *)exception {
[self validateAndSaveCriticalApplicationData:exception];
if (!showAlertView) {
return;
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
UIAlertView *alert =
[[UIAlertView alloc]
initWithTitle:@"出错啦"
message:[NSString stringWithFormat:@"你可以尝试继续操作,但是应用可能无法正常运行.\n"]
delegate:self
cancelButtonTitle:@"退出"
otherButtonTitles:@"继续", nil];
[alert show];
#pragma clang diagnostic pop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!self.dismissed) {
//点击继续
for (NSString *mode in (__bridge NSArray *)allModes) {
//快速切换Mode
CFRunLoopRunInMode((CFStringRef)mode, 0.001, 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];
}
}
@end
void HandleException(NSException *exception) {
int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
// 如果太多不用处理
if (exceptionCount > UncaughtExceptionMaximum) {
return;
}
//获取调用堆栈
NSArray *callStack = [exception callStackSymbols];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey];
//在主线程中,执行制定的方法, withObject是执行方法传入的参数
[[[UncaughtExceptionHandler alloc] init]
performSelectorOnMainThread:@selector(handleException:)
withObject:
[NSException exceptionWithName:[exception name]
reason:[exception reason]
userInfo:userInfo]
waitUntilDone:YES];
}
//处理signal报错
void SignalHandler(int signal) {
int32_t exceptionCount = OSAtomicIncrement32(&UncaughtExceptionCount);
// 如果太多不用处理
if (exceptionCount > UncaughtExceptionMaximum) {
return;
}
NSString* description = nil;
switch (signal) {
case SIGABRT:
description = [NSString stringWithFormat:@"Signal SIGABRT was raised!\n"];
break;
case SIGILL:
description = [NSString stringWithFormat:@"Signal SIGILL was raised!\n"];
break;
case SIGSEGV:
description = [NSString stringWithFormat:@"Signal SIGSEGV was raised!\n"];
break;
case SIGFPE:
description = [NSString stringWithFormat:@"Signal SIGFPE was raised!\n"];
break;
case SIGBUS:
description = [NSString stringWithFormat:@"Signal SIGBUS was raised!\n"];
break;
case SIGPIPE:
description = [NSString stringWithFormat:@"Signal SIGPIPE was raised!\n"];
break;
default:
description = [NSString stringWithFormat:@"Signal %d was raised!",signal];
}
NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
NSArray *callStack = [UncaughtExceptionHandler backtrace];
[userInfo setObject:callStack forKey:UncaughtExceptionHandlerAddressesKey];
[userInfo setObject:[NSNumber numberWithInt:signal] forKey:UncaughtExceptionHandlerSignalKey];
//在主线程中,执行指定的方法, withObject是执行方法传入的参数
[[[UncaughtExceptionHandler alloc] init]
performSelectorOnMainThread:@selector(handleException:)
withObject:
[NSException exceptionWithName:UncaughtExceptionHandlerSignalExceptionName
reason: description
userInfo: userInfo]
waitUntilDone:YES];
}
NSString* getAppInfo() {
NSString *appInfo = [NSString stringWithFormat:@"App : %@ %@(%@)\nDevice : %@\nOS Version : %@ %@\n",
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleDisplayName"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleShortVersionString"],
[[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"],
[UIDevice currentDevice].model,
[UIDevice currentDevice].systemName,
[UIDevice currentDevice].systemVersion];
return appInfo;
}
AppDelegate.m调用
#import "AppDelegate.h"
#import "UncaughtExceptionHandler.h"
@interface AppDelegate ()
@end
@implementation AppDelegate
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[UncaughtExceptionHandler installUncaughtExceptionHandler:YES showAlert:YES];
//用来制造一些异常
[self performSelector:@selector(string) withObject:nil afterDelay:2.0];
return YES;
}
@end