方法交換(Method Swizzling),顧名思義就是將兩個方法的實現交換,即由原來的SEL(A)-IMP(A)、SEL(B)-IMP(B)對應關係變成了SEL(A)-IMP(B)、SEL(B)-IMP(A),以下圖:app
Method類型是一個objc_method
結構體指針,而結構體objc_method
有三個成員,方法交換(Method Swizzling)的本質就是更改兩個成員method_types
和method_imp
ide
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; // 方法的具體的實現的指針 }
好比咱們有一個控制器ParentViewController
繼承於UIViewController
,子控制器SubViewController
繼承於ParentViewController
。咱們想替換SubViewController
的viewDidAppear
爲swizzle_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
可知道,可是執行的是SubViewController
的swizzle_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
方法交換
好了,就說到這裏吧,有問題請留言