1. Crash常规处理
1.1 异常产生
日常开发中,我们会遇到各种Crash
的出现。其中较为常见的,例如:数组越界
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic,strong) NSArray *arr;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
_arr = @[@"kc", @"hk", @"kd", @"cat"];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSString *str = [self.arr objectAtIndex:4];
NSLog(@"str:%@", str);
}
@end
-------------------------
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'
terminating with uncaught exception of type NSException
1.2 常规处理
解决方式也比较简单,创建一个NSArray
的分类,使用MethodSwizzling
将objectAtIndex
和自定义方法进行交互,在自定义方法中,增加索引判断,从而避免数组越界
#import "NSArray+Extension.h"
#import <objc/runtime.h>
@implementation NSArray (Extension)
+ (void)initialize
{
if (self == [NSArray class]) {
Method method1 = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
Method method2 = class_getInstanceMethod(self, @selector(lg_objectAtIndex:));
method_exchangeImplementations(method1, method2);
}
}
- (id)lg_objectAtIndex:(NSUInteger)index{
if(index < self.count){
return [self lg_objectAtIndex:index];
}
NSLog(@"数组越界:index = %lu", index);
return nil;
}
@end
被替换的
objectAtIndex:
方法在__NSArrayI
中在
iOS
的Foundation
框架中,类簇是一种常用的设计模式,他将一些相近的,私有的,具体的子类组合在一个实体的抽象类下面,我称这个抽象类为实体的,是因为和我们交互的接口承载者,就是这个抽象大类我们平时常用的三大类,
NSString
、NSArray
、NSDictionary
都是类簇,我们通过他们创建的对象都是其子类对象的实例化,并不是他本身的实例化
一般来说方法交换都会在load
方法中进行
因为
load
方法执行时机早,在main
函数之间就会调用,并且会主动调用所有本类和分类中的
load
方法都会调用,父类优先于子类,本类优先于分类load
方法的弊端,在objc_init
→load_images
函数中被调用,影响启动速度,让原本的懒加载类在启动时刻被迫营业
如果使用initialize
方法代替,该方法在首次接收消息时自动调用,也能达到相同的效果,并且可以保持类的懒加载特性
initialize
方法的特点:
initialize
方法属于被动调用的方法。原则上来说,会在main
函数之后触发。但也有特殊情况,当A类
在load
中调用B类
的方法,会触发B类
的initialize
方法,此时B类
的initialize
在main
函数之前执行当子类首次接收消息时:
父类、子类都有,先调用父类的,再调用子类的
子类没有父类有,执行父类的方法。原则上一个类只会调用一次
initialize
方法,但这种情况父类的initialize
会执行多次,可以通过添加if(self == [ClassName self])
来进行判断父类没有子类有,执行子类的方法
分类和本类都有,本类方法会被分类覆盖。多分类看文件的编译顺序,最后编译的分类中的方法会被执行
由于分类会覆盖本类的initialize
方法,所以在initialize
中进行方法交互会存在一定风险。所以还是推荐load
方法内部实现Method Swizzle
,initialize
方法初始化全局变量或者静态变量
2. Crash底层分析
当出现异常时,打印函数调用栈
- 在
libobjc
中,调用了objc_exception_rethrow
函数
2.1 objc_exception_rethrow
打开objc
源码,全局搜索objc_exception_rethrow
- 只能找到函数的实现,但是无法找到函数的调用
2.2 _objc_terminate
我们只能尝试搜索函数调用栈中,在objc源码中的另一个_objc_terminate
函数
进入_objc_terminate
函数
static void _objc_terminate(void)
{
if (PrintExceptions) {
_objc_inform("EXCEPTIONS: terminating");
}
if (! __cxa_current_exception_type()) {
// No current exception.
(*old_terminate)();
}
else {
// There is a current exception. Check if it's an objc exception.
@try {
__cxa_rethrow();
} @catch (id e) {
// It's an objc object. Call Foundation's handler, if any.
(*uncaught_handler)((id)e);
(*old_terminate)();
} @catch (...) {
// It's not an objc object. Continue to C++ terminate.
(*old_terminate)();
}
}
}
- 在
catch
的代码中,通过*uncaught_handler
进行函数调用
搜索uncaught_handler
的赋值
static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
static void _objc_default_uncaught_exception_handler(id exception)
{
}
- 默认赋值
_objc_default_uncaught_exception_handler
空函数
2.3 objc_setUncaughtExceptionHandler
搜索到uncaught_handler
的另一处代码,在objc_setUncaughtExceptionHandler
函数中,可以对其进行回调函数的赋值
/***********************************************************************
* objc_setUncaughtExceptionHandler
* Set a handler for uncaught Objective-C exceptions.
* Returns the previous handler.
**********************************************************************/
objc_uncaught_exception_handler
objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
{
objc_uncaught_exception_handler result = uncaught_handler;
uncaught_handler = fn;
return result;
}
objc_setUncaughtExceptionHandler
属于底层objc
函数,在Objective-C
层,对应封装的为NSSetUncaughtExceptionHandler
方法所以在
OC
中,调用NSSetUncaughtExceptionHandler
方法,并设置自定义的回调函数。当出现异常后,系统自动调用回调函数,可以在里面拦截异常
2.4 exception_init
寻址_objc_terminate
函数更直接的流程,在objc
初始化的objc_init
函数,对异常信号处理的初始化代码中,也有相关逻辑的处理
进入exception_init
函数
/***********************************************************************
* exception_init
* Initialize libobjc's exception handling system.
* Called by map_images().
**********************************************************************/
void exception_init(void)
{
old_terminate = std::set_terminate(&_objc_terminate);
}
- 将
_objc_terminate
设置为回调函数
3. Crash拦截
造成Crash
的情况多种多样,例如:
KVO
问题NSNotification
线程问题数组越界
野指针
后台任务超时
内存爆出
主线程卡顿超阀值
死锁
如果针对每一种Crash
都进行分别处理,代码会非常繁琐。而且面对各种不同的Crash
,很难将所有场景全部覆盖。我们需要找到一种方案,可以拦截所有Crash
情况,使得程序不会崩溃
3.1 Crash
的回调函数
创建LGUncaughtExceptionHandler
类,针对Crash
进行统一处理
+ (void)installUncaughtExceptionHandler {
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
}
- 调用系统提供的
NSSetUncaughtExceptionHandler
函数,传入LGExceptionHandlers
函数地址
3.2 处理Exception
异常
当出现Crash
,会自动触发LGExceptionHandlers
回调函数
void LGExceptionHandlers(NSException *exception) {
int32_t exceptionCount = OSAtomicIncrement32(&LGUncaughtExceptionCount);
// 如果太多不用处理
if (exceptionCount > LGUncaughtExceptionCount) {
return;
}
//获取调用堆栈
NSArray *callStack = [exception callStackSymbols];
NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
[userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
NSException *ex = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
//在主线程中,执行制定的方法, withObject是执行方法传入的参数
[[[LGUncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:ex waitUntilDone:YES];
}
- 调用
LGUncaughtExceptionHandler
类的lg_handleException
实例方法
3.3 App
回光返照
进入lg_handleException
方法,写入以下测试代码:
- (void)lg_handleException:(NSException *)exception{
//处理报错信息,可以写入沙盒文件,下次启动时上传服务器
[self validateAndSaveCriticalApplicationData:exception];
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Crash" message:nil preferredStyle:UIAlertControllerStyleAlert];
[controller addAction:[UIAlertAction actionWithTitle:@"继续执行" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}]];
[controller addAction:[UIAlertAction actionWithTitle:@"退出程序" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
self.dismissed = YES;
}]];
UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
[rootController presentViewController:controller animated:true completion:nil];
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);
}
- 一旦出现
Crash
,界面会弹窗UIAlert
弹窗。之后拿到当前RunLoop
,监听所有Mode
- 点击继续,循环切换
Mode
- 点击退出,改变
dismissed
标记,停止while
循环
3.4 代码的使用
在AppDelegate
的应用启动方法中,调用installUncaughtExceptionHandler
方法
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
[LGUncaughtExceptionHandler installUncaughtExceptionHandler];
return YES;
}
这种方式,相当于应用程序自启的Runloop
的平行空间。在这个平行空间我们开启一个弹框,跟着应用程序保活,并具备响应能力,也就是App
的回光返照
4. Signal异常拦截
常见的Crash
分为Exception
和Signal
两种
常规的Exception
- 当触发
Exception
异常,可以使用上述方法成功拦截
Signal
异常的产生
- 当触发
Signal
异常,单凭NSSetUncaughtExceptionHandler
注册回调函数是无法拦截到的,我们需要针对Signal
进行额外的处理
Crash
分析中的Signal
:https://www.jianshu.com/p/3a9dc6bd5e58
4.1 Signal
的回调函数
在LGUncaughtExceptionHandler
类中,增加对Signal
的处理
+ (void)installUncaughtExceptionHandler {
NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
//针对Signal的处理
signal(SIGABRT, LGSignalHandler);
signal(SIGILL, LGSignalHandler);
signal(SIGSEGV, LGSignalHandler);
signal(SIGFPE, LGSignalHandler);
signal(SIGBUS, LGSignalHandler);
signal(SIGPIPE, LGSignalHandler);
}
4.2 处理Signal
异常
进入LGSignalHandler
函数
//处理signal报错
void LGSignalHandler(int signal) {
int32_t exceptionCount = OSAtomicIncrement32(&LGUncaughtExceptionCount);
// 如果太多不用处理
if (exceptionCount > LGUncaughtExceptionCount) {
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 = [LGUncaughtExceptionHandler backtrace];
[userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
[userInfo setObject:[NSNumber numberWithInt:signal] forKey:LGUncaughtExceptionHandlerSignalKey];
NSException *ex = [NSException exceptionWithName:LGUncaughtExceptionHandlerSignalExceptionName reason:description userInfo:userInfo];
//在主线程中,执行指定的方法, withObject是执行方法传入的参数
[[[LGUncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:ex waitUntilDone:YES];
}
- 将
signal
一起包装到NSMutableDictionary
中 - 创建一个自定义名称和描述的
NSException
- 调用
LGUncaughtExceptionHandler
类的lg_handleException
对象方法
进入lg_handleException
方法,包含Exception
和Signa
的处理
- (void)lg_handleException:(NSException *)exception{
NSLog(@"%@", exception);
UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Crash" message:nil preferredStyle:UIAlertControllerStyleAlert];
[controller addAction:[UIAlertAction actionWithTitle:@"继续执行" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
}]];
[controller addAction:[UIAlertAction actionWithTitle:@"退出程序" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
self.dismissed = YES;
}]];
UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
[rootController presentViewController:controller animated:true completion:nil];
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:LGUncaughtExceptionHandlerSignalExceptionName]) {
kill(getpid(), [[[exception userInfo] objectForKey:LGUncaughtExceptionHandlerSignalKey] intValue]);
} else {
[exception raise];
}
}
- 对
Signa
的监听回收相应内存 - 对自定义
NSException
进行特殊处理
总结
NSSetUncaughtExceptionHandler
:系统提供的OC
层的方法,可以设置Crash
的回调函数,底层对应objc
的objc_setUncaughtExceptionHandler
函数当触发异常后,可以自启一个
Runloop
的平行空间。监听所有Mode
,循环切换Mode
。使用这种方式让程序保活,并具备响应能力,也就是App
的回光返照触发
Signal
异常,需要针对Signal
进行额外的处理使用
signal
函数,对每一种Signal
设置回调函数最后需要对
Signa
的监听回收相应内存
Crash
分析中的Signal
:https://www.jianshu.com/p/3a9dc6bd5e58