现代移动应用的用户体验依赖于其稳定性和可靠性。然而,在开发过程中,我们时常会遇到各种崩溃问题。崩溃不仅会影响用户的使用体验,还可能损害应用的声誉。因此,本文将详细介绍一个名为CrashPrevention的工具类,它能够为iOS开发者提供多方面的崩溃预防措施,借助该工具类,开发者能够有效减少崩溃的发生,并提升应用的稳定性。
CrashPrevention工具类概述
CrashPrevention是一个易于集成的工具类,专为iOS应用中的多种常见崩溃情况提供预防措施。通过调用相关方法,开发者可以开启针对数组操作、字典操作、未识别的选择器、通知中心、键值观察(KVO)、字符串操作、多线程操作以及UI线程操作的保护机制。特别值得一提的是,CrashPrevention让开发者可以通过一个全局的isDebug
标志,灵活控制是否启用这些崩溃预防措施。
CrashPrevention.h 头文件
首先,我们来看一下CrashPrevention的头文件,其中定义了所有的预防方法:
#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>@interface CrashPrevention : NSObject// 设置是否在 Debug 模式中启用崩溃预防+ (void)setDebugMode:(BOOL)isDebug;// 启用所有崩溃保护+ (void)enableAllCrashPrevention;// 启用各类崩溃保护+ (void)enableArrayProtection;+ (void)enableDictionaryProtection;+ (void)enableSelectorProtection;+ (void)enableNotificationProtection;+ (void)enableKVOCrashProtection;+ (void)enableStringProtection;+ (void)enableThreadSafetyProtection;+ (void)enableUIThreadProtection;@end
CrashPrevention.m 实现文件
接下来,我们深入了解实现文件的设计思路和具体代码。
全局Debug标志
@implementation CrashPrevention// 用于记录是否在 Debug 模式下启用崩溃预防static BOOL debugModeEnabled = NO;// 设置是否在 Debug 模式中启用崩溃预防+ (void)setDebugMode:(BOOL)isDebug { debugModeEnabled = isDebug;}
通过static BOOL debugModeEnabled
,我们可以记录是否启用调试模式,基于此标志决定是否启用崩溃预防功能。
启用所有崩溃保护
+ (void)enableAllCrashPrevention { if (!debugModeEnabled) { return; } [self enableArrayProtection]; [self enableDictionaryProtection]; [self enableSelectorProtection]; [self enableNotificationProtection]; [self enableKVOCrashProtection]; [self enableStringProtection]; [self enableThreadSafetyProtection]; [self enableUIThreadProtection];}
该方法通过检查debugModeEnabled
标志,决定是否依次启用各类崩溃保护。
数组越界保护
+ (void)enableArrayProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayI") original:@selector(objectAtIndex:) swizzled:@selector(safe_objectAtIndex:)]; [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayM") original:@selector(objectAtIndex:) swizzled:@selector(safe_objectAtIndex:)]; });}- (id)safe_objectAtIndex:(NSUInteger)index { if (index < self.count) { return [self safe_objectAtIndex:index]; } else { @try { NSLog(@"Array index out of bound: %lu", (unsigned long)index); } @catch (NSException *exception) { // 处理异常 } return nil; }}
通过Method Swizzling
,我们可以将数组的objectAtIndex:
方法替换为安全版本。在越界的情况下,返回nil
并记录日志,而不会崩溃。
字典键值检查保护
+ (void)enableDictionaryProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryI") original:@selector(objectForKey:) swizzled:@selector(safe_objectForKey:)]; });}- (id)safe_objectForKey:(id)key { if (key) { return [self safe_objectForKey:key]; } else { @try { NSLog(@"Attempted to access dictionary with nil key"); } @catch (NSException *exception) { // 处理异常 } return nil; }}
类似地,通过Method Swizzling
,我们可以将objectForKey:
方法替换为安全版本,防止使用nil
作为键值时的崩溃。
消息转发保护
+ (void)enableSelectorProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method forwardMethod = class_getInstanceMethod([self class], @selector(forwardingMethod:)); class_addMethod([self class], NSSelectorFromString(@"unrecognizedSelectorHandler"), method_getImplementation(forwardMethod), method_getTypeEncoding(forwardMethod)); });}- (void)forwardingMethod:(SEL)aSelector {}+ (BOOL)resolveInstanceMethod:(SEL)sel { if (!debugModeEnabled) { return [super resolveInstanceMethod:sel]; } class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(forwardingMethod:)),"v@:"); return YES;}
通过添加一个默认的forwardingMethod:
,我们防止调用未实现的方法时崩溃。
通知中心保护
+ (void)enableNotificationProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[NSNotificationCenter class] original:@selector(addObserver:selector:name:object:) swizzled:@selector(safe_addObserver:selector:name:object:)]; [self swizzleInstanceMethod:[NSNotificationCenter class] original:@selector(removeObserver:name:object:) swizzled:@selector(safe_removeObserver:name:object:)]; });}- (void)safe_addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject { if (observer) { [[NSNotificationCenter defaultCenter] safe_addObserver:observer selector:aSelector name:aName object:anObject]; } else { @try { NSLog(@"Attempted to add a nil observer for name: %@", aName); } @catch (NSException *exception) { // 处理异常 } }}- (void)safe_removeObserver:(NSObject *)observer name:(NSString *)aName object:(id)anObject { if (observer) { [[NSNotificationCenter defaultCenter] safe_removeObserver:observer name:aName object:anObject]; } else { @try { NSLog(@"Attempted to remove a nil observer for name: %@", aName); } @catch (NSException *exception) { // 处理异常 } }}
在添加和移除观察者时进行空检查,防止空观察者导致的崩溃。
KVO 保护
+ (void)enableKVOCrashProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[NSObject class] original:@selector(addObserver:forKeyPath:options:context:) swizzled:@selector(safe_addObserver:forKeyPath:options:context:)]; [self swizzleInstanceMethod:[NSObject class] original:@selector(removeObserver:forKeyPath:) swizzled:@selector(safe_removeObserver:forKeyPath:)]; });}- (void)safe_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { if (observer && keyPath) { [self safe_addObserver:observer forKeyPath:keyPath options:options context:context]; } else { @try { NSLog(@"Attempted to add observer with nil observer or key path: %@", keyPath); } @catch (NSException *exception) { // 处理异常 } }}- (void)safe_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { if (observer && keyPath) { [self safe_removeObserver:observer forKeyPath:keyPath]; } else { @try { NSLog(@"Attempted to remove observer with nil observer or key path: %@", keyPath); } @catch (NSException *exception) { // 处理异常 } }}
在添加和移除KVO时进行必要检查,确保参数合法,防止崩溃。
字符串越界检查
+ (void)enableStringProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString") original:@selector(substringFromIndex:) swizzled:@selector(safe_substringFromIndex:)]; });}- (NSString *)safe_substringFromIndex:(NSUInteger)from { if (from <= self.length) { return [self safe_substringFromIndex:from]; } else { @try { NSLog(@"String index out of bound: %lu", (unsigned long)from); } @catch (NSException *exception) { // 处理异常 } return nil; }}
通过交换NSString的相关方法,确保在越界访问时返回nil
并记录日志,从而避免崩溃。
线程安全保护
+ (void)enableThreadSafetyProtection { if (!debugModeEnabled) { return; } // 实现是与具体使用场景相关的,需要结合项目实际情况实现}
这部分的实现高度依赖于具体的使用场景,比如可以使用dispatch_barrier_async
、NSLock
等技术实现。
UI线程保护
+ (void)enableUIThreadProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[UIView class] original:@selector(setNeedsLayout) swizzled:@selector(safe_setNeedsLayout)]; });}- (void)safe_setNeedsLayout { if ([NSThread isMainThread]) { [self safe_setNeedsLayout]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [self safe_setNeedsLayout]; }); @try { NSLog(@"setNeedsLayout was called off the main thread. Fixed by dispatching to main queue."); } @catch (NSException *exception) { // 处理异常 } }}
确保UI操作总在主线程进行,如果不是,则调度到主线程执行,并记录警告日志。
方法交换
#pragma mark - Method Swizzling+ (void)swizzleInstanceMethod:(Class)cls original:(SEL)originalSelector swizzled:(SEL)swizzledSelector { Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}@end
核心方法交换逻辑,通过Method Swizzling
替换原有的方法实现。
使用CrashPrevention工具类
在应用启动时初始化CrashPrevention,并设置是否启用调试模式:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 设置 Debug 模式 #ifdef DEBUG [CrashPrevention setDebugMode:YES]; #else [CrashPrevention setDebugMode:NO]; #endif [CrashPrevention enableAllCrashPrevention]; return YES;}
总结
CrashPrevention工具类为iOS开发者提供了多个方面的崩溃预防措施,通过简单调用,即可为数组、字典、未识别选择器、通知中心、KVO、字符串、多线程和UI线程的操作提供全面的保护。特别是通过isDebug
标志,让开发者可以灵活控制这些预防措施在调试阶段和正式发布中的启用状态。借助这一工具类,开发者能够有效减少崩溃问题的发生,提升应用的稳定性和用户体验。
最后附上完整代码:
CrashPrevention.h文件
#import <Foundation/Foundation.h>#import <UIKit/UIKit.h>@interface CrashPrevention : NSObject// 设置是否在 Debug 模式中启用崩溃预防+ (void)setDebugMode:(BOOL)isDebug;// 启用所有崩溃保护+ (void)enableAllCrashPrevention;// 启用各类崩溃保护+ (void)enableArrayProtection;+ (void)enableDictionaryProtection;+ (void)enableSelectorProtection;+ (void)enableNotificationProtection;+ (void)enableKVOCrashProtection;+ (void)enableStringProtection;+ (void)enableThreadSafetyProtection;+ (void)enableUIThreadProtection;@end
CrashPrevention.m文件
#import"CrashPrevention.h"#import <objc/runtime.h>@implementation CrashPrevention// 用于记录是否在 Debug 模式下启用崩溃预防static BOOL debugModeEnabled = NO;// 设置是否在 Debug 模式中启用崩溃预防+ (void)setDebugMode:(BOOL)isDebug { debugModeEnabled = isDebug;}// 启用所有崩溃保护+ (void)enableAllCrashPrevention { if (!debugModeEnabled) { return; } [self enableArrayProtection]; [self enableDictionaryProtection]; [self enableSelectorProtection]; [self enableNotificationProtection]; [self enableKVOCrashProtection]; [self enableStringProtection]; [self enableThreadSafetyProtection]; [self enableUIThreadProtection];}#pragma mark - Array Protection+ (void)enableArrayProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayI") original:@selector(objectAtIndex:) swizzled:@selector(safe_objectAtIndex:)]; [self swizzleInstanceMethod:NSClassFromString(@"__NSArrayM") original:@selector(objectAtIndex:) swizzled:@selector(safe_objectAtIndex:)]; });}- (id)safe_objectAtIndex:(NSUInteger)index { if (index < self.count) { return [self safe_objectAtIndex:index]; } else { @try { NSLog(@"Array index out of bound: %lu", (unsigned long)index); } @catch (NSException *exception) { // 处理异常 } @finally { return nil; } }}#pragma mark - Dictionary Protection+ (void)enableDictionaryProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryI") original:@selector(objectForKey:) swizzled:@selector(safe_objectForKey:)]; [self swizzleInstanceMethod:NSClassFromString(@"__NSDictionaryM") original:@selector(objectForKey:) swizzled:@selector(safe_objectForKey:)]; });}- (id)safe_objectForKey:(id)key { if (key) { return [self safe_objectForKey:key]; } else { @try { NSLog(@"Attempted to access dictionary with nil key"); } @catch (NSException *exception) { // 处理异常 } @finally { return nil; } }}#pragma mark - Selector Protection+ (void)enableSelectorProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method forwardMethod = class_getInstanceMethod([self class], @selector(forwardingMethod:)); class_addMethod([self class], NSSelectorFromString(@"unrecognizedSelectorHandler"), method_getImplementation(forwardMethod), method_getTypeEncoding(forwardMethod)); });}- (void)forwardingMethod:(SEL)aSelector {}+ (BOOL)resolveInstanceMethod:(SEL)sel { if (!debugModeEnabled) { return [super resolveInstanceMethod:sel]; } class_addMethod([self class], sel, class_getMethodImplementation([self class], @selector(forwardingMethod:)),"v@:"); return YES;}#pragma mark - Notification Protection+ (void)enableNotificationProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[NSNotificationCenter class] original:@selector(addObserver:selector:name:object:) swizzled:@selector(safe_addObserver:selector:name:object:)]; [self swizzleInstanceMethod:[NSNotificationCenter class] original:@selector(removeObserver:name:object:) swizzled:@selector(safe_removeObserver:name:object:)]; [self swizzleInstanceMethod:[NSNotificationCenter class] original:@selector(removeObserver:) swizzled:@selector(safe_removeObserver:)]; });}- (void)safe_addObserver:(NSObject *)observer selector:(SEL)aSelector name:(NSString *)aName object:(id)anObject { if (observer) { [[NSNotificationCenter defaultCenter] safe_addObserver:observer selector:aSelector name:aName object:anObject]; } else { @try { NSLog(@"Attempted to add a nil observer for name: %@", aName); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}- (void)safe_removeObserver:(NSObject *)observer { if (observer) { [[NSNotificationCenter defaultCenter] safe_removeObserver:observer]; } else { @try { NSLog(@"Attempted to remove a nil observer"); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}- (void)safe_removeObserver:(NSObject *)observer name:(NSString *)aName object:(id)anObject { if (observer) { [[NSNotificationCenter defaultCenter] safe_removeObserver:observer name:aName object:anObject]; } else { @try { NSLog(@"Attempted to remove a nil observer for name: %@", aName); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}#pragma mark - KVO Protection+ (void)enableKVOCrashProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[NSObject class] original:@selector(addObserver:forKeyPath:options:context:) swizzled:@selector(safe_addObserver:forKeyPath:options:context:)]; [self swizzleInstanceMethod:[NSObject class] original:@selector(removeObserver:forKeyPath:) swizzled:@selector(safe_removeObserver:forKeyPath:)]; });}- (void)safe_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { if (observer && keyPath) { [self safe_addObserver:observer forKeyPath:keyPath options:options context:context]; } else { @try { NSLog(@"Attempted to add observer with nil observer or key path: %@", keyPath); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}- (void)safe_removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { if (observer && keyPath) { [self safe_removeObserver:observer forKeyPath:keyPath]; } else { @try { NSLog(@"Attempted to remove observer with nil observer or key path: %@", keyPath); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}#pragma mark - String Protection+ (void)enableStringProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString") original:@selector(substringFromIndex:) swizzled:@selector(safe_substringFromIndex:)]; [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString") original:@selector(substringToIndex:) swizzled:@selector(safe_substringToIndex:)]; [self swizzleInstanceMethod:NSClassFromString(@"__NSCFConstantString") original:@selector(substringWithRange:) swizzled:@selector(safe_substringWithRange:)]; });}- (NSString *)safe_substringFromIndex:(NSUInteger)from { if (from <= self.length) { return [self safe_substringFromIndex:from]; } else { @try { NSLog(@"String index out of bound: %lu", (unsigned long)from); } @catch (NSException *exception) { // 处理异常 } @finally { return nil; } }}- (NSString *)safe_substringToIndex:(NSUInteger)to { if (to <= self.length) { return [self safe_substringToIndex:to]; } else { @try { NSLog(@"String index out of bound: %lu", (unsigned long)to); } @catch (NSException *exception) { // 处理异常 } @finally { return nil; } }}- (NSString *)safe_substringWithRange:(NSRange)range { if (range.location + range.length <= self.length) { return [self safe_substringWithRange:range]; } else { @try { NSLog(@"String range out of bound: %@", NSStringFromRange(range)); } @catch (NSException *exception) { // 处理异常 } @finally { return nil; } }}#pragma mark - Thread Safety Protection+ (void)enableThreadSafetyProtection { if (!debugModeEnabled) { return; } // 实现是与具体使用场景相关的,需要结合项目实际情况实现}#pragma mark - UI Thread Protection+ (void)enableUIThreadProtection { if (!debugModeEnabled) { return; } static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleInstanceMethod:[UIView class] original:@selector(setNeedsLayout) swizzled:@selector(safe_setNeedsLayout)]; [self swizzleInstanceMethod:[UIView class] original:@selector(setNeedsDisplay) swizzled:@selector(safe_setNeedsDisplay)]; [self swizzleInstanceMethod:[UIView class] original:@selector(setNeedsDisplayInRect:) swizzled:@selector(safe_setNeedsDisplayInRect:)]; });}- (void)safe_setNeedsLayout { if ([NSThread isMainThread]) { [self safe_setNeedsLayout]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [self safe_setNeedsLayout]; }); @try { NSLog(@"setNeedsLayout was called off the main thread. Fixed by dispatching to main queue."); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}- (void)safe_setNeedsDisplay { if ([NSThread isMainThread]) { [self safe_setNeedsDisplay]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [self safe_setNeedsDisplay]; }); @try { NSLog(@"setNeedsDisplay was called off the main thread. Fixed by dispatching to main queue."); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}- (void)safe_setNeedsDisplayInRect:(CGRect)rect { if ([NSThread isMainThread]) { [self safe_setNeedsDisplayInRect:rect]; } else { dispatch_async(dispatch_get_main_queue(), ^{ [self safe_setNeedsDisplayInRect:rect]; }); @try { NSLog(@"setNeedsDisplayInRect: was called off the main thread. Fixed by dispatching to main queue."); } @catch (NSException *exception) { // 处理异常 } @finally { // 什么也不做 } }}#pragma mark - Method Swizzling+ (void)swizzleInstanceMethod:(Class)cls original:(SEL)originalSelector swizzled:(SEL)swizzledSelector { Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); BOOL didAddMethod = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}@end