1. Crash常规处理

1.1 异常产生

日常开发中,我们会遇到各种Crash的出现。其中较为常见的,例如:数组越界

  1. #import "ViewController.h"
  2. @interface ViewController ()
  3. @property (nonatomic,strong) NSArray *arr;
  4. @end
  5. @implementation ViewController
  6. - (void)viewDidLoad {
  7. [super viewDidLoad];
  8. _arr = @[@"kc", @"hk", @"kd", @"cat"];
  9. }
  10. - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
  11. NSString *str = [self.arr objectAtIndex:4];
  12. NSLog(@"str:%@", str);
  13. }
  14. @end
  15. -------------------------
  16. *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'
  17. terminating with uncaught exception of type NSException

1.2 常规处理

解决方式也比较简单,创建一个NSArray的分类,使用MethodSwizzlingobjectAtIndex和自定义方法进行交互,在自定义方法中,增加索引判断,从而避免数组越界

  1. #import "NSArray+Extension.h"
  2. #import <objc/runtime.h>
  3. @implementation NSArray (Extension)
  4. + (void)initialize
  5. {
  6. if (self == [NSArray class]) {
  7. Method method1 = class_getInstanceMethod(NSClassFromString(@"__NSArrayI"), @selector(objectAtIndex:));
  8. Method method2 = class_getInstanceMethod(self, @selector(lg_objectAtIndex:));
  9. method_exchangeImplementations(method1, method2);
  10. }
  11. }
  12. - (id)lg_objectAtIndex:(NSUInteger)index{
  13. if(index < self.count){
  14. return [self lg_objectAtIndex:index];
  15. }
  16. NSLog(@"数组越界:index = %lu", index);
  17. return nil;
  18. }
  19. @end
  • 被替换的objectAtIndex:方法在__NSArrayI

    • iOSFoundation框架中,类簇是一种常用的设计模式,他将一些相近的,私有的,具体的子类组合在一个实体的抽象类下面,我称这个抽象类为实体的,是因为和我们交互的接口承载者,就是这个抽象大类

    • 我们平时常用的三大类,NSStringNSArrayNSDictionary都是类簇,我们通过他们创建的对象都是其子类对象的实例化,并不是他本身的实例化

一般来说方法交换都会在load方法中进行

  • 因为load方法执行时机早,在main函数之间就会调用,并且会主动调用

  • 所有本类和分类中的load方法都会调用,父类优先于子类,本类优先于分类

  • load方法的弊端,在objc_initload_images函数中被调用,影响启动速度,让原本的懒加载类在启动时刻被迫营业

如果使用initialize方法代替,该方法在首次接收消息时自动调用,也能达到相同的效果,并且可以保持类的懒加载特性

initialize方法的特点:

  • initialize方法属于被动调用的方法。原则上来说,会在main函数之后触发。但也有特殊情况,当A类load中调用B类的方法,会触发B类initialize方法,此时B类initializemain函数之前执行

  • 当子类首次接收消息时:

    • 父类、子类都有,先调用父类的,再调用子类的

    • 子类没有父类有,执行父类的方法。原则上一个类只会调用一次initialize方法,但这种情况父类的initialize会执行多次,可以通过添加if(self == [ClassName self])来进行判断

    • 父类没有子类有,执行子类的方法

  • 分类和本类都有,本类方法会被分类覆盖。多分类看文件的编译顺序,最后编译的分类中的方法会被执行

由于分类会覆盖本类的initialize方法,所以在initialize中进行方法交互会存在一定风险。所以还是推荐load方法内部实现Method Swizzleinitialize方法初始化全局变量或者静态变量

2. Crash底层分析

当出现异常时,打印函数调用栈
image.png

  • libobjc中,调用了objc_exception_rethrow函数

2.1 objc_exception_rethrow

打开objc源码,全局搜索objc_exception_rethrow
image.png

  • 只能找到函数的实现,但是无法找到函数的调用

2.2 _objc_terminate

我们只能尝试搜索函数调用栈中,在objc源码中的另一个_objc_terminate函数
image.png

进入_objc_terminate函数

  1. static void _objc_terminate(void)
  2. {
  3. if (PrintExceptions) {
  4. _objc_inform("EXCEPTIONS: terminating");
  5. }
  6. if (! __cxa_current_exception_type()) {
  7. // No current exception.
  8. (*old_terminate)();
  9. }
  10. else {
  11. // There is a current exception. Check if it's an objc exception.
  12. @try {
  13. __cxa_rethrow();
  14. } @catch (id e) {
  15. // It's an objc object. Call Foundation's handler, if any.
  16. (*uncaught_handler)((id)e);
  17. (*old_terminate)();
  18. } @catch (...) {
  19. // It's not an objc object. Continue to C++ terminate.
  20. (*old_terminate)();
  21. }
  22. }
  23. }
  • catch的代码中,通过*uncaught_handler进行函数调用

搜索uncaught_handler的赋值

  1. static objc_uncaught_exception_handler uncaught_handler = _objc_default_uncaught_exception_handler;
  2. static void _objc_default_uncaught_exception_handler(id exception)
  3. {
  4. }
  • 默认赋值_objc_default_uncaught_exception_handler空函数

2.3 objc_setUncaughtExceptionHandler

搜索到uncaught_handler的另一处代码,在objc_setUncaughtExceptionHandler函数中,可以对其进行回调函数的赋值

  1. /***********************************************************************
  2. * objc_setUncaughtExceptionHandler
  3. * Set a handler for uncaught Objective-C exceptions.
  4. * Returns the previous handler.
  5. **********************************************************************/
  6. objc_uncaught_exception_handler
  7. objc_setUncaughtExceptionHandler(objc_uncaught_exception_handler fn)
  8. {
  9. objc_uncaught_exception_handler result = uncaught_handler;
  10. uncaught_handler = fn;
  11. return result;
  12. }
  • objc_setUncaughtExceptionHandler属于底层objc函数,在Objective-C层,对应封装的为NSSetUncaughtExceptionHandler方法

  • 所以在OC中,调用NSSetUncaughtExceptionHandler方法,并设置自定义的回调函数。当出现异常后,系统自动调用回调函数,可以在里面拦截异常

2.4 exception_init

寻址_objc_terminate函数更直接的流程,在objc初始化的objc_init函数,对异常信号处理的初始化代码中,也有相关逻辑的处理
image.png

进入exception_init函数

  1. /***********************************************************************
  2. * exception_init
  3. * Initialize libobjc's exception handling system.
  4. * Called by map_images().
  5. **********************************************************************/
  6. void exception_init(void)
  7. {
  8. old_terminate = std::set_terminate(&_objc_terminate);
  9. }
  • _objc_terminate设置为回调函数

3. Crash拦截

造成Crash的情况多种多样,例如:

  • KVO问题

  • NSNotification线程问题

  • 数组越界

  • 野指针

  • 后台任务超时

  • 内存爆出

  • 主线程卡顿超阀值

  • 死锁

如果针对每一种Crash都进行分别处理,代码会非常繁琐。而且面对各种不同的Crash,很难将所有场景全部覆盖。我们需要找到一种方案,可以拦截所有Crash情况,使得程序不会崩溃

3.1 Crash的回调函数

创建LGUncaughtExceptionHandler类,针对Crash进行统一处理

  1. + (void)installUncaughtExceptionHandler {
  2. NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
  3. }
  • 调用系统提供的NSSetUncaughtExceptionHandler函数,传入LGExceptionHandlers函数地址

3.2 处理Exception异常

当出现Crash,会自动触发LGExceptionHandlers回调函数

  1. void LGExceptionHandlers(NSException *exception) {
  2. int32_t exceptionCount = OSAtomicIncrement32(&LGUncaughtExceptionCount);
  3. // 如果太多不用处理
  4. if (exceptionCount > LGUncaughtExceptionCount) {
  5. return;
  6. }
  7. //获取调用堆栈
  8. NSArray *callStack = [exception callStackSymbols];
  9. NSMutableDictionary *userInfo = [NSMutableDictionary dictionaryWithDictionary:[exception userInfo]];
  10. [userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
  11. NSException *ex = [NSException exceptionWithName:[exception name] reason:[exception reason] userInfo:userInfo];
  12. //在主线程中,执行制定的方法, withObject是执行方法传入的参数
  13. [[[LGUncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:ex waitUntilDone:YES];
  14. }
  • 调用LGUncaughtExceptionHandler类的lg_handleException实例方法

3.3 App回光返照

进入lg_handleException方法,写入以下测试代码:

  1. - (void)lg_handleException:(NSException *)exception{
  2. //处理报错信息,可以写入沙盒文件,下次启动时上传服务器
  3. [self validateAndSaveCriticalApplicationData:exception];
  4. UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Crash" message:nil preferredStyle:UIAlertControllerStyleAlert];
  5. [controller addAction:[UIAlertAction actionWithTitle:@"继续执行" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  6. }]];
  7. [controller addAction:[UIAlertAction actionWithTitle:@"退出程序" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
  8. self.dismissed = YES;
  9. }]];
  10. UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
  11. [rootController presentViewController:controller animated:true completion:nil];
  12. CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  13. CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
  14. while (!self.dismissed) {
  15. //点击继续
  16. for (NSString *mode in (__bridge NSArray *)allModes) {
  17. //快速切换Mode
  18. CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
  19. }
  20. }
  21. //点击退出
  22. CFRelease(allModes);
  23. NSSetUncaughtExceptionHandler(NULL);
  24. }
  • 一旦出现Crash,界面会弹窗UIAlert弹窗。之后拿到当前RunLoop,监听所有Mode
  • 点击继续,循环切换Mode
  • 点击退出,改变dismissed标记,停止while循环

3.4 代码的使用

AppDelegate的应用启动方法中,调用installUncaughtExceptionHandler方法

  1. - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
  2. [LGUncaughtExceptionHandler installUncaughtExceptionHandler];
  3. return YES;
  4. }

这种方式,相当于应用程序自启的Runloop的平行空间。在这个平行空间我们开启一个弹框,跟着应用程序保活,并具备响应能力,也就是App的回光返照

4. Signal异常拦截

常见的Crash分为ExceptionSignal两种

常规的Exception
image.png

  • 当触发Exception异常,可以使用上述方法成功拦截

Signal异常的产生
image.png

  • 当触发Signal异常,单凭NSSetUncaughtExceptionHandler注册回调函数是无法拦截到的,我们需要针对Signal进行额外的处理

Crash分析中的Signalhttps://www.jianshu.com/p/3a9dc6bd5e58

4.1 Signal的回调函数

LGUncaughtExceptionHandler类中,增加对Signal的处理

  1. + (void)installUncaughtExceptionHandler {
  2. NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
  3. //针对Signal的处理
  4. signal(SIGABRT, LGSignalHandler);
  5. signal(SIGILL, LGSignalHandler);
  6. signal(SIGSEGV, LGSignalHandler);
  7. signal(SIGFPE, LGSignalHandler);
  8. signal(SIGBUS, LGSignalHandler);
  9. signal(SIGPIPE, LGSignalHandler);
  10. }

4.2 处理Signal异常

进入LGSignalHandler函数

  1. //处理signal报错
  2. void LGSignalHandler(int signal) {
  3. int32_t exceptionCount = OSAtomicIncrement32(&LGUncaughtExceptionCount);
  4. // 如果太多不用处理
  5. if (exceptionCount > LGUncaughtExceptionCount) {
  6. return;
  7. }
  8. NSString* description = nil;
  9. switch (signal) {
  10. case SIGABRT:
  11. description = [NSString stringWithFormat:@"Signal SIGABRT was raised!\n"];
  12. break;
  13. case SIGILL:
  14. description = [NSString stringWithFormat:@"Signal SIGILL was raised!\n"];
  15. break;
  16. case SIGSEGV:
  17. description = [NSString stringWithFormat:@"Signal SIGSEGV was raised!\n"];
  18. break;
  19. case SIGFPE:
  20. description = [NSString stringWithFormat:@"Signal SIGFPE was raised!\n"];
  21. break;
  22. case SIGBUS:
  23. description = [NSString stringWithFormat:@"Signal SIGBUS was raised!\n"];
  24. break;
  25. case SIGPIPE:
  26. description = [NSString stringWithFormat:@"Signal SIGPIPE was raised!\n"];
  27. break;
  28. default:
  29. description = [NSString stringWithFormat:@"Signal %d was raised!",signal];
  30. }
  31. NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
  32. NSArray *callStack = [LGUncaughtExceptionHandler backtrace];
  33. [userInfo setObject:callStack forKey:LGUncaughtExceptionHandlerAddressesKey];
  34. [userInfo setObject:[NSNumber numberWithInt:signal] forKey:LGUncaughtExceptionHandlerSignalKey];
  35. NSException *ex = [NSException exceptionWithName:LGUncaughtExceptionHandlerSignalExceptionName reason:description userInfo:userInfo];
  36. //在主线程中,执行指定的方法, withObject是执行方法传入的参数
  37. [[[LGUncaughtExceptionHandler alloc] init] performSelectorOnMainThread:@selector(lg_handleException:) withObject:ex waitUntilDone:YES];
  38. }
  • signal一起包装到NSMutableDictionary
  • 创建一个自定义名称和描述的NSException
  • 调用LGUncaughtExceptionHandler类的lg_handleException对象方法

进入lg_handleException方法,包含ExceptionSigna的处理

  1. - (void)lg_handleException:(NSException *)exception{
  2. NSLog(@"%@", exception);
  3. UIAlertController *controller = [UIAlertController alertControllerWithTitle:@"Crash" message:nil preferredStyle:UIAlertControllerStyleAlert];
  4. [controller addAction:[UIAlertAction actionWithTitle:@"继续执行" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
  5. }]];
  6. [controller addAction:[UIAlertAction actionWithTitle:@"退出程序" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
  7. self.dismissed = YES;
  8. }]];
  9. UIViewController *rootController = [UIApplication sharedApplication].keyWindow.rootViewController;
  10. [rootController presentViewController:controller animated:true completion:nil];
  11. CFRunLoopRef runLoop = CFRunLoopGetCurrent();
  12. CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
  13. while (!self.dismissed) {
  14. //点击继续
  15. for (NSString *mode in (__bridge NSArray *)allModes) {
  16. //快速切换Mode
  17. CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
  18. }
  19. }
  20. //点击退出
  21. CFRelease(allModes);
  22. NSSetUncaughtExceptionHandler(NULL);
  23. signal(SIGABRT, SIG_DFL);
  24. signal(SIGILL, SIG_DFL);
  25. signal(SIGSEGV, SIG_DFL);
  26. signal(SIGFPE, SIG_DFL);
  27. signal(SIGBUS, SIG_DFL);
  28. signal(SIGPIPE, SIG_DFL);
  29. if ([[exception name] isEqual:LGUncaughtExceptionHandlerSignalExceptionName]) {
  30. kill(getpid(), [[[exception userInfo] objectForKey:LGUncaughtExceptionHandlerSignalKey] intValue]);
  31. } else {
  32. [exception raise];
  33. }
  34. }
  • Signa的监听回收相应内存
  • 对自定义NSException进行特殊处理

总结

  • NSSetUncaughtExceptionHandler:系统提供的OC层的方法,可以设置Crash的回调函数,底层对应objcobjc_setUncaughtExceptionHandler函数

  • 当触发异常后,可以自启一个Runloop的平行空间。监听所有Mode,循环切换Mode。使用这种方式让程序保活,并具备响应能力,也就是App的回光返照

  • 触发Signal异常,需要针对Signal进行额外的处理

    • 使用signal函数,对每一种Signal设置回调函数

    • 最后需要对Signa的监听回收相应内存

  • Crash分析中的Signalhttps://www.jianshu.com/p/3a9dc6bd5e58