導(dǎo)語(yǔ):Method Swizzling是Objective-C中運(yùn)行時(shí)中討論較多的內(nèi)容奖慌,本文主要介紹使用Method Swizzling遇到的問(wèn)題和項(xiàng)目中使用的Swizzling方案。
一、Method Swizzling簡(jiǎn)介
Method Swizzling的本質(zhì)是在運(yùn)行時(shí)交換方法實(shí)現(xiàn)(IMP),如hook系統(tǒng)方法斩狱,在原有的方法中愧怜,插入自己的業(yè)務(wù)需求。
1蝇率、Method Swizzling原理
-
Objective-C的消息機(jī)制:在 Objective-C 中調(diào)用一個(gè)方法迟杂, 實(shí)際上是在底層通過(guò) objc_msgSend()發(fā)送一個(gè)消息。 而查找消息的唯一依據(jù)是selector的方法名本慕。
//調(diào)用方法 [obj doSomething]; //[obj doSomething]本質(zhì)上是給obj發(fā)doSomething消息 objc_msgSend(obj,@selector(doSomething))
每一個(gè)OC實(shí)例對(duì)象都保存有isa指針和實(shí)例變量排拷,其中isa指針?biāo)鶎兕悾惥S護(hù)一個(gè)運(yùn)行時(shí)可接收的方法列表(MethodLists)锅尘;方法列表(MethodLists)中保存selector的方法名和方法實(shí)現(xiàn)(IMP监氢,指向Method實(shí)現(xiàn)的指針)的映射關(guān)系。在運(yùn)行時(shí)藤违,通過(guò)selecter找到匹配的IMP浪腐,從而找到的具體的實(shí)現(xiàn)函數(shù)。
- 開發(fā)中可以利用Objective-C的動(dòng)態(tài)特性顿乒,在運(yùn)行時(shí)替換selector對(duì)應(yīng)的方法實(shí)現(xiàn)(IMP)议街,達(dá)到給hook的目的。下圖是利用Method Swizzling來(lái)替換selector對(duì)應(yīng)IMP后的方法列表示意圖淆游。
2傍睹、Method Swizzling使用
Method Swizzling的本質(zhì)就是偷換selector的IMP,下面就Swizzle NSObject的description方法犹菱,簡(jiǎn)單舉例:
#import "NSObject+Swizzle.h"
#import <objc/runtime.h>
@implementation NSObject (Swizzle)
+ (void)load{
//調(diào)換IMP
Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description));
Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description));
method_exchangeImplementations(originalMethod, myMethod);
}
- (void)qs_description{
NSLog(@"description 被 Swizzle 了");
return [self qs_description];
}
@end
說(shuō)明:調(diào)用被hook的description方法拾稳,獲取內(nèi)容前,會(huì)打印“description 被 Swizzle 了”這樣的日志腊脱。
3访得、Method Swizzling存在的問(wèn)題
- 不是線程安全的(Method swizzling is not atomic)
- 改變了代碼本來(lái)的行為(Changes behavior of un-owned code)
- 潛在的命名沖突(Possible naming conflicts)
- 改變方法的參數(shù)(Swizzling changes the method's arguments)
- 繼承問(wèn)題(The order of swizzles matters)
- 難以理解 (Difficult to understand)
- 難以調(diào)試(Difficult to debug)
二、RSSwizzle:Method Swizzling的優(yōu)雅方案
RSSwizzle是線程安全的Method Swizzling方案陕凹,能夠幫我們解決Method Swizzling的使用問(wèn)題悍抑。介紹如下:
1、不是線程安全的(Method swizzling is not atomic)
通常在 load方法中交換方法實(shí)現(xiàn)杜耙,如果在其他時(shí)機(jī)交換方法實(shí)現(xiàn)搜骡,需要考慮線程安全的問(wèn)題。
RSSwizzle利用了自旋鎖OSSpinLock保證線程安全佑女〖敲遥可以在任意時(shí)機(jī)交換方法實(shí)現(xiàn)。
2团驱、 改變了代碼本來(lái)的行為(Changes behavior of un-owned code)
這正是Swizzle的目標(biāo)摸吠。但是在Swizzle方法中,我們保留*調(diào)用原始實(shí)現(xiàn)的好習(xí)慣嚎花,能避免絕大多數(shù)問(wèn)題寸痢。我們利用Swizzle,一般是為了在原始實(shí)現(xiàn)基礎(chǔ)上紊选,添加某些自己的業(yè)務(wù)需求啼止,并不想刻意去破壞原有實(shí)現(xiàn)道逗。
RSSwizzle提供調(diào)用原來(lái)實(shí)現(xiàn)的宏RSSWCallOriginal,很方便献烦。
3憔辫、潛在的命名沖突(Possible naming conflicts)#####
通常在替換的方法名前加前綴,可以很大程度上避免命名沖突沖突問(wèn)題仿荆。
RSSwizzle在自定義的swizzle的靜態(tài)方法完成方法替換,完全避免了命名沖突問(wèn)題坏平。
4拢操、改變方法的參數(shù)(Swizzling changes the method's arguments)
-
參數(shù) _cmd 被篡改,正常調(diào)用Swizzle 的方法有問(wèn)題舶替。
//調(diào)用方法 [self qs_setFrame:frame]; //發(fā)消息 objc_msgSend(self, @selector(qs_setFrame:), frame);
說(shuō)明:在運(yùn)行時(shí)令境,尋找qs_setFrame:的方法實(shí)現(xiàn), _cmd參數(shù)雖然是 qs_setFrame: ,但是實(shí)際上找到的方法實(shí)現(xiàn)是原始的 setFrame: 實(shí)現(xiàn)顾瞪。
RSSwizzle的自定義的swizzle的靜態(tài)方法解決這個(gè)問(wèn)題舔庶。
5、繼承問(wèn)題(The order of swizzles matters)
多個(gè)有繼承關(guān)系的類的對(duì)象Swizzle時(shí)陈醒,先從父對(duì)象開始惕橙。 這樣才能保證子類方法拿到父類中的被Swizzle的實(shí)現(xiàn)。
在load中Swizzle不用擔(dān)心這種問(wèn)題钉跷,因?yàn)閘oad類方法會(huì)默認(rèn)從父類開始調(diào)用弥鹦。
6、難以理解 (Difficult to understand)
主要表現(xiàn)在調(diào)用原始實(shí)現(xiàn)爷辙,看起來(lái)像遞歸彬坏,有點(diǎn)懵。
RSSwizzle提供的宏RSSWCallOriginal讓調(diào)用原始實(shí)現(xiàn)更容易膝晾,代碼閱讀性更強(qiáng)栓始。
7、難以調(diào)試(Difficult to debug)
Debug時(shí)候打印出的backtrace(回溯)血当,其中摻雜著被swizzle的方法名幻赚,看起來(lái)比較亂,所以命名清晰很重要歹颓;
RSSwizzle打印出來(lái)的命名很清晰坯屿,此外Swizzle了什么,最好有文檔記錄巍扛。
三领跛、RSSwizzle的基礎(chǔ)使用
RSSwizzle中提供了兩種使用方式,一種是通過(guò)調(diào)用類方法來(lái)實(shí)現(xiàn)函數(shù)的替換撤奸,另一種是使用RSSwizzle定義的宏來(lái)進(jìn)行函數(shù)的替換吠昭。
1喊括、 使用類方法替換實(shí)例方法實(shí)現(xiàn)
/**
參數(shù)1:要被替換的函數(shù)選擇器
參數(shù)2:要被替換的函數(shù)所在的類
參數(shù)3: block中返回替換后的方法,block參數(shù)中需要返回一個(gè)方法函數(shù),這個(gè)函數(shù)為要替換成的函數(shù)矢棚,要和原函數(shù)類型相同郑什。在類中的函數(shù)默認(rèn)都會(huì)有一個(gè)名為self的id參數(shù)
參數(shù)4:此次替換用到的key
*/
[RSSwizzle swizzleInstanceMethod:@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){
NSLog(@"touchesBegan:withEvent:被Swizzle了");
};
} mode:RSSwizzleModeAlways key:NULL];
2、 使用宏替換實(shí)例方法實(shí)現(xiàn)
/*
參數(shù)1:要被替換的函數(shù)所在的類
參數(shù)2: 要被替換的函數(shù)選擇器
參數(shù)3:返回值類型蒲肋,
參數(shù)4:參數(shù)列表
參數(shù)5:要替換的代碼塊蘑拯,
參數(shù)6:執(zhí)行模式,
參數(shù)7:key值標(biāo)識(shí),RSSwizzleModeOncePerClass模式下使用兜粘,其他情況置為NULL
*/
RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet<UITouch *> *touches,UIEvent *event),RSSWReplacement({
NSLog(@"touchesEnded:withEvent被Swizzle了");
RSSWCallOriginal(touches,event);
}), RSSwizzleModeAlways, NULL);
3申窘、 使用類方法替換類方法實(shí)現(xiàn)
/*
參數(shù)1:要替換的函數(shù)選擇器
參數(shù)2:要替換此函數(shù)的類
參數(shù)3:block中返回替換后的方法,block參數(shù)中需要返回一個(gè)方法函數(shù),這個(gè)函數(shù)為要替換成的函數(shù)孔轴,要和原函數(shù)類型相同剃法。在類中的函數(shù)默認(rèn)都會(huì)有一個(gè)名為self的id參數(shù)
*/
[RSSwizzle swizzleClassMethod:@selector(testClassMethod1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) {
return ^(__unsafe_unretained id self){
NSLog(@"Class testClassMethod1 Swizzle");
};
}];
4、使用宏替換類方法實(shí)現(xiàn)
/*
參數(shù)1:要替換方法的類
參數(shù)2:要替換的方法選擇器
參數(shù)3:方法的返回值類型
參數(shù)4:方法的參數(shù)列表
參數(shù)5:要替換的方法代碼塊
*/
RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@"testClassMethod2"), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
//先執(zhí)行原始方法
RSSWCallOriginal();
NSLog(@"Class testClassMethod2 Swizzle");
}));
說(shuō)明:RSSwizzle還提供了Swizzle模式路鹰,使用Swizzle實(shí)例方法時(shí)候需要用到贷洲。Swizzle類方法,默認(rèn)RSSwizzleModeAlways晋柱,定義如下:
typedef NS_ENUM(NSUInteger, RSSwizzleMode) {
//任何情況下 始終執(zhí)行替換操作
RSSwizzleModeAlways = 0,
//相同key標(biāo)識(shí)的替換操作只會(huì)被執(zhí)行一次
RSSwizzleModeOncePerClass = 1,
//相同key標(biāo)識(shí)的替換操作在子類父類中只會(huì)被執(zhí)行一次
RSSwizzleModeOncePerClassAndSuperclasses = 2
};
四优构、一個(gè)使用Swizzling典型的錯(cuò)誤案例
網(wǎng)絡(luò)上很多博客介紹了使用Swizzling來(lái)防止重復(fù)點(diǎn)擊UIButton,但是大部分都會(huì)有問(wèn)題雁竞。
1俩块、錯(cuò)誤代碼
一般在load中替換sendAction:to:forEvent:方法,主要代碼如下:
+ (void)load {
Method before = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method after = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
method_exchangeImplementations(before, after);
}
- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
return;
}
if (self.qs_acceptEventInterval > 0) {
self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
[self qs_sendAction:action to:target forEvent:event];
}
錯(cuò)誤現(xiàn)象:
點(diǎn)擊UITabBar上按鈕會(huì)crash, 提示類似于:[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance ...浓领。
錯(cuò)誤原因:
1)UITabBarButton是UITabBarController中各個(gè)子控制器在工具條中對(duì)應(yīng)的按鈕玉凯,是UITabBar的私有屬性,UITabBarButton的父類是UIControl联贩,而UIButton的父類也是UIControl漫仆,sendAction:to:forEvent:是UIControl的實(shí)例方法;
2) 在UIButton類中沒有sendAction:to:forEvent:這個(gè)方法實(shí)現(xiàn)泪幌,通過(guò)class_getInstanceMethod() 獲取的是父類的 Method 對(duì)象盲厌,使用 method_exchangeImplementations() 就把父類的原始實(shí)現(xiàn)(IMP)跟自己的 Swizzle 實(shí)現(xiàn)交換了。這就導(dǎo)致UIControl的其他子類祸泪,如UITabBarButton在被點(diǎn)擊后吗浩,都調(diào)用了UIButton的Swizzle 實(shí)現(xiàn),發(fā)生了嚴(yán)重的Crash問(wèn)題没隘。
說(shuō)明:雖然在UIControl的分類的load方法交換方法實(shí)現(xiàn)懂扼,能解決問(wèn)題,我們將Swizzling的影響擴(kuò)大很多倍,不是理想的做法阀湿。下面介紹解決辦法赶熟。
2、解決辦法
在項(xiàng)目直接使用method_exchangeImplementations很危險(xiǎn)陷嘴,甚至導(dǎo)致Crash映砖,在項(xiàng)目中不建議這么做≡职ぃ可采用的解決辦法有兩種:
方法A
原理:如果類中沒有實(shí)現(xiàn) Original Selector 對(duì)應(yīng)的方法邑退,那就通過(guò)class_addMethod方法為Original Selector增加Swizzle 的實(shí)現(xiàn),通過(guò)class_replaceMethod修改Swizzle Selector 的 實(shí)現(xiàn) 為 Original 的實(shí)現(xiàn)劳澄;如果已經(jīng)有Original Selector 對(duì)應(yīng)的方法(通過(guò)class_addMethod方法添加是失敗的), 這時(shí)才使用method_exchangeImplementations來(lái)直接交換瓜饥。
代碼如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(sendAction:to:forEvent:);
SEL swizzledSelector = @selector(qs_sendAction:to:forEvent:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
if (success) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
說(shuō)明1:class_addMethod方法可以為類添加新的方法實(shí)現(xiàn)(IMP),添加成功返回YES.浴骂。否則返回NO。如果選擇器(select)已經(jīng)有對(duì)應(yīng)的方法實(shí)現(xiàn)(IMP), 添加也是失敗的宪潮,利用這點(diǎn)可以檢查是否有源方法實(shí)現(xiàn)溯警,如果沒有利用class_replaceMethod來(lái)將swizzledSelector和originalMethod對(duì)應(yīng)設(shè)置好。
說(shuō)明2:.class_replaceMethod用來(lái)替換類中的方法實(shí)現(xiàn)狡相,會(huì)調(diào)用class_addMethod和method_setImplementation方法(直接設(shè)置某個(gè)方法的IMP)
方法B
原理:RSSwizzle完美避開了在load中使用method_exchangeImplementations交換方法的尷尬梯轻,基于Swizzle模式和class_replaceMethod完美控制了替換方法實(shí)現(xiàn)。
代碼如下:
+ (void)load{
RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
UIButton *btn = self;
if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
return;
}
if (btn.qs_acceptEventInterval > 0) {
btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
}
RSSWCallOriginal(action,target,event);
}), RSSwizzleModeAlways, NULL);
}
說(shuō)明:RSSwizzleInstanceMethod宏實(shí)現(xiàn)方法實(shí)現(xiàn)的替換尽棕,代碼更易閱讀喳挑。
End
Demo地址
QSSwizzleKitDemo參考資料
Objective-C的hook方案(一): Method Swizzling
Objective-C Method Swizzling我是南華coder,一名北漂的初級(jí)iOS程序猿滔悉。iOS札記是我的一點(diǎn)學(xué)習(xí)筆記伊诵,不足之處,望批評(píng)指正回官。