原文 : 與佳期的個(gè)人博客(gonghonglou.com)
因?yàn)?SafeKit 的異常保護(hù)的原理是在 category 替換系統(tǒng)方法,只需在工程中引用 SafeKit 即可避免 NSArray 數(shù)組越界等引發(fā)的 crash,并不需要額外操作。所以日常開發(fā)中漸漸的并不會(huì)怎么在意到 SafeKit 的存在。
最近公司有一份項(xiàng)目需要重構(gòu),完全重寫的那種,從新建一份空工程開始。之前并沒有在意 SafeKit 的存在,所以在最開始并沒有在工程中引入 SafeKit,直到一次痛苦的 debug 發(fā)現(xiàn) crash 發(fā)生在這樣的地方:
// cacheId 為 NSNmber 類型
if ([obj1.cacheId isEqualToNumber:obj2.cacheId]) {
// ...
}
報(bào)錯(cuò)信息:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber compare:]: nil argument'
因?yàn)樵趫?zhí)行 NSNmber 的 isEqualToNumber: 方法時(shí)并沒有判斷 obj2.cacheId 是否為 nil,蘋果的API也沒有對(duì)異常保護(hù),所以當(dāng) obj2.cacheId 為 nil 時(shí)便會(huì) crash。然后才想起 SafeKit
而且以這種寫法 Xcode 也不會(huì)給出警告,所以在 coding 時(shí)很容易忽略為 nil 的情況。
SafeKit 源碼
SafeKit 的源碼非常少,原理非常簡單,就是將 NSNumber, NSArray, NSMutableArray, NSDictionary, NSMutableArray, NSString, NSMutableString 中會(huì)因越界、為 nil 等情況發(fā)生 crash 的方法替換為自己的方法,在自己的方法中加判斷,如果越界、為 nil等 直接 return,否則繼續(xù)執(zhí)行。
例如NSArray
#import "NSArray+SafeKit.h"
#import "NSObject+swizzle.h"
@implementation NSArray (SafeKit)
- (instancetype)initWithObjects_safe:(id *)objects count:(NSUInteger)cnt {
NSUInteger newCnt = 0;
for (NSUInteger i = 0; i < cnt; i++) {
if (!objects[i]) {
break;
}
newCnt++;
}
self = [self initWithObjects_safe:objects count:newCnt];
return self;
}
- (id)safe_objectAtIndex:(NSUInteger)index {
if (index >= [self count]) {
return nil;
}
return [self safe_objectAtIndex:index];
}
- (NSArray *)safe_arrayByAddingObject:(id)anObject {
if (!anObject) {
return self;
}
return [self safe_arrayByAddingObject:anObject];
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self safe_swizzleMethod:@selector(initWithObjects_safe:count:) tarClass:@"__NSPlaceholderArray" tarSel:@selector(initWithObjects:count:)];
[self safe_swizzleMethod:@selector(safe_objectAtIndex:) tarClass:@"__NSArrayI" tarSel:@selector(objectAtIndex:)];
[self safe_swizzleMethod:@selector(safe_arrayByAddingObject:) tarClass:@"__NSArrayI" tarSel:@selector(arrayByAddingObject:)];
});
}
@end
以 safe_arrayByAddingObject: 替換 arrayByAddingObject: 方法,當(dāng) anObject 不存在則直接返回self
以 safe_objectAtIndex: 替換 objectAtIndex: 方法,當(dāng)數(shù)組越界時(shí)直接返回 nil
注意,在 class_getInstanceMethod 方法中,要先知道類對(duì)應(yīng)的真實(shí)的類名才行,例如 NSArray 其實(shí)在 Runtime 中對(duì)應(yīng)著 __NSArrayI:
| 類 | Runtime 中對(duì)應(yīng) |
|---|---|
| NSNumber | __NSCFNumber |
| NSArray | __NSArrayI |
| NSMutableArray | __NSArrayM |
| NSDictionary | __NSDictionaryI |
| NSMutableDictionary | __NSDictionaryM |
| NSString | __NSCFString |
| NSString | __NSCFConstantString |
具體對(duì)應(yīng)參考 SafeKit 源碼
其中,為了方便 NANumber, NSDictionary 等分類調(diào)用,Method Swizzling 操作也被作者在 NSObject 的 Swizzle 分類中替換成自己的 safe_swizzleMethod 方法:
@implementation NSObject(Swizzle)
+ (void)safe_swizzleMethod:(SEL)srcSel tarSel:(SEL)tarSel{
Class clazz = [self class];
[self safe_swizzleMethod:clazz srcSel:srcSel tarClass:clazz tarSel:tarSel];
}
+ (void)safe_swizzleMethod:(SEL)srcSel tarClass:(NSString *)tarClassName tarSel:(SEL)tarSel{
if (!tarClassName) {
return;
}
Class srcClass = [self class];
Class tarClass = NSClassFromString(tarClassName);
[self safe_swizzleMethod:srcClass srcSel:srcSel tarClass:tarClass tarSel:tarSel];
}
+ (void)safe_swizzleMethod:(Class)srcClass srcSel:(SEL)srcSel tarClass:(Class)tarClass tarSel:(SEL)tarSel{
if (!srcClass) {
return;
}
if (!srcSel) {
return;
}
if (!tarClass) {
return;
}
if (!tarSel) {
return;
}
Method srcMethod = class_getInstanceMethod(srcClass,srcSel);
Method tarMethod = class_getInstanceMethod(tarClass,tarSel);
method_exchangeImplementations(srcMethod, tarMethod);
}
@end
需要注意的是:
在 iOS10 及以前,NSArray 的語法糖 array[i] 用法會(huì)先調(diào)用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0); 方法,如果沒有再調(diào)用 - (ObjectType)objectAtIndex:(NSUInteger)index; 方法,所以 SafeKit 可以保證安全。
但是在 iOS11 beta 版中, array[i] 語法糖會(huì)直接調(diào)用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0); 方法,如果沒有則直接報(bào)錯(cuò),所以為了適配 iOS11 ,在 SafeKit 的 NSArray+SafeKit 分類中還應(yīng)該替換掉 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0); 方法。
Method Swizzling 使用分析
Method Swizzling 大概是 Runtime 中最常用的一個(gè)黑魔法了,它本質(zhì)上就是對(duì) IMP 和 SEL 進(jìn)行交換。
Method Swizzling 應(yīng)該在 +load 方法中執(zhí)行
+load 方法是當(dāng)類或分類被添加到 Objective-C runtime 時(shí)被調(diào)用的;
+initialize 方法是在類或它的子類收到第一條消息之前被調(diào)用的,這里所指的消息包括實(shí)例方法和類方法的調(diào)用。也就是說 +initialize 方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個(gè)類或它的子類發(fā)送消息,那么這個(gè)類的 +initialize 方法是永遠(yuǎn)不會(huì)被調(diào)用的。
所以 Method Swizzling 應(yīng)該在 +load 方法中執(zhí)行,避免 Method Swizzling 不會(huì)被執(zhí)行到的情況
使用 dispatch_once 保證執(zhí)行次數(shù)
Method Swizzling 本質(zhì)上就是對(duì) IMP 和 SEL 進(jìn)行交換,如果被執(zhí)行偶數(shù)次那么調(diào)換就會(huì)失效,相當(dāng)于沒有調(diào)換。比如同時(shí)調(diào)換 NSArray 和 NSMutableArray 中的 objectAtIndex:,如果不用 dispatch_once 保證執(zhí)行,就可能導(dǎo)致調(diào)換方法失效。
也正因?yàn)檫@個(gè)原因,在 load 方法中執(zhí)行 Method Swizzling 時(shí)不可調(diào)用 [super load] 方法,否則同樣會(huì)導(dǎo)致調(diào)換方法失效。
參考
Objective-C Method Swizzling 的最佳實(shí)踐 一文中給出的最佳實(shí)踐:
@interface UIViewController (MRCUMAnalytics)
@end
@implementation UIViewController (MRCUMAnalytics)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(mrc_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)mrc_viewWillAppear:(BOOL)animated {
[self mrc_viewWillAppear:animated];
[MobClick beginLogPageView:NSStringFromClass([self class])];
}
@end
- 主類本身有實(shí)現(xiàn)需要替換的方法,也就是
class_addMethod方法返回NO。這種情況的處理比較簡單,直接交換兩個(gè)方法的實(shí)現(xiàn)。 - 主類本身沒有實(shí)現(xiàn)需要替換的方法,而是繼承了父類的實(shí)現(xiàn),即
class_addMethod方法返回YES。這時(shí)使用class_getInstanceMethod函數(shù)獲取到的originalSelector指向的就是父類的方法,我們再通過執(zhí)行class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));將父類的實(shí)現(xiàn)替換到我們自定義的mrc_viewWillAppear方法中。這樣就達(dá)到了在mrc_viewWillAppear方法的實(shí)現(xiàn)中調(diào)用父類實(shí)現(xiàn)的目的。 -
mrc_viewWillAppear:方法的定義看似是遞歸調(diào)用引發(fā)死循環(huán),其實(shí)不會(huì)。因?yàn)?[self mrc_viewWillAppear:animated]消息會(huì)動(dòng)態(tài)找到mrc_viewWillAppear:方法的實(shí)現(xiàn),而它的實(shí)現(xiàn)已經(jīng)被我們與viewWillAppear:方法實(shí)現(xiàn)進(jìn)行了互換,所以這段代碼不僅不會(huì)死循環(huán),如果把[self mrc_viewWillAppear:animated]換成[self viewWillAppear:animated]反而會(huì)引發(fā)死循環(huán)。
神經(jīng)病院Objective-C Runtime出院第三天——如何正確使用Runtime 一文中給出的Swizzling Method 標(biāo)準(zhǔn)定義,避免命名沖突:
@implementation NSView (MyViewAdditions)
static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);
static void MySetFrame(id self, SEL _cmd, NSRect frame) {
// do custom work
SetFrameIMP(self, _cmd, frame);
}
+ (void)load {
[self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
}
@end
雖然上面的代碼看上去不是OC(因?yàn)槭褂昧撕瘮?shù)指針),但是這種做法確實(shí)有效的防止了命名沖突的問題。原則上來說,其實(shí)上述做法更加符合標(biāo)準(zhǔn)化的Swizzling。這種做法可能和人們使用方法不同,但是這種做法更好。Swizzling Method 標(biāo)準(zhǔn)定義應(yīng)該是如下的樣子:
typedef IMP *IMPPointer;
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
IMP imp = NULL;
Method method = class_getInstanceMethod(class, original);
if (method) {
const char *type = method_getTypeEncoding(method);
imp = class_replaceMethod(class, original, replacement, type);
if (!imp) {
imp = method_getImplementation(method);
}
}
if (imp && store) { *store = imp; }
return (imp != NULL);
}
@implementation NSObject (FRRuntimeAdditions)
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end
后記
小白出手,請多指教。如言有誤,還望斧正!
轉(zhuǎn)載請保留原文地址:http://gonghonglou.com/2017/09/07/analyse-safekit/