iOS Method Swizzle的祕密

什麼是Method Swizzling

方法交換(Method Swizzling),顧名思義就是將兩個方法的實現交換,即由原來的SEL(A)-IMP(A)、SEL(B)-IMP(B)對應關係變成了SEL(A)-IMP(B)、SEL(B)-IMP(A),以下圖:app

圖片描述

Method類型

Method類型是一個objc_method結構體指針,而結構體objc_method有三個成員,方法交換(Method Swizzling)的本質就是更改兩個成員method_typesmethod_impide

runtime.h源碼oop

/// An opaque type that represents a method in a class definition.
typedef struct objc_method *Method; // 本質是一個結構體

struct objc_method {
    SEL method_name;        // 方法名稱
    char *method_types;    // 參數和返回類型的描述字串
    IMP method_imp;         // 方法的具體的實現的指針
}

Method Swizzling 實現方式

好比咱們有一個控制器ParentViewController繼承於UIViewController,子控制器SubViewController繼承於ParentViewController。咱們想替換SubViewControllerviewDidAppearswizzle_viewDidAppear, 運行後先顯示ParentViewController頁面,而後點擊一個Button按鈕,push到SubViewController頁面,代碼以下:(頁面都是經過StoryBoard來構建的,請自行構建,我比較懶,這裏就只貼上代碼)編碼

@interface ParentViewController : UIViewController
@end

@implementation ParentViewController
- (void)viewDidAppear:(BOOL)animated{
    NSLog(@"%@ %s (IMP = ParentViewController viewDidAppear)",self, _cmd);
    [super viewDidAppear:animated];
}
@end


@interface SubViewController : ParentViewController
@end

@implementation SubViewController
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 原方法名和替換方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        // 原方法結構體和替換方法結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 若是當前類沒有原方法的實現IMP,先調用class_addMethod來給原方法添加默認的方法實現IMP
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {// 添加方法實現IMP成功後,修改替換方法結構體內的方法實現IMP和方法類型編碼TypeEncoding
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else { // 添加失敗,調用交互兩個方法的實現
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)swizzle_viewDidAppear:(BOOL)animated {
    NSLog(@"%@ %s (IMP = SubViewController swizzle_viewDidAppear)",self, _cmd);
    [self swizzle_viewDidAppear:animated];
}
@end

代碼說明:spa

dispatch_once 保證方法替換隻被執行一次指針

爲何要先調用類添加方法class_addMethod,而後判斷添加失敗後,再調用方法交換實現方法method_exchangeImplementations日誌

上面代碼中SubViewController是沒有Override父類的viewDidAppear。若是咱們直接調用method_exchangeImplementations會怎麼樣? 在這種狀況下,咱們試試code

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 原方法名和替換方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        // 原方法結構體和替換方法結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 調用交互兩個方法的實現
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

改爲上面的代碼,而後運行。哎喲,出錯了!Let me see see 什麼狀況對象

2018-05-07 21:38:56.615884+0800 TestiOS[3469:385923] <ParentViewController: 0x7f8f38c09aa0> viewDidAppear: (IMP = SubViewController swizzle_viewDidAppear)
2018-05-07 21:38:56.616189+0800 TestiOS[3469:385923] -[ParentViewController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7f8f38c09aa0
2018-05-07 21:38:56.621157+0800 TestiOS[3469:385923] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ParentViewController swizzle_viewDidAppear:]: unrecognized selector sent to instance 0x7f8f38c09aa0'
*** First throw call stack:
(
    0   CoreFoundation                      0x000000010e0fd1e6 __exceptionPreprocess + 294
    1   libobjc.A.dylib                     0x000000010d792031 objc_exception_throw + 48
    2   CoreFoundation                      0x000000010e17e784 -[NSObject(NSObject) doesNotRecognizeSelector:] + 132
    3   UIKit                               0x000000010e7a873b -[UIResponder doesNotRecognizeSelector:] + 295
    4   CoreFoundation                      0x000000010e07f898 ___forwarding___ + 1432
    5   CoreFoundation                      0x000000010e07f278 _CF_forwarding_prep_0 + 120
    6   TestiOS                             0x000000010ce903fb -[SubViewController swizzle_viewDidAppear:] + 75
    7   UIKit                               0x000000010e723ebf -[UIViewController _setViewAppearState:isAnimating:] + 697
    8   UIKit                               0x000000010e75ac53 -[UINavigationController viewDidAppear:] + 187
    9   UIKit                               0x000000010e723ebf -[UIViewController _setViewAppearState:isAnimating:] + 697
    10  UIKit                               0x000000010e726cfb __64-[UIViewController viewDidMoveToWindow:shouldAppearOrDisappear:]_block_invoke + 42
    11  UIKit                               0x000000010e72503f -[UIViewController _executeAfterAppearanceBlock] + 78
    12  UIKit                               0x000000010e58564f _runAfterCACommitDeferredBlocks + 634
    13  UIKit                               0x000000010e57477e _cleanUpAfterCAFlushAndRunDeferredBlocks + 388
    14  UIKit                               0x000000010e5942d7 __34-[UIApplication _firstCommitBlock]_block_invoke_2 + 155
    15  CoreFoundation                      0x000000010e09fb0c __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ + 12
    16  CoreFoundation                      0x000000010e0842db __CFRunLoopDoBlocks + 331
    17  CoreFoundation                      0x000000010e083a84 __CFRunLoopRun + 1284
    18  CoreFoundation                      0x000000010e08330b CFRunLoopRunSpecific + 635
    19  GraphicsServices                    0x0000000113267a73 GSEventRunModal + 62
    20  UIKit                               0x000000010e57a0b7 UIApplicationMain + 159
    21  TestiOS                             0x000000010ce9047f main + 111
    22  libdyld.dylib                       0x0000000111b56955 start + 1
)

控制檯打印ParentViewController找不到swizzle_viewDidAppear方法。 這是爲何呢?blog

第一行日誌顯示,ParentViewController是調用本身的方法viewDidAppear,經過打印_cmd輸出爲viewDidAppear可知道,可是執行的是SubViewControllerswizzle_viewDidAppear的方法實現IMP,在swizzle_viewDidAppear方法實現IMP裏又調用了[self swizzle_viewDidAppear:animated]這行代碼,可是此時self是ParentViewController實例對象,類方法列表里根本沒有swizzle_viewDidAppear方法,因此就致使找不到方法錯誤。說的我都以爲挺繞口的,千言萬語不如一張圖來的直觀,向下瞅:

圖片描述

如今明白了吧,方法交換(Method Swizzling)在子類沒有實現viewDidAppear方法的狀況下會交換父類的viewDidAppear的實現IMP,因此在swizzle_viewDidAppear實現IMP中調用swizzle_viewDidAppear方法會觸發doesNotRecognizeSelector找不到方法錯誤


若是咱們的子類SubViewController重寫Override了父類的viewDidAppear方法會怎麼樣?
咱們在SubViewController中重寫viewDidAppear方法

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 原方法名和替換方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        // 原方法結構體和替換方法結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 調用交互兩個方法的實現
        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)viewDidAppear:(BOOL)animated{
    NSLog(@"%@ %s (IMP = SubViewController viewDidAppear)",self, _cmd);
    [super viewDidAppear:animated];
}

- (void)swizzle_viewDidAppear:(BOOL)animated {
    NSLog(@"%@ %s (IMP = SubViewController swizzle_viewDidAppear)",self, _cmd);
    [self swizzle_viewDidAppear:animated];
}

改爲上面的代碼,而後運行。此次居然成功了

2018-05-07 21:41:58.467190+0800 TestiOS[3556:400520] <ParentViewController: 0x7fd5a060b730> viewWillAppear
2018-05-07 21:41:58.475185+0800 TestiOS[3556:400520] <ParentViewController: 0x7fd5a060b730> viewDidAppear: (IMP = ParentViewController viewDidAppear)
2018-05-07 21:42:08.772307+0800 TestiOS[3556:400520] <SubViewController: 0x7fd5a0405170> viewWillAppear
2018-05-07 21:42:09.312426+0800 TestiOS[3556:400520] <SubViewController: 0x7fd5a0405170> viewDidAppear: (IMP = SubViewController swizzle_viewDidAppear)
2018-05-07 21:42:09.312643+0800 TestiOS[3556:400520] <SubViewController: 0x7fd5a0405170> swizzle_viewDidAppear: (IMP = SubViewController viewDidAppear)
2018-05-07 21:42:09.312792+0800 TestiOS[3556:400520] <SubViewController: 0x7fd5a0405170> viewDidAppear: (IMP = ParentViewController viewDidAppear)

你逗我玩兒呢,怎麼子類實現viewDidAppear就行了。那是由於子類在檢查到本身有viewDidAppear方法就直接交換本身的viewDidAppear方法實現IMP,直接上圖,我不想廢話了
圖片描述

這下咱們明白了,若是直接調用方法method_exchangeImplementations來交換方法,須要考慮到子類有沒有相應的方法,若是沒有就要特殊處理,那豈不是太麻煩了。哈哈😁不用你瞎操心,蘋果有這個方法class_addMethod來幫助咱們解決


咱們把直接調用method_exchangeImplementations稍微作點修改

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        // 原方法名和替換方法名
        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
        
        // 原方法結構體和替換方法結構體
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        // 若是當前類沒有原方法的實現IMP,先調用class_addMethod來給原方法添加默認的方法實現IMP
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {// 添加方法實現IMP成功後,修改替換方法結構體內的方法實現IMP和方法類型編碼TypeEncoding
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else { // 添加失敗,調用交互兩個方法的實現
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

這樣咱們就不用關心子類有沒有實現viewDidAppear方法,方法class_addMethod在子類沒有實現viewDidAppear方法的時候,爲其添加swizzle_viewDidAppear方法實現IMP,原方法swizzle_viewDidAppear指向父類的viewDidAppear方法實現IMP

圖片描述

這就是爲何先要調用class_addMethod方法的緣由了,若是子類SubViewController實現了方法viewDidAppear,那麼class_addMethod方法會返回NO,意思子類存在viewDidAppear方法實現,就直接走method_exchangeImplementations方法交換

好了,就說到這裏吧,有問題請留言

參考:

相關文章
相關標籤/搜索