iOS之Crash拦截

547 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第11天,点击查看活动详情

造成Crash的情况有很多种,例如:KVO问题、NSNotification线程问题、数组越界、野指针、后台任务超时、内存爆出、主线程卡顿超阀值和死锁等。如果针对每一种Crash都进行分别处理,代码会非常繁琐。而且面对各种不同的Crash,很难将所有场景全部覆盖。我们需要找到一种方案,可以拦截所有Crash情况,使得程序不会崩溃。

Crash的回调函数

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

+ (void)installUncaughtExceptionHandler {
    NSSetUncaughtExceptionHandler(&LGExceptionHandlers);
}
  • 调用系统提供的NSSetUncaughtExceptionHandler函数,传入LGExceptionHandlers函数地址。
处理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实例方法。

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循环。

代码的使用

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

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

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