從 SafeKit 看異常保護及 Method Swizzling 使用分析

原文 : 與佳期的個人博客(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'

因為在執行 NSNmberisEqualToNumber: 方法時并沒有判斷 obj2.cacheId 是否為 nil,蘋果的API也沒有對異常保護,所以當 obj2.cacheIdnil 時便會 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 操作也被作者在 NSObjectSwizzle 分類中替換成自己的 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

后記

Reference

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容