Objective-C Runtime Method Swizzling

Method Swizzling 原理

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
}
typedef struct objc_method *Method;

SEL是把函數的名稱,參數類型,返回值類型拼在一起進行hash化后得到的在一個類里唯一存在的用以標識唯一一個函數指針的數據類型。
IMP是用來調用C的函數的函數指針。
Type Coding把OC的各種類型進行了類型編碼,其實就是進行了對應關系的約定。

OC里的Method其實是一個封裝了方法名method_name(SEL),方法實現的函數指針method_imp(IMP),參數和返回值的類型(method_types)的結構體,由此我們知道方法的SEL和IMP是一一對應的,同一個類中沒有相同的兩個SEL。

原則上,方法的名稱 name 和方法的實現 imp 是一一對應的,而 Method Swizzling 的原理就是動態地改變它們的對應關系,以達到替換方法實現的目的。

Method Swizzling的使用

1.給NSObject添加分類

@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; 

@end


====================================================================
#import "NSObject+MethodSwizzling.h"
#import 

@implementation NSObject (MethodSwizzling)
+  (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector
{
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, originalSelector);

    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

    //先嘗試給源SEL添加IMP,這里是為了避免源SEL沒有實現IMP的情況
    BOOL didAddMethod = class_addMethod(class,originalSelector,

                                        method_getImplementation(swizzledMethod),

                                        method_getTypeEncoding(swizzledMethod));




    if (didAddMethod) {//添加成功:說明源SEL沒有實現IMP,將源SEL的IMP替換到交換SEL的IMP
        class_replaceMethod(class,swizzledSelector,

                            method_getImplementation(originalMethod),

                            method_getTypeEncoding(originalMethod));

    } else {//添加失敗:說明源SEL已經有IMP,直接將兩個SEL的IMP交換即可

        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

@end

簡單調用:

#import "UIViewController+MethodSwizzling.h"
#import "NSObject+MethodSwizzling.h"

@implementation UIViewController (MethodSwizzling)
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    [self methodSwizzlingWithOriginalSelector:@selector(viewWillAppear:) bySwizzledSelector:@selector(my_viewWillAppear:) ];
    });
}

-(void)my_viewWillAppear:(BOOL)animated{
    NSLog(@"調用了自己定義的viewWillAppear方法");
    [self my_viewWillAppear:YES];
}
@end

Tips:
1.為什么要在+(void)load方法里面調用method swizzling?
+load 和 +initialize 是 Objective-C runtime 會自動調用的兩個類方法。但是它們被調用的時機卻是有差別的,+load 方法是在類被加載的時候調用的,也就是一定會被調用。而 +initialize 方法是在類或它的子類收到第一條消息之前被調用的,這里所指的消息包括實例方法和類方法的調用。也就是說 +initialize 方法是以懶加載的方式被調用的,如果程序一直沒有給某個類或它的子類發送消息,那么這個類的 +initialize 方法是永遠不會被調用的。此外 +load 方法還有一個非常重要的特性,那就是子類、父類和分類中的 +load 方法的實現是被區別對待的。換句話說在 Objective-C runtime 自動調用 +load 方法時,分類中的 +load 方法并不會對主類中的 +load 方法造成覆蓋。綜上所述,+load 方法是實現 Method Swizzling 邏輯的最佳“場所”。

2.為什么使用dispatch_once執行方法交換?
方法交換應該要線程安全,而且保證在任何情況下(多線程環境,或者被其他人手動再次調用+load方法)只交換一次,除非只是臨時交換使用,在使用完成后又交換回來。 最常用的用法是在+load方法中使用dispatch_once來保證交換是安全的。

應用一:NSMutableArray崩潰問題

當我們對一個NSMutableArray插入或者添加一個nil會導致崩潰,或者需要獲取、移除的對象超過了數組的最大邊界,就會出現數組越界,也會導致崩潰。

這個時候我們可以使用method swizzling來解決該問題,讓數組在上述情況不會崩潰。

** 實現代碼: **

#import "NSObject+MethodSwizzling.h"
#import "NSMutableArray+SWmethod.h"
#import 

@implementation NSMutableArray (SWmethod)
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];

        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];

        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];

        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];

        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
    });
}



- (void)safeAddObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
    } else {
        [self safeAddObject:obj];
    }
}
- (void)safeRemoveObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
        return;
    }
    [self safeRemoveObject:obj];
}

- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
    if (anObject == nil) {
        NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
    } else if (index > self.count) {
        NSLog(@"%s index is invalid", __FUNCTION__);
    } else {
        [self safeInsertObject:anObject atIndex:index];
    }
}

- (id)safeObjectAtIndex:(NSUInteger)index {
    if (self.count == 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return nil;
    }
    if (index > self.count) {
        NSLog(@"%s index out of bounds in array", __FUNCTION__);
        return nil;
    }
    return [self safeObjectAtIndex:index];
}

- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
    if (self.count <= 0)="" {="" nslog(@"%s="" can't="" get="" any="" object="" from="" an="" empty="" array",="" __function__);="" return;="" }="" if="" (index="">= self.count) {
        NSLog(@"%s index out of bound", __FUNCTION__);
        return;
    }
    [self safeRemoveObjectAtIndex:index];
}
@end

上面只是展示了如何避免NSMutableArray的崩潰,主要是在原有的系統方法里面加上了判斷。據此大家可以自己實現NSArray、NSDictonary的崩潰處理。

應用二:全局更換UILabel的默認字體

需求

在項目比較成熟的基礎上,遇到了這樣一個需求,應用中需要引入新的字體,需要更換所有Label的默認字體,但是同時,對于一些特殊設置了字體的label又不需要更換。乍看起來,這個問題確實十分棘手,首先項目比較大,一個一個設置所有使用到的label的font工作量是巨大的,并且在許多動態展示的界面中,可能會漏掉一些label,產生bug。其次,項目中的label來源并不唯一,有用代碼創建的,有xib和storyBoard中的,這也將浪費很大的精力。

解決辦法

這是最簡單方便的方法,我們可以使用runtime機制替換掉UILabel的初始化方法,在其中對label的字體進行默認設置。因為Label可以從initWithFrame、init和nib文件三個來源初始化,所以我們需要將這三個初始化的方法都替換掉。

實現代碼

#import "UILabel+YHBaseChangeDefaultFont.h"
#import 
@implementation UILabel (YHBaseChangeDefaultFont)
/**
 *每個NSObject的子類都會調用下面這個方法 在這里將init方法進行替換,使用我們的新字體
 *如果在程序中又特殊設置了字體 則特殊設置的字體不會受影響 但是不要在Label的init方法中設置字體
 *從init和initWithFrame和nib文件的加載方法 都支持更換默認字體
 */
+(void)load{
    //只執行一次這個方法
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        //替換三個方法
        SEL originalSelector = @selector(init);
        SEL originalSelector2 = @selector(initWithFrame:);
        SEL originalSelector3 = @selector(awakeFromNib);
        SEL swizzledSelector = @selector(YHBaseInit);
        SEL swizzledSelector2 = @selector(YHBaseInitWithFrame:);
        SEL swizzledSelector3 = @selector(YHBaseAwakeFromNib);


        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method originalMethod2 = class_getInstanceMethod(class, originalSelector2);
        Method originalMethod3 = class_getInstanceMethod(class, originalSelector3);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        Method swizzledMethod2 = class_getInstanceMethod(class, swizzledSelector2);
        Method swizzledMethod3 = class_getInstanceMethod(class, swizzledSelector3);
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        BOOL didAddMethod2 =
        class_addMethod(class,
                        originalSelector2,
                        method_getImplementation(swizzledMethod2),
                        method_getTypeEncoding(swizzledMethod2));
        BOOL didAddMethod3 =
        class_addMethod(class,
                        originalSelector3,
                        method_getImplementation(swizzledMethod3),
                        method_getTypeEncoding(swizzledMethod3));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));

        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
        if (didAddMethod2) {
            class_replaceMethod(class,
                                swizzledSelector2,
                                method_getImplementation(originalMethod2),
                                method_getTypeEncoding(originalMethod2));
        }else {
            method_exchangeImplementations(originalMethod2, swizzledMethod2);
        }
        if (didAddMethod3) {
            class_replaceMethod(class,
                                swizzledSelector3,
                                method_getImplementation(originalMethod3),
                                method_getTypeEncoding(originalMethod3));
        }else {
            method_exchangeImplementations(originalMethod3, swizzledMethod3);
        }
    });

}
/**
 *在這些方法中將你的字體名字換進去
 */
- (instancetype)YHBaseInit
{
    id __self = [self YHBaseInit];
    UIFont * font = [UIFont fontWithName:@"這里輸入你的字體名字" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}

-(instancetype)YHBaseInitWithFrame:(CGRect)rect{
    id __self = [self YHBaseInitWithFrame:rect];
    UIFont * font = [UIFont fontWithName:@"這里輸入你的字體名字" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
-(void)YHBaseAwakeFromNib{
    [self YHBaseAwakeFromNib];
    UIFont * font = [UIFont fontWithName:@"這里輸入你的字體名字" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }

}

@end

應用三: 數據統計

需求
跟蹤記錄APP中按鈕的點擊次數和頻率等數據

實現代碼

@implementation UIButton (Hook)

+ (void)load {

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        Class selfClass = [self class];

        SEL oriSEL = @selector(sendAction:to:forEvent:);
        Method oriMethod = class_getInstanceMethod(selfClass, oriSEL);

        SEL cusSEL = @selector(mySendAction:to:forEvent:);
        Method cusMethod = class_getInstanceMethod(selfClass, cusSEL);

        BOOL addSucc = class_addMethod(selfClass, oriSEL, method_getImplementation(cusMethod), method_getTypeEncoding(cusMethod));
        if (addSucc) {
            class_replaceMethod(selfClass, cusSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
        }else {
            method_exchangeImplementations(oriMethod, cusMethod);
        }

    });
}

- (void)mySendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [CountTool addClickCount];
    [self mySendAction:action to:target forEvent:event];
}

@end

總結

method swizzling方法的強大之處在于可以替換的系統的方法實現,添加自己想要的功能。上面的一些使用場景都是網上找的一些經典例子,大家可以舉一反三。

附上文章的原著。

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

推薦閱讀更多精彩內容