Runtime Method Swizzling
前言
在我學習runtime的method swizzling特性之前,有很多同事或者朋友經常在我耳邊說起swizzling特性,一個個在我面前說這個東西千萬不能用,會引起很多問題的。但是,在我學習完這一節的知識后,我終于明白其所以然。
學習完swizzling特性后,我很喜歡她。她就像一把雙刃劍,用好了可以帶你飛,亂用則會反傷。但是,我更相信她的強大,更相信自己夠能駕馭她!一起來學習吧!
Method Swizzling
試想一下,蘋果的源碼是閉源的,我們只有類名和類的屬性、方法等聲明,卻看不到實現,這時候我們若想改變其中一個方法的實現,有哪些方案呢?筆者想到的有以下幾種方案:
- 繼承于這個類,然后通過重寫方法(很常用,比如基類控制器,可以在視圖加載完成時做一些公共的配置等)
- 通過類別重寫方法,暴力搶先(此法太暴力,盡量不要這么做)
- swizzling(本文特講內容)
Swizzling原理
在Objective-C中調用一個方法,其實是向一個對象發送消息,而查找消息的唯一依據是selector的名字。所以,我們可以利用Objective-C的runtime機制,實現在運行時交換selector對應的方法實現以達到我們的目的。
每個類都有一個方法列表,存放著selector的名字和方法實現的映射關系。IMP有點類似函數指針,指向具體的Method實現。如果對Method不了解,請閱讀筆者的文章:runtime Method精講。
我們先看看SEL與IMP之間的關系圖(圖片來源 http://blog.csdn.net/yiyaaixuexi/article/details/9374411):
從上圖可以看出來,每一個SEL與一個IMP一一對應,正常情況下通過SEL可以查找到對應消息的IMP實現。
但是,現在我們要做的就是把鏈接線解開,然后連到我們自定義的函數的IMP上。當然,交換了兩個SEL的IMP,還是可以再次交換回來了。交換后變成這樣的,如下圖(圖片來源 http://blog.csdn.net/yiyaaixuexi/article/details/9374411):
從圖中可以看出,我們通過swizzling特性,將selectorC的方法實現IMPc與selectorN的方法實現IMPn交換了,當我們調用selectorC,也就是給對象發送selectorC消息時,所查找到的對應的方法實現就是IMPn而不是IMPc了。
在+load方法中交換
Swizzling應該在+load方法中實現,因為+load方法可以保證在類最開始加載時會調用。因為method swizzling的影響范圍是全局的,所以應該放在最保險的地方來處理是非常重要的。+load能夠保證在類初始化的時候一定會被加載,這可以保證統一性。試想一下,若是在實際時需要的時候才去交換,那么無法達到全局處理的效果,而且若是臨時使用的,在使用后沒有及時地使用swizzling將系統方法與我們自定義的方法實現交換回來,那么后續的調用系統API就可能出問題。
類文件在工程中,一定會加載,因此可以保證+load會被調用。
不要在+initialize中交換
+initialize是類第一次初始化時才會被調用,因為這個類有可能一直都沒有使用到,因此這個類可能永遠不會被調用。
類文件雖然在工程中,但是如果沒有任何地方調用過,那么是不會調用+initialize方法的。
使用dispatch_once保證只交換一次
方法交換應該要線程安全,而且保證只交換一次,除非只是臨時交換使用,在使用完成后又交換回來。
最常用的用法是在+load方法中使用dispatch_once來保證交換是安全的。因為swizzling會改變全局,我們需要在運行時采取相應的防范措施。保證原子操作就是一個措施,確保代碼即使在多線程環境下也只會被執行一次。而diapatch_once就提供這些保障,因此我們應該將其加入到swizzling的使用標準規范中。
通用交換IMP寫法
網上有很多的版本,但是有很多是不全面的,考慮的范圍不夠全面。下面我們來寫一個通用的寫法,現在擴展到NSObject中,因為NSObject是根類,這樣其它類都可以使用了:
@interface NSObject (Swizzling) + (void)swizzleSelector:(SEL)originalSelectorwithSwizzledSelector:(SEL)swizzledSelector; @end #import "NSObject+Swizzling.h" #import <objc/runtime.h> // 實現代碼如下 @implementation NSObject (Swizzling) + (void)swizzleSelector:(SEL)originalSelectorwithSwizzledSelector:(SEL)swizzledSelector { Class class = [self class]; Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // 若已經存在,則添加會失敗 BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // 若原來的方法并不存在,則添加即可 if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } @end
因為方法可能不是在這個類里,可能是在其父類中才有實現,因此先嘗試添加方法的實現,若添加成功了,則直接替換一下實現即可。若添加失敗了,說明已經存在這個方法實現了,則只需要交換這兩個方法的實現就可以了。
盡量使用 method_exchangeImplementations 函數來交換,因為它是原子操作的,線程安全。盡量不要自己手動寫這樣的代碼:
IMP imp1 = method_getImplementation(m1); IMP imp2 = method_getImplementation(m2); method_setImplementation(m1, imp2); method_setImplementation(m2, imp1);
雖然 method_exchangeImplementations 函數的本質也是這么寫法,但是它內部做了線程安全處理。
當然,我們也可以寫成C語言函數,而不是歸屬于類的方法:
// C語言版 void swizzleSelector(Class class, SEL originalSelector, SEL swizzledSelector) { Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); // 若已經存在,則添加會失敗 BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // 若原來的方法并不存在,則添加即可 if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }
簡單使用swizzling
最簡單的方法實現交換如下:
Method originalMethod = class_getInstanceMethod([NSArray class], @selector(lastObject)); Method newMedthod = class_getInstanceMethod([NSArray class], NSSelectorFromString(@"hdf_lastObject")); method_exchangeImplementations(originalMethod, newMedthod); // NSArray提供了這樣的實現 - (id)hdf_lastObject { if (self.count == 0) { NSLog(@"%s 數組為空,直接返回nil", __FUNCTION__); return nil; } return [self hdf_lastObject]; }
看到 hdf_lastObject 這個方法遞歸調用自己了嗎?為什么不是調用 return [self lastObject] ?因為我們交換了方法的實現,那么系統在調用 lastObject 方法是,找的是 hdf_lastObject 方法的實現,而手動調用 hdf_lastObject 方法時,會調用 lastObject 方法的實現。不清楚?回到前面看一看那個交換IMP的圖吧!
我們通過使用swizzling只是為了添加個打印?當然不是,我們還可以做很多事的。比如,上面我們還做了防崩潰處理。
NSMutableArray擴展交換處理崩潰
還記得那些調用數組的 addObject: 方法加入一個nil值是的崩潰情景嗎?還記得 [__NSPlaceholderArray initWithObjects:count:] 因為有nil值而崩潰的提示嗎?還記得調用 objectAtIndex: 時出現崩潰提示empty數組問題嗎?那么通過swizzling特性,我們可以做到不讓它崩潰,而只是打印一些有用的日志信息。
我們先來看看NSMutableArray的擴展實現:
#import "NSMutableArray+Swizzling.h" #import <objc/runtime.h> #import "NSObject+Swizzling.h" @implementation NSMutableArray (Swizzling) + (void)load { static dispatch_once_tonceToken; dispatch_once(&onceToken, ^{ [selfswizzleSelector:@selector(removeObject:) withSwizzledSelector:@selector(hdf_safeRemoveObject:)]; [objc_getClass("__NSArrayM")swizzleSelector:@selector(addObject:) withSwizzledSelector:@selector(hdf_safeAddObject:)]; [objc_getClass("__NSArrayM")swizzleSelector:@selector(removeObjectAtIndex:) withSwizzledSelector:@selector(hdf_safeRemoveObjectAtIndex:)]; [objc_getClass("__NSArrayM")swizzleSelector:@selector(insertObject:atIndex:) withSwizzledSelector:@selector(hdf_insertObject:atIndex:)]; [objc_getClass("__NSPlaceholderArray")swizzleSelector:@selector(initWithObjects:count:)withSwizzledSelector:@selector(hdf_initWithObjects:count:)]; [objc_getClass("__NSArrayM")swizzleSelector:@selector(objectAtIndex:)withSwizzledSelector:@selector(hdf_objectAtIndex:)]; }); } - (instancetype)hdf_initWithObjects:(const id _Nonnull__unsafe_unretained*)objectscount:(NSUInteger)cnt { BOOL hasNilObject = NO; for (NSUInteger i = 0; i < cnt; i++) { if ([objects[i]isKindOfClass:[NSArray class]]) { NSLog(@"%@", objects[i]); } if (objects[i] == nil) { hasNilObject = YES; NSLog(@"%s object at index %lu is nil, it will be filtered", __FUNCTION__, i); //#if DEBUG // // 如果可以對數組中為nil的元素信息打印出來,增加更容易讀懂的日志信息,這對于我們改bug就好定位多了 // NSString *errorMsg = [NSString stringWithFormat:@"數組元素不能為nil,其index為: %lu", i]; // NSAssert(objects[i] != nil, errorMsg); //#endif } } // 因為有值為nil的元素,那么我們可以過濾掉值為nil的元素 if (hasNilObject) { id __unsafe_unretainednewObjects[cnt]; NSUInteger index = 0; for (NSUInteger i = 0; i < cnt; ++i) { if (objects[i] != nil) { newObjects[index++] = objects[i]; } } return [selfhdf_initWithObjects:newObjectscount:index]; } return [selfhdf_initWithObjects:objectscount:cnt]; } - (void)hdf_safeAddObject:(id)obj { if (obj == nil) { NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__); } else { [selfhdf_safeAddObject:obj]; } } - (void)hdf_safeRemoveObject:(id)obj { if (obj == nil) { NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__); return; } [selfhdf_safeRemoveObject:obj]; } - (void)hdf_insertObject:(id)anObjectatIndex:(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 { [selfhdf_insertObject:anObjectatIndex:index]; } } - (id)hdf_objectAtIndex:(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 [selfhdf_objectAtIndex:index]; } - (void)hdf_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; } [selfhdf_safeRemoveObjectAtIndex:index]; } @end
然后,我們測試nil值的情況,是否還會崩潰呢?
NSMutableArray *array = [@[@"value", @"value1"]mutableCopy]; [arraylastObject]; [arrayremoveObject:@"value"]; [arrayremoveObject:nil]; [arrayaddObject:@"12"]; [arrayaddObject:nil]; [arrayinsertObject:nilatIndex:0]; [arrayinsertObject:@"sdf"atIndex:10]; [arrayobjectAtIndex:100]; [arrayremoveObjectAtIndex:10]; NSMutableArray *anotherArray = [[NSMutableArray alloc]init]; [anotherArrayobjectAtIndex:0]; NSString *nilStr = nil; NSArray *array1 = @[@"ara", @"sdf", @"dsfdsf", nilStr]; NSLog(@"array1.count = %lu", array1.count); // 測試數組中有數組 NSArray *array2 = @[@[@"12323", @"nsdf", nilStr], @[@"sdf", @"nilsdf", nilStr, @"sdhfodf"]];
哈哈,都不崩潰了,而且還打印出崩潰原因。是不是很神奇?如果充分利用這種特性,是不是可以給我們帶來很多便利之處?
上面只是swizzling的一種應用場景而已。其實利用swizzling特性還可以做很多事情的,比如處理按鈕重復點擊問題等。
源代碼
大家可以到筆者的GITHUB下載對應的源代碼來測試: https://github.com/CoderJackyHuang/RuntimeDemo
喜歡就隨手給個star吧!
關注我
如果在使用過程中遇到問題,或者想要與我交流,可加入有問必答 QQ群: 324400294
關注微信公眾號: iOSDevShares
關注新浪微博賬號:標哥Jacky
標哥的GITHUB地址: CoderJackyHuang
支持并捐助
如果您覺得文章對您很有幫忙,希望得到您的支持。您的捐肋將會給予我最大的鼓勵,感謝您的支持!
支付寶捐助 | 微信捐助 |
---|---|
![]() |
![]() |