原文 : 與佳期的個人博客(gonghonglou.com)
因為 SafeKit 的異常保護的原理是在 category 替換系統方法,只需在工程中引用 SafeKit 即可避免 NSArray 數組越界等引發的 crash,并不需要額外操作。所以日常開發中漸漸的并不會怎么在意到 SafeKit 的存在。
最近公司有一份項目需要重構,完全重寫的那種,從新建一份空工程開始。之前并沒有在意 SafeKit 的存在,所以在最開始并沒有在工程中引入 SafeKit,直到一次痛苦的 debug 發現 crash 發生在這樣的地方:
// cacheId 為 NSNmber 類型
if ([obj1.cacheId isEqualToNumber:obj2.cacheId]) {
// ...
}
報錯信息:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFNumber compare:]: nil argument'
因為在執行 NSNmber
的 isEqualToNumber:
方法時并沒有判斷 obj2.cacheId
是否為 nil
,蘋果的API也沒有對異常保護,所以當 obj2.cacheId
為 nil
時便會 crash。然后才想起 SafeKit
而且以這種寫法 Xcode 也不會給出警告,所以在 coding 時很容易忽略為 nil
的情況。
SafeKit 源碼
SafeKit 的源碼非常少,原理非常簡單,就是將 NSNumber
, NSArray
, NSMutableArray
, NSDictionary
, NSMutableArray
, NSString
, NSMutableString
中會因越界、為 nil
等情況發生 crash 的方法替換為自己的方法,在自己的方法中加判斷,如果越界、為 nil
等 直接 return,否則繼續執行。
例如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:
方法,當 anObject 不存在則直接返回self
以 safe_objectAtIndex:
替換 objectAtIndex:
方法,當數組越界時直接返回 nil
注意,在 class_getInstanceMethod
方法中,要先知道類對應的真實的類名才行,例如 NSArray 其實在 Runtime 中對應著 __NSArrayI
:
類 | Runtime 中對應 |
---|---|
NSNumber | __NSCFNumber |
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
NSString | __NSCFString |
NSString | __NSCFConstantString |
具體對應參考 SafeKit 源碼
其中,為了方便 NANumber
, NSDictionary
等分類調用,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]
用法會先調用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法,如果沒有再調用 - (ObjectType)objectAtIndex:(NSUInteger)index;
方法,所以 SafeKit 可以保證安全。
但是在 iOS11 beta 版中, array[i]
語法糖會直接調用 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法,如果沒有則直接報錯,所以為了適配 iOS11 ,在 SafeKit 的 NSArray+SafeKit 分類中還應該替換掉 - (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx NS_AVAILABLE(10_8, 6_0);
方法。
Method Swizzling 使用分析
Method Swizzling 大概是 Runtime 中最常用的一個黑魔法了,它本質上就是對 IMP 和 SEL 進行交換。
Method Swizzling 應該在 +load 方法中執行
+load 方法是當類或分類被添加到 Objective-C runtime 時被調用的;
+initialize 方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用。也就是說 +initialize 方法是以懶加載的方式被調用的,如果程序一直沒有給某個類或它的子類發送消息,那么這個類的 +initialize 方法是永遠不會被調用的。
所以 Method Swizzling 應該在 +load 方法中執行,避免 Method Swizzling 不會被執行到的情況
使用 dispatch_once 保證執行次數
Method Swizzling 本質上就是對 IMP 和 SEL 進行交換,如果被執行偶數次那么調換就會失效,相當于沒有調換。比如同時調換 NSArray 和 NSMutableArray 中的 objectAtIndex:,如果不用 dispatch_once 保證執行,就可能導致調換方法失效。
也正因為這個原因,在 load 方法中執行 Method Swizzling 時不可調用 [super load]
方法,否則同樣會導致調換方法失效。
參考
Objective-C Method Swizzling 的最佳實踐 一文中給出的最佳實踐:
@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
- 主類本身有實現需要替換的方法,也就是
class_addMethod
方法返回NO
。這種情況的處理比較簡單,直接交換兩個方法的實現。 - 主類本身沒有實現需要替換的方法,而是繼承了父類的實現,即
class_addMethod
方法返回YES
。這時使用class_getInstanceMethod
函數獲取到的originalSelector
指向的就是父類的方法,我們再通過執行class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
將父類的實現替換到我們自定義的mrc_viewWillAppear
方法中。這樣就達到了在mrc_viewWillAppear
方法的實現中調用父類實現的目的。 -
mrc_viewWillAppear:
方法的定義看似是遞歸調用引發死循環,其實不會。因為[self mrc_viewWillAppear:animated]
消息會動態找到mrc_viewWillAppear:
方法的實現,而它的實現已經被我們與viewWillAppear:
方法實現進行了互換,所以這段代碼不僅不會死循環,如果把[self mrc_viewWillAppear:animated]
換成[self viewWillAppear:animated]
反而會引發死循環。
神經病院Objective-C Runtime出院第三天——如何正確使用Runtime 一文中給出的Swizzling Method 標準定義,避免命名沖突:
@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(因為使用了函數指針),但是這種做法確實有效的防止了命名沖突的問題。原則上來說,其實上述做法更加符合標準化的Swizzling。這種做法可能和人們使用方法不同,但是這種做法更好。Swizzling Method 標準定義應該是如下的樣子:
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
后記
小白出手,請多指教。如言有誤,還望斧正!
轉載請保留原文地址:http://gonghonglou.com/2017/09/07/analyse-safekit/