現代移動應用的使用者體驗依賴於其穩定性和可靠性。然而,在開發過程中,我們時常會遇到各種崩潰問題。崩潰不僅會影響使用者的使用體驗,還可能損害應用的聲譽。因此,本文將詳細介紹一個名為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