相信你們對於Method Swizzling並不陌生,在平時的開發中多多少少都有些使用,這也是Runtime的開發應用中比較普遍的用法。可是它確確實實是個黑魔法,一有不慎,就是一座天坑。本章就來研究一下它,讓他變成白魔法編程
Method Swizzling
(方法交換),顧名思義,就是將兩個方法的實現交換,即由原來的A-AImp
、B-BImp
對應關係變成了A-BImp
、B-AImp
。設計模式
每一個類都維護一個方法Method
列表,Method
則包含SEL
和其對應IMP
的信息,方法交換作的事情就是把SEL
和IMP
的對應關係斷開,並和新的IMP
生成對應關係。數組
Method Swizzing
是發生在運行時的,主要用於在運行時將兩個Method進行交換,咱們能夠將Method Swizzling
代碼寫到任何地方,可是隻有在這段Method Swilzzling
代碼執行完畢以後互換才起做用。bash
Method Swizzling
是OC動態性的最好詮釋,深刻地去學習並理解其特性,將有助於咱們在業務量不斷增大的同時還能保持代碼的低耦合度,下降維護的工做量和難度。服務器
能夠用圖來更好的解釋一下app
交換前: 框架
//獲取經過SEL獲取一個方法
class_getInstanceMethod
複製代碼
//獲取一個方法的實現
method_getImplementation
複製代碼
//獲取一個OC實現的編碼類型
method_getTypeEncoding
複製代碼
//給方法添加實現
class_addMethod
複製代碼
//用一個方法的實現替換另外一個方法的實現
class_replaceMethod
複製代碼
//交換兩個方法的實現
method_exchangeImplementations
複製代碼
+load
方法中實現,這樣能夠保證方法必定會調用且不會出現異常。dispatch_once
來執行方法交換,這樣能夠保證只運行一次。第一個坑點比較簡單,就是咱們在load
中交換完方法後,不作處理的話,若是再去調用load
,方法IMP
會又被交換回來,致使交換不成功。函數
解決的方法也比較簡單,在上述的注意事項1中已經說過,使用單例模式來交換方法,保證方法的交換隻執行一次post
咱們能夠經過一個例子來實現,建立父類LGPerson
,子類LGStudent
和分類LGStudent+LG
來進行方法的交換。學習
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person對象方法:%s",__func__);
}
@end
**************************************************************
@interface LGStudent : LGPerson
@end
@implementation LGStudent
@end
**************************************************************
複製代碼
首先進行普通的方法交換,並在VC裏面正常調用,根據打印結果,能夠發現咱們在子類交換了父類的方法後,沒有產生崩潰,而且子類的分類中交換的方法也正常執行了
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分類添加的lg對象方法:%s",__func__);
}
@end
*******************************調用*******************************
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
}
*******************************打印結果*******************************
2020-01-20 10:45:31.809408+0800 006---Method-Swizzling坑[81429:20470219] person對象方法:-[LGPerson personInstanceMethod]
2020-01-20 10:45:31.809568+0800 006---Method-Swizzling坑[81429:20470219] LGStudent分類添加的lg對象方法:-[LGStudent(LG) lg_studentInstanceMethod]
複製代碼
可是,若是咱們在調用的時候,父類自己再調用一下這個方法的話,就會出現崩潰,緣由也比較清楚,就是子類將此方法交換了,父類並無交換後方法的IMP,因此會出現找不到方法的崩潰
- (void)viewDidLoad {
[super viewDidLoad];
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
複製代碼
一句話總結就是:若是該方法本身沒有,則先給本身添加要交換的方法。以後再父類原方法IMP指向交換的方法
在交換方法的時候,先嚐試添加一下原方法到類中,並將IMP指向交換的方法
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
// oriSEL personInstanceMethod
// swizzledSEL lg_studentInstanceMethod
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 嘗試添加
// ✅對應關係:personInstanceMethod(sel) - lg_studentInstanceMethod(imp)
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 本身沒有 - 交換 - 沒有父類進行處理 (重寫一個)
//✅lg_studentInstanceMethod (swizzledSEL) - personInstanceMethod(imp)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 本身有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
複製代碼
仍是使用上述例子,若是LGStudent
有一個方法- (void)helloword
只有生命,沒有實現。
就算咱們使用了上述的解決方法,添加了方法,可是因爲原方法找不到,爲nil。因此會形成死循環調用
咱們能夠經過判斷原方法是否存在,並添加一個一個空實現來解決這個坑點。以後再進行判斷原方法進行交換,這樣就能完美解決了
具體代碼以下
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能爲空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
✅// 在oriMethod爲nil時,替換後將swizzledSEL複製一個不作任何事的空實現,代碼以下:
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
✅// 通常交換方法: 交換本身有的方法 -- 走下面 由於本身有意味添加方法失敗
✅// 交換本身沒有實現的方法:
✅// 首先第一步:會先嚐試給本身添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
✅// 而後再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
複製代碼
如下swizzling方法的具體封裝,和上述代碼中同樣
在 iOS 開發中最多見的三種埋點,就是對頁面進入次數、頁面停留時間、點擊事件的埋點。這些均可以經過Method Swizzling來實現。
下面的例子中,咱們經過交換UIViewController
中viewWillAppear
和viewWillDisappear
的方法,來實現了進入界面和退出界面的統計,並記錄了相關的類名,經過映射的關係,就能夠清楚的知道用戶的行爲了
@implementation UIViewController (logger)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
✅// 經過 @selector 得到被替換和替換方法的 SEL,做爲 SMHook:hookClass:fromeSelector:toSelector 的參數傳入
SEL fromSelectorAppear = @selector(viewWillAppear:);
SEL toSelectorAppear = @selector(hook_viewWillAppear:);
[SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear];
SEL fromSelectorDisappear = @selector(viewWillDisappear:);
SEL toSelectorDisappear = @selector(hook_viewWillDisappear:);
[SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear];
});
}
- (void)hook_viewWillAppear:(BOOL)animated {
✅// 先執行插入代碼,再執行原 viewWillAppear 方法
[self insertToViewWillAppear];
[self hook_viewWillAppear:animated];
}
- (void)hook_viewWillDisappear:(BOOL)animated {
✅// 執行插入代碼,再執行原 viewWillDisappear 方法
[self insertToViewWillDisappear];
[self hook_viewWillDisappear:animated];
}
- (void)insertToViewWillAppear {
✅// 在 ViewWillAppear 時進行日誌的埋點
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
- (void)insertToViewWillDisappear {
✅// 在 ViewWillDisappear 時進行日誌的埋點
[[[[SMLogger create]
message:[NSString stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
classify:ProjectClassifyOperation]
save];
}
@end
複製代碼
那麼點擊方法,咱們也能夠經過運行時方法替換的方式進行無侵入埋點。
這裏最主要的工做是,找到這個點擊事件的方法 sendAction:to:forEvent:
,而後在 +load()
方法替換成爲你定義的方法。完整代碼實現以下:
和 UIViewController
生命週期埋點不一樣的是,UIButton
在一個視圖類中可能有多個不一樣的繼承類,相同 UIButton
的子類在不一樣視圖類的埋點也要區別開。因此,咱們須要經過 「action 選擇器名 NSStringFromSelector(action)」 +「視圖類名 NSStringFromClass([target class])」
組合成一個惟一的標識,來進行埋點記錄
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
✅// 經過 @selector 得到被替換和替換方法的 SEL,做爲 SMHook:hookClass:fromeSelector:toSelector 的參數傳入
SEL fromSelector = @selector(sendAction:to:forEvent:);
SEL toSelector = @selector(hook_sendAction:to:forEvent:);
[SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
});
}
- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
[self insertToSendAction:action to:target forEvent:event];
[self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
✅// 日誌記錄
if ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
NSString *actionString = NSStringFromSelector(action);
NSString *targetName = NSStringFromClass([target class]);
[[[SMLogger create] message:[NSString stringWithFormat:@"%@ %@",targetName,actionString]] save];
}
}
複製代碼
除了 UIViewController
、UIButton
控件之外,Cocoa 框架的其餘控件均可以使用這種方法來進行無侵入埋點。以 Cocoa 框架中最複雜的 UITableView
控件爲例,你可使用 hook setDelegate
方法來實現無侵入埋點。另外,對於 Cocoa 框架中的手勢事件(Gesture Event
),咱們也能夠經過 hook initWithTarget:action:
方法來實現無侵入埋點。
這個例子我相信平時在開發中,你們都用到過,由於數組越界等是最容易形成crash
的一種方式,並且通常崩潰起來比較嚴重,因此咱們必定要避免的
在iOS中NSNumber、NSArray、NSDictionary
等這些類都是類簇(Class Clusters),一個NSArray
的實現可能由多個類組成。因此若是想對NSArray
進行Swizzling
,必須獲取到其真身進行Swizzling
,直接對NSArray進行操做是無效的。這是由於Method Swizzling對NSArray這些的類簇是不起做用的。
由於這些類簇類,實際上是一種抽象工廠的設計模式。抽象工廠內部有不少其它繼承自當前類的子類,抽象工廠類會根據不一樣狀況,建立不一樣的抽象對象來進行使用。例如咱們調用NSArray
的objectAtIndex:
方法,這個類會在方法內部判斷,內部建立不一樣抽象類進行操做。
因此若是咱們對NSArray
類進行Swizzling
操做其實只是對父類進行了操做,在NSArray
內部會建立其餘子類來執行操做,真正執行Swizzling
操做的並非NSArray
自身,因此咱們應該對其「真身」進行操做。
下面列舉了NSArray
和NSDictionary
本類的類名,能夠經過Runtime函數取出本類:
下面是一個常見的例子
@implementation NSArray (CrashHandle)
✅// 若是下面代碼不起做用,形成這個問題的緣由大多都是其調用了super load方法。在下面的load方法中,不該該調用父類的load方法。這樣會致使方法交換無效
+ (void)load {
Method originMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(wy_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
✅// 爲了不和系統的方法衝突,我通常都會在swizzling方法前面加前綴
- (id)wy_objectAtIndex:(NSUInteger)index {
✅// 判斷下標是否越界,若是越界就進入異常攔截
if (self.count-1 < index) {
@try {
return [self cm_objectAtIndex:index];
}
@catch (NSException *exception) {
✅// 在崩潰後會打印崩潰信息。若是是線上,能夠在這裏將崩潰信息發送到服務器
NSLog(@"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
NSLog(@"%@", [exception callStackSymbols]);
return nil;
}
@finally {}
} ✅// 若是沒有問題,則正常進行方法調用
else {
return [self cm_objectAtIndex:index];
}
}
**************************調用******************************
- (void)viewDidLoad {
[super viewDidLoad];
// 測試代碼
NSArray *array = @[@0, @1, @2, @3];
[array objectAtIndex:3];
//原本要奔潰的,可是沒有,打印出了信息
[array objectAtIndex:4];
}
複製代碼
以上的兩個例子,只是開發中經常使用的,還有不少其餘的應用,就須要根據需求來不斷調整了。這些都屬於AOP面向切面編程的一個實際應用,Method Swizzling也是其在iOS開發中應用的最經常使用的一種AOP思想