因?yàn)镺bjective-C的runtime機(jī)制, Method Swizzling這個(gè)黑魔法解決了我們實(shí)際開發(fā)中諸多常規(guī)手段所無法解決的問題, 比如代碼的插樁,Hook,Patch等等. 我們首先看看常規(guī)的Method Swizzling是怎樣用的, NSHipster有一篇介紹基本用法的文章Method Swizzling, 我們就先以這篇文章中的示例開始說起吧:
#import
@implementation UIViewController (Tracking)
+ (void)load {
staticdispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Classclass= [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xby_viewWillAppear:);
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);
}
});
}
#pragma mark - Method Swizzling
- (void)xby_viewWillAppear:(BOOL)animated {
[self xby_viewWillAppear:animated];//因?yàn)榉椒ㄌ鎿Q,這里相當(dāng)于調(diào)用的是系統(tǒng)的viewWillAppear,所以自然也就不會(huì)造成循環(huán)調(diào)用的問題
NSLog(@"viewWillAppear: %@", self);
}
@end
簡(jiǎn)要說明一下以上代碼的幾個(gè)重點(diǎn):
通過在Category的+ (void)load方法中添加Method Swizzling的代碼,在類初始加載時(shí)自動(dòng)被調(diào)用,load方法按照父類到子類,類自身到Category的順序被調(diào)用.
在dispatch_once中執(zhí)行Method Swizzling是一種防護(hù)措施,以保證代碼塊只會(huì)被執(zhí)行一次并且線程安全,不過此處并不需要,因?yàn)楫?dāng)前Category中的load方法并不會(huì)被多次調(diào)用.
嘗試先調(diào)用class_addMethod方法,以保證即便originalSelector只在父類中實(shí)現(xiàn),也能達(dá)到Method Swizzling的目的.
xby_viewWillAppear:方法中[self xby_viewWillAppear:animated];
代碼并不會(huì)造成死循環(huán),因?yàn)镸ethod Swizzling之后,調(diào)用xby_viewWillAppear:實(shí)際執(zhí)行的代碼已經(jīng)是原來viewWillAppear中的代碼了.
其實(shí)以上的代碼也可以簡(jiǎn)寫為以下:
+ (void)load {
Classclass= [self class];
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xby_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
if(!originalMethod || !swizzledMethod) {
return;
}
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
// 這兒的先后順序是有講究的,如果先執(zhí)行后一句,那么在執(zhí)行完瞬間方法被調(diào)用容易引發(fā)死循環(huán)
class_replaceMethod(class,swizzledSelector,originalIMP,originalType);
class_replaceMethod(class,originalSelector,swizzledIMP,swizzledType);
}
這是因?yàn)閏lass_replaceMethod方法其實(shí)能夠覆蓋到class_addMethod和method_setImplementation兩種場(chǎng)景, 對(duì)于第一個(gè)class_replaceMethod來說, 如果viewWillAppear:實(shí)現(xiàn)在父類, 則執(zhí)行class_addMethod, 否則就執(zhí)行method_setImplementation將原方法的IMP指定新的代碼塊; 而第二個(gè)class_replaceMethod完成的工作便只是將新方法的IMP指向原來的代碼.
但此處需要特別注意交換的順序,應(yīng)該優(yōu)化把新的方法指定原IMP,再修改原有的方法的IMP.
除了以上的場(chǎng)景之外,其它場(chǎng)景下我們?nèi)绾问褂肕ethod Swizzling呢?
1.在不同類之間實(shí)現(xiàn)Method Swizzling
上面示例是通過Category來新增一個(gè)方法然后實(shí)現(xiàn)Method Swizzling的, 但有一些場(chǎng)景可能并不適合使用Category(比如私有的類,未獲取到該類的聲明), 此時(shí)我們應(yīng)該如何來做Method Swizzling呢?
例如已知一個(gè)className為Car的類中有一個(gè)實(shí)例方法- (void)run:(double)speed, 目前需要Hook該方法對(duì)速度小于120才執(zhí)行run的代碼, 按照方法交換的流程, 代碼應(yīng)該是這樣的:
#import
@interface MyCar : NSObject
@end
@implementation MyCar
+ (void)load {
Class originalClass = NSClassFromString(@"Car");
Class swizzledClass = [self class];
SEL originalSelector = NSSelectorFromString(@"run:");
SEL swizzledSelector = @selector(xby_run:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
// 向Car類中新添加一個(gè)xby_run:的方法
BOOL registerMethod = class_addMethod(originalClass, swizzledSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if(!registerMethod) {
return;
}
// 需要更新swizzledMethod變量,獲取當(dāng)前Car類中xby_run:的Method指針
swizzledMethod = class_getInstanceMethod(originalClass, swizzledSelector);
if(!swizzledMethod) {
return;
}
// 后續(xù)流程與之前的一致
BOOL didAddMethod = class_addMethod(originalClass, originalSelector,
method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if(didAddMethod) {
class_replaceMethod(originalClass, swizzledSelector,
method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}else{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
- (void)xby_run:(double)speed {
if(speed < 120) {
[self xby_run:speed];
}
}
@end
與之前的流程相比,在前面添加了兩個(gè)邏輯:
利用runtime向目標(biāo)類Car動(dòng)態(tài)添加了一個(gè)新的方法,此時(shí)Car類與MyCar類一樣具備了xby_run:這個(gè)方法,MyCar的利用價(jià)值便結(jié)束了;
為了完成后續(xù)Car類中run:與xby_run:的方法交換,此時(shí)需要更新swizzledMethod變量為Car中的xby_run:方法所對(duì)應(yīng)的Method.
以上所有的邏輯也可以合并簡(jiǎn)化為以下:
+ (void)load {
Class originalClass = NSClassFromString(@"Car");
Class swizzledClass = [selfclass];
SEL originalSelector = NSSelectorFromString(@"run:");
SEL swizzledSelector = @selector(xby_run:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}
簡(jiǎn)化后的代碼便與之前使用Category的方式并沒有什么差異, 這樣代碼就很容易覆蓋到這兩種場(chǎng)景了, 但我們需要明確此時(shí)class_replaceMethod所完成的工作卻是不一樣的.
第一個(gè)class_replaceMethod直接在Car類中注冊(cè)了xby_run:方法,并且指定的IMP為當(dāng)前run:方法的IMP;
第二個(gè)class_replaceMethod與之前的邏輯一致,當(dāng)run:方法是實(shí)現(xiàn)在Car類或Car的父類,分別執(zhí)行method_setImplementation或class_addMethod;
2.如何實(shí)現(xiàn)類方法的Method Swizzling
以上的代碼都是實(shí)現(xiàn)的對(duì)實(shí)例方法的交換, 那如何來實(shí)現(xiàn)對(duì)類方法的交換呢, 依舊直接貼代碼吧:
@interface NSDictionary (Test)
@end
@implementation NSDictionary (Test)
+ (void)load {
Class cls = [self class];
SEL originalSelector = @selector(dictionary);
SEL swizzledSelector = @selector(xby_dictionary);
// 使用class_getClassMethod來獲取類方法的Method
Method originalMethod = class_getClassMethod(cls, originalSelector);
Method swizzledMethod = class_getClassMethod(cls, swizzledSelector);
if(!originalMethod || !swizzledMethod) {
return;
}
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
// 類方法添加,需要將方法添加到MetaClass中
Class metaClass = objc_getMetaClass(class_getName(cls));
class_replaceMethod(metaClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(metaClass,originalSelector,swizzledIMP,swizzledType);
}
+ (id)xby_dictionary {
id result = [self xby_dictionary];
return result;
}
@end
相比實(shí)例方法的Method Swizzling,流程有兩點(diǎn)差異:
獲取Method的方法變更為class_getClassMethod(Class cls, SEL name),從函數(shù)命名便直觀體現(xiàn)了和class_getInstanceMethod(Class cls, SEL name)的差別;
對(duì)于類方法的動(dòng)態(tài)添加,需要將方法添加到MetaClass中,因?yàn)閷?shí)例方法記錄在class的method-list中,類方法是記錄在meta-class中的method-list中的.
3.在類簇中如何實(shí)現(xiàn)Method Swizzling
在上面的代碼中我們實(shí)現(xiàn)了對(duì)NSDictionary中的+ (id)dictionary方法的交換,但如果我們用類似代碼嘗試對(duì)- (id)objectForKey:(id)key方法進(jìn)行交換后, 你便會(huì)發(fā)現(xiàn)這似乎并沒有什么用.
這是為什么呢? 平常我們?cè)赬code調(diào)試時(shí),在下方Debug區(qū)域左側(cè)的Variables View中,常常會(huì)發(fā)現(xiàn)如__NSArrayI或是__NSCFConstantString這樣的Class類型, 這便是在Foundation框架中被廣泛使用的類簇, 詳情請(qǐng)參看Apple文檔class cluster的內(nèi)容.
所以針對(duì)類簇的Method Swizzling問題就轉(zhuǎn)變?yōu)槿绾螌?duì)這些類簇中的私有類做Method Swizzling, 在上面介紹的不同類之間做Method Swizzling便已經(jīng)能解決該問題, 下面一個(gè)簡(jiǎn)單的示例通過交換NSMutableDictionary的setObject:forKey:方法,讓調(diào)用這個(gè)方法時(shí)當(dāng)參數(shù)object或key為空的不會(huì)拋出異常:
@interface MySafeDictionary : NSObject
@end
@implementation MySafeDictionary
+ (void)load {
Class originalClass = NSClassFromString(@"__NSDictionaryM");
Class swizzledClass = [selfclass];
SEL originalSelector = @selector(setObject:forKey:);
SEL swizzledSelector = @selector(safe_setObject:forKey:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
constchar*originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}
- (void)safe_setObject:(id)anObject forKey:(id)aKey {
if(anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
} else if(aKey) {
[(NSMutableDictionary *)self removeObjectForKey:aKey];
}
}
@end
4.在Method Swizzling之后如何恢復(fù)
使用了Method Swizzling的各種姿勢(shì)之后, 是否有考慮如何恢復(fù)到交換之前的現(xiàn)場(chǎng)呢?
一種方案就是通過一個(gè)開關(guān)標(biāo)識(shí)符, 如果需要從邏輯上面恢復(fù)到交換之前, 就設(shè)置一下這個(gè)標(biāo)識(shí)符, 在實(shí)現(xiàn)中判定如果設(shè)定了該標(biāo)識(shí)符, 邏輯就直接調(diào)用原方法的實(shí)現(xiàn), 其它什么事兒也不干, 這是目前大多數(shù)代碼的實(shí)現(xiàn)方法, 當(dāng)然也是非常安全的方式, 只不過當(dāng)交換方法過多時(shí), 每一個(gè)交換的方法體中都需要增加這樣的邏輯, 并且也需要維護(hù)大量這些標(biāo)識(shí)符變量, 只是會(huì)覺得不夠優(yōu)雅, 所以此處也就不展開詳細(xì)討論了.
那下面來討論一下有沒有更好的方案, 以上描述的Method Swizzling各種場(chǎng)景和處理的技巧, 但綜合總結(jié)之后最核心的其實(shí)也只做了兩件事情:
class_addMethod添加一個(gè)新的方法,可能是把其它類中實(shí)現(xiàn)的方法添加到目標(biāo)類中,也可能是把父類實(shí)現(xiàn)的方法添加一份在子類中,可能是添加的實(shí)例方法,也可能是添加的類方法,總之就是添加了方法.
交換 IMP交換方法的實(shí)現(xiàn)IMP,完成這個(gè)步驟除了使用method_exchangeImplementations
這個(gè)方法外,也可以是調(diào)用了method_setImplementation
方法來單獨(dú)修改某個(gè)方法的IMP,或者是采用在調(diào)用class_addMethod方法中設(shè)定了IMP而直接就完成了IMP的交換,總之就是對(duì)IMP的交換.
那我們來分別看一下這兩件事情是否都還能恢復(fù):
對(duì)于class_addMethod,我們首先想到的可能就是有沒有對(duì)應(yīng)的remove方法呢,在Objective-C 1.0的時(shí)候有class_removeMethods這個(gè)方法,不過在2.0的時(shí)候就已經(jīng)被禁用了,也就是蘋果并不推薦我們這樣做,想想似乎也是挺有道理的,本來runtime的接口看著就挺讓人心驚膽戰(zhàn)的,又是添加又是刪除總覺得會(huì)出岔子,所以只能放棄remove的想法,反正方法添加在那兒倒也沒什么太大的影響.
針對(duì)IMP的交換,在Method Swizzling時(shí)做的交換動(dòng)作,如果需要恢復(fù)其實(shí)要做的動(dòng)作還是交換回來罷了,所以是可以做到的,不過需要怎樣做呢?對(duì)于同一個(gè)類,同一個(gè)方法,可能會(huì)在不同的地方被多次做Method Swizzling,所以要回退某一次的Method Swizzling,我們就需要記錄下來這一次交換的時(shí)候是哪兩個(gè)IMP做了交換,恢復(fù)的時(shí)候再換回來即可.另一個(gè)問題是如果已經(jīng)經(jīng)過多次交換,我們?cè)鯓诱业竭@兩個(gè)IMP所對(duì)應(yīng)的Mehod呢,還好runtime提供了一個(gè)class_copyMethodList方法,可以直接取出Method列表,然后我們就可以逐個(gè)遍歷找到IMP所對(duì)應(yīng)的Method了,下面是對(duì)上一個(gè)示例添加恢復(fù)之后實(shí)現(xiàn)的代碼邏輯:
#import
@interface MySafeDictionary : NSObject
@end
static NSLock *kMySafeLock = nil;
static IMP kMySafeOriginalIMP = NULL;
static IMP kMySafeSwizzledIMP = NULL;
@implementation MySafeDictionary
+ (void)swizzlling {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
kMySafeLock = [[NSLock alloc] init];
});
[kMySafeLock lock];
do{
if(kMySafeOriginalIMP || kMySafeSwizzledIMP) {
break;
}
Class originalClass = NSClassFromString(@"__NSDictionaryM");
if(!originalClass) {
break;
}
Class swizzledClass = [selfclass];
SEL originalSelector = @selector(setObject:forKey:);
SEL swizzledSelector = @selector(safe_setObject:forKey:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
if(!originalMethod || !swizzledMethod) {
break;
}
IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);
kMySafeOriginalIMP = originalIMP;
kMySafeSwizzledIMP = swizzledIMP;
class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
} while(NO);
[kMySafeLock unlock];
}
+ (void)restore {
[kMySafeLock lock];
do{
if(!kMySafeOriginalIMP || !kMySafeSwizzledIMP)break;
Class originalClass = NSClassFromString(@"__NSDictionaryM");
if(!originalClass) {
break;
}
Method originalMethod = NULL;
Method swizzledMethod = NULL;
unsigned int outCount = 0;
Method *methodList = class_copyMethodList(originalClass, &outCount);
for(unsigned int idx=0; idx < outCount; idx++) {
Method aMethod = methodList[idx];
IMP aIMP = method_getImplementation(aMethod);
if(aIMP == kMySafeSwizzledIMP) {
originalMethod = aMethod;
} else if(aIMP == kMySafeOriginalIMP) {
swizzledMethod = aMethod;
}
}
// 盡可能使用exchange,因?yàn)樗莂tomic的
if(originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
} else if(originalMethod) {
method_setImplementation(originalMethod, kMySafeOriginalIMP);
} else if(swizzledMethod) {
method_setImplementation(swizzledMethod, kMySafeSwizzledIMP);
}
kMySafeOriginalIMP = NULL;
kMySafeSwizzledIMP = NULL;
}while(NO);
[kMySafeLock unlock];
}
- (void)safe_setObject:(id)anObject forKey:(id)aKey {
if(anObject && aKey) {
[self safe_setObject:anObject forKey:aKey];
} else if(aKey) {
[(NSMutableDictionary *)self removeObjectForKey:aKey];
}
}
@end
注意這段代碼的Method Swizzling和恢復(fù)都需要主動(dòng)調(diào)用, 并且相比上面其它的示例, 這段代碼還添加如鎖機(jī)制來加之保護(hù). 這個(gè)示例是以不同的類來實(shí)現(xiàn)的Method Swizzling和恢復(fù), 如果是Category或者是類方法, 根據(jù)之前的示例也需要做相應(yīng)的調(diào)整.