什麼是Method Swizzling,在iOS開發中它有什麼作用?
簡單來說我們主要是使用Method Swizzling來把系統的方法交換為我們自己的方法,從而給系統方法新增一些我們想要的功能。已經有很多文章從各個角度解釋Method Swizzling的涵義甚至實現機制,該篇文章主要列舉Method Swizzling在開發中的一些現實用例。希望閱讀文章的朋友們也可以提供一些文中尚未舉出的例子。
在列舉之前,我們可以將Method Swizzling功能封裝為類方法,作為NSObject的類別,這樣我們後續呼叫也會方便些。
1 2 3 4 5 6 |
#import #import @interface NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#import "NSObject+Swizzling.h" @implementation NSObject (Swizzling) + (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 |
例項一:替換ViewController生命週期方法
App跳轉到某具有網路請求的介面時,為了使用者體驗效果常會新增載入欄或進度條來顯示當前請求情況或進度。這種介面都會存在這樣一個問題,在請求較慢時,使用者手動退出介面,這時候需要去除載入欄。
當然可以依次在每個介面的viewWillDisappear方法中新增去除方法,但如果類似的介面過多,一味的複製貼上也不是方法。這時候就能體現Method Swizzling的作用了,我們可以替換系統的viewWillDisappear方法,使得每當執行該方法時即自動去除載入欄。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#import "UIViewController+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIViewController (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)]; }); } - (void)sure_viewWillDisappear:(BOOL)animated { [self sure_viewWillDisappear:animated]; [SVProgressHUD dismiss]; } |
程式碼如上,這樣就不用考慮介面是否移除載入欄的問題了。
例項二:解決獲取索引、新增、刪除元素越界崩潰問題
對於NSArray、NSDictionary、NSMutableArray、NSMutableDictionary不免會進行索引訪問、新增、刪除元素的操作,越界問題也是很常見,這時我們可以通過Method Swizzling解決這些問題,越界給予提示防止崩潰。
這裡以NSMutableArray為例說明
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
#import "NSMutableArray+Swizzling.h" #import "NSObject+Swizzling.h" @implementation NSMutableArray (Swizzling) + (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 = self.count) { NSLog(@"%s index out of bound", __FUNCTION__); return; } [self safeRemoveObjectAtIndex:index]; } @end |
對應大家可以舉一反三,相應的實現新增、刪除等,以及NSArray、NSDictionary等操作,因程式碼篇幅較大,這裡就不一一書寫了。
這裡沒有使用self來呼叫,而是使用objc_getClass(“__NSArrayM”)來呼叫的。因為NSMutableArray的真實類只能通過後者來獲取,而不能通過[self class]來獲取,而method swizzling只對真實的類起作用。這裡就涉及到一個小知識點:類簇。補充以上物件對應類簇表。
例項三:防止按鈕重複暴力點選
程式中大量按鈕沒有做連續響應的校驗,連續點選出現了很多不必要的問題,例如發表帖子操作,使用者手快點選多次,就會導致同一帖子釋出多次。
1 2 3 4 5 6 7 8 9 |
#import //預設時間間隔 #define defaultInterval 1 @interface UIButton (Swizzling) //點選間隔 @property (nonatomic, assign) NSTimeInterval timeInterval; //用於設定單個按鈕不需要被hook @property (nonatomic, assign) BOOL isIgnore; @end |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIButton (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)]; }); } - (NSTimeInterval)timeInterval{ return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setTimeInterval:(NSTimeInterval)timeInterval{ objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } //當按鈕點選事件sendAction 時將會執行sure_SendAction - (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ if (self.isIgnore) { //不需要被hook [self sure_SendAction:action to:target forEvent:event]; return; } if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { self.timeInterval =self.timeInterval == 0 ?defaultInterval:self.timeInterval; if (self.isIgnoreEvent){ return; }else if (self.timeInterval > 0){ [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval]; } } //此處 methodA和methodB方法IMP互換了,實際上執行 sendAction;所以不會死迴圈 self.isIgnoreEvent = YES; [self sure_SendAction:action to:target forEvent:event]; } //runtime 動態繫結 屬性 - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{ // 注意BOOL型別 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,否則set方法會賦值出錯 objc_setAssociatedObject(self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnoreEvent{ //_cmd == @select(isIgnore); 和set方法裡一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsIgnore:(BOOL)isIgnore{ // 注意BOOL型別 需要用OBJC_ASSOCIATION_RETAIN_NONATOMIC 不要用錯,否則set方法會賦值出錯 objc_setAssociatedObject(self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnore{ //_cmd == @select(isIgnore); 和set方法裡一致 return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)resetState{ [self setIsIgnoreEvent:NO]; } @end |
例項四:全域性更換控制元件初始效果
以UILabel為例,在專案比較成熟的基礎上,應用中需要引入新的字型,需要更換所有Label的預設字型,但是同時,對於一些特殊設定了字型的label又不需要更換。乍看起來,這個問題確實十分棘手,首先專案比較大,一個一個設定所有使用到的label的font工作量是巨大的,並且在許多動態展示的介面中,可能會漏掉一些label,產生bug。其次,專案中的label來源並不唯一,有用程式碼建立的,有xib和storyBoard中的,這也將浪費很大的精力。這時Method Swizzling可以解決此問題,避免繁瑣的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#import "UILabel+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UILabel (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)]; [self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)]; [self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)]; }); } - (instancetype)sure_Init{ id __self = [self sure_Init]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } return __self; } -(instancetype)sure_InitWithFrame:(CGRect)rect{ id __self = [self sure_InitWithFrame:rect]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } return __self; } -(void)sure_AwakeFromNib{ [self sure_AwakeFromNib]; UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize]; if (font) { self.font=font; } } @end |
這一例項個人認為使用率可能不高,對於產品的設計這些點都是已經確定好的,更改的機率很低。況且我們也可以使用appearance來進行統一設定。
例項五:App熱修復
因為AppStore上線稽核時間較長,且如果線上上版本出現bug修復起來也是很困難,這時App熱修復就可以解決此問題。熱修復即在不更改線上版本的前提下,對線上版本進行更新甚至新增模組。國內比較好的熱修復技術:JSPatch。JSPatch能做到通過JS呼叫和改寫OC方法最根本的原因是Objective-C是動態語言,OC上所有方法的呼叫/類的生成都通過Objective-C Runtime在執行時進行,我們可以通過類名/方法名反射得到相應的類和方法,進而替換出現bug的方法或者新增方法等。bang的部落格上有詳細的描述有興趣可以參考,這裡就不贅述了。
暫時寫到這裡,部分內容來源於網路,後續還會更新。
最後,還是希望看過的朋友們可以提供一些自己開發中的例項加以補充。