在我們開始接觸到runtime之后,我們經(jīng)常能見到Method-Swizzling這個(gè)被稱作 黑魔法 的東西,那么到底什么是Method-Swizzling 怎么使用、使用過程中又有哪些坑點(diǎn),我們今天來探究一下
什么是Method-Swizzling
字面上: 方法調(diào)配 、方法交換
實(shí)現(xiàn)上: 我們常說的方法、方法名sel 通過其指向的IMP指針(方法的實(shí)現(xiàn)),通過這樣的對(duì)應(yīng)關(guān)系在使用時(shí)去調(diào)用對(duì)應(yīng)的方法。
這里通過 objc/runtime.h中提供的api:method_exchangeImplementations(Method m1, Method m2)來交換IMP,通過這樣的方法重新綁定sel和 IMP對(duì)應(yīng)關(guān)系。

Method-Swizzling有什么作用
那么上面這樣的方法交換在我們開發(fā)中到底有什么作用?
舉個(gè)栗子??
我們?cè)陂_發(fā)過程中一直都會(huì)有一個(gè)無法避開的需求--埋點(diǎn)比如記錄用戶在操作APP期間到底訪問了哪些頁(yè)面。
- 最基礎(chǔ)的,我們?cè)诿總€(gè)VC的
ViewDidLoad中去預(yù)留方法 - 高一點(diǎn)的,我們實(shí)現(xiàn)一個(gè)VC的基類,在基類中我們對(duì)
ViewDidLoad重寫 - 再或者,我們可以通過
category去實(shí)現(xiàn)
但這些我們都需要寫大量代碼,如果2、3方法是在項(xiàng)目后期去做時(shí),也需要去調(diào)整很多類信息。
甚至,我們有可能是需要再SDK中,在不接觸頁(yè)面代碼的情況下去實(shí)現(xiàn)這個(gè)埋點(diǎn)的需求,我們就可以通過Method-Swizzling 去直接hook ViewDidLoad方法,直接實(shí)現(xiàn)埋點(diǎn)。
這也就是AOP(Aspect Oriented Programming) 面向切面編程 思想
通過預(yù)編譯方式和運(yùn)行期間動(dòng)態(tài)代理實(shí)現(xiàn)程序功能的統(tǒng)一維護(hù)的一種技術(shù)。
AOP是OOP的延續(xù),是軟件開發(fā)中的一個(gè)熱點(diǎn),也是Spring框架中的一個(gè)重要內(nèi)容,是函數(shù)式編程的一種衍生范型。利用AOP可以對(duì)業(yè)務(wù)邏輯的各個(gè)部分進(jìn)行隔離,從而使得業(yè)務(wù)邏輯各部分之間的耦合度降低,提高程序的可重用性,同時(shí)提高了開發(fā)的效率。(摘自百度百科)
除了上面說的埋點(diǎn),還有一下crash收集我們也可以通過這樣的方法實(shí)現(xiàn)。
Method-Swizzling具體使用 及 坑點(diǎn)
接下來,我們實(shí)戰(zhàn)看一下Method-Swizzling的具體應(yīng)用
/// 實(shí)例方法交換
/// @param cls 方法交換的類
/// @param oriSEL 需要被交換的方法編號(hào)
/// @param swizzledSEL 用來交換的方法編號(hào)
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能為空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
這是最基礎(chǔ)的Method-Swizzling應(yīng)用,其中會(huì)有一些問題,比如
重復(fù)調(diào)用問題
因?yàn)?code>+(void)load方法調(diào)用的最早,所以一般我們放在其中去做方法交換。而+(void)load并不能確定只調(diào)用一次,如果發(fā)生多次調(diào)用,那么方法交換也會(huì)發(fā)生多次,IMP就會(huì)反復(fù)的交換。
解決方案
通過單例的思路,我們通過 dispatch_once(&onceToken, ^{})去做數(shù)據(jù)保護(hù),避免反復(fù)橫跳的發(fā)生
或者我們可以通過手動(dòng)觸發(fā),只調(diào)用一次
子類交換了繼承于父類方法
- (void)viewDidLoad {
[super viewDidLoad];
///LGStudent 繼承于 LGPerson
LGStudent *s = [[LGStudent alloc] init];
[s personInstanceMethod];
LGPerson *p = [[LGPerson alloc] init];
[p personInstanceMethod];
}
#import "LGPerson.h"
@implementation LGPerson
- (void)personInstanceMethod{
NSLog(@"person對(duì)象方法:%s",__func__);
}
+ (void)personClassMethod{
NSLog(@"person類方法:%s",__func__);
}
@end
@implementation LGStudent (LG)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
});
}
- (void)lg_studentInstanceMethod{
NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}
運(yùn)行結(jié)果

我們?cè)谥暗奶剿髦芯椭?,?dāng)子類沒有實(shí)現(xiàn)方法時(shí),方法會(huì)遍歷到父類的方法列表中返回IMP,而Method-Swizzling是直接修改的IMP,所以被交換的其實(shí)就是父類的方法
那么問題就來了,如果交換后的方法父類本身不存在,那就找不到對(duì)應(yīng)方法,就會(huì)出現(xiàn)崩潰。
解決方案
+ (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);
//加一層保護(hù)措施,如果添加成功,則表示該方法不存在于本類,而是存在于父類中,不能交換父類的方法,否則父類的對(duì)象調(diào)用該方法會(huì)crash;添加失敗則表示本類存在該方法
BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
if (success) {// 自己沒有 - 交換 - 沒有父類進(jìn)行處理 (重寫一個(gè))
//再將原有的實(shí)現(xiàn)替換到swizzledMethod方法上,從而實(shí)現(xiàn)方法的交換,并且未影響到父類方法的實(shí)現(xiàn)
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{ // 自己有
method_exchangeImplementations(oriMethod, swiMethod);
}
}
在交換前判斷下是否是自己的方法,如果不是,那就改為替換,不直接交換,避免影響父類的方法實(shí)現(xiàn)。
探究 在交換的方法中再次調(diào)用原方法,是否會(huì)發(fā)生遞歸
// 是否遞歸
- (void)lg_studentInstanceMethod{
[self lg_studentInstanceMethod];
NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}
答案是不會(huì)。

此時(shí)lg_studentInstanceMethod 內(nèi)部實(shí)現(xiàn)是通過personInstanceMethod 方法名去調(diào)用的,而中間lg_studentInstanceMethod的方法,則指向了personInstanceMethod的IMP,并不會(huì)行成一個(gè)遞歸循環(huán)。
但如果改為
// 是否遞歸
- (void)lg_studentInstanceMethod{
[self personInstanceMethod];
NSLog(@"LGStudent分類添加的lg對(duì)象方法:%s",__func__);
}
還是會(huì)一直在遞歸循環(huán)中,所以在這里要清楚理解SEL和IMP的關(guān)系和區(qū)別