Runtime 3 Method Swizzling
- 簡(jiǎn)介
- 對(duì)象、類(lèi)的結(jié)構(gòu)
- objc_object
- objc_class
- 消息傳遞(Messaging)
- objc_method
- objc_msgSend
- 動(dòng)態(tài)方法解析和轉(zhuǎn)發(fā)
- 動(dòng)態(tài)方法解析
- 快速消息轉(zhuǎn)發(fā)
- 標(biāo)準(zhǔn)消息轉(zhuǎn)發(fā)
- 消息轉(zhuǎn)發(fā)與多繼承
- 消息轉(zhuǎn)發(fā)與代理對(duì)象
- Method Swizzling
- class_replaceMethod
- method_setImplementation
- method_exchangeImplementations
- Method Swizzling 的應(yīng)用
- Method Swizzling 注意事項(xiàng)
- isa swizzling
- 介紹
- 應(yīng)用之KVO
- 注意
持續(xù)更新中...
Method Swizzling
其實(shí)runtime的概念坊饶、特性已經(jīng)講完了∪福現(xiàn)在來(lái)說(shuō)一下runtime很強(qiáng)大的一個(gè)黑色技能:Method Swizzling高每。
Method Swizzling 利用 Runtime 特性把一個(gè)方法的實(shí)現(xiàn)與另一個(gè)方法的實(shí)現(xiàn)進(jìn)行替換淤井。
消息傳遞 中有講到腺晾,每個(gè)類(lèi)里都有一個(gè) dispatch table 捆昏,將方法的名字(SEL)跟方法的實(shí)現(xiàn)(IMP煌恢,指向 C 函數(shù)的指針)一一對(duì)應(yīng)。swizzle 一個(gè)方法其實(shí)就是在程序運(yùn)行時(shí)在 dispatch table 里做點(diǎn)改動(dòng)瑰抵,讓這個(gè)方法的名字(SEL)對(duì)應(yīng)到另個(gè) IMP 。
轉(zhuǎn)換前谍憔,一一對(duì)應(yīng):
轉(zhuǎn)換后,交換一一對(duì)應(yīng):
在objc/runtime.h
中逛球,OC提供了以下API來(lái)動(dòng)態(tài)替換方法的實(shí)現(xiàn):
class_replaceMethod
method_setImplementation
method_exchangeImplementations
這些方法歸根結(jié)底苫昌,都是偷換了method的IMP。
class_replaceMethod
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
在文檔中詳細(xì)說(shuō)明了祟身,它有兩種不同的行為。當(dāng)類(lèi)中沒(méi)有想替換的原方法時(shí)袜硫,該方法會(huì)調(diào)用 class_addMethod
來(lái)為該類(lèi)增加一個(gè)新方法。也因此它需要在調(diào)用時(shí)傳入types參數(shù)婉陷,而method_exchangeImplementations
和method_setImplementation
卻不需要帚称。
method_setImplementation
最簡(jiǎn)單,僅僅是給一個(gè)方法設(shè)置其實(shí)現(xiàn)方式秽澳。
method_exchangeImplementations
顧名思義闯睹,是交換兩個(gè)方法的實(shí)現(xiàn),等同于調(diào)用兩次method_setImplementation
:
IMP imp1 = method_getImplementation(m1);
IMP imp2 = method_getImplementation(m2);
method_setImplementation(m1, imp2);
method_setImplementation(m2, imp1);
Method Swizzling 的應(yīng)用
有時(shí)候我們想對(duì)已有類(lèi)的已有實(shí)現(xiàn)增加一些額外處理担神,這時(shí)候我們可以在已有類(lèi)的分類(lèi)中做Method Swizzling楼吃。
下面我們?cè)赨IControl的分類(lèi)里,將-setTag:
和自定義的-xxx_setTag:
方法交換:
- (void)xxx_setTag:(NSInteger)tag {
NSLog(@"%s %@ tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
return [self xxx_setTag:tag];
}
上面是自定義-xxx_setTag:
方法的實(shí)現(xiàn)妄讯,乍一看像是遞歸孩锡。但是別忘了我們是要調(diào)換方法的IMP的,在runtime的時(shí)候捞挥,函數(shù)實(shí)現(xiàn)已經(jīng)被交換了浮创。調(diào)用-setTag:
會(huì)調(diào)用你實(shí)現(xiàn)的-xxx_setTag:
,而在 -xxx_setTag:
里調(diào)用-xxx_setTag:
實(shí)際上調(diào)用的是原來(lái)的-setTag:
砌函。
//UIControl+XXXExtension.m
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
//1
SEL originalSelector = @selector(setTag:);
SEL swizzledSelector = @selector(xxx_setTag:);
//2
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
// ...
// Method originalMethod = class_getClassMethod(class, originalSelector);
// Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
//3
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
//4
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
//5
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- 拿到要交換的兩個(gè)方法名SEL
- 通過(guò)方法名SEL平道,拿到方法對(duì)象Method
- 在交換方法前草雕,先調(diào)用了
class_addMethod
。是因?yàn)橐WC所交換的原方法是本類(lèi)的方法,不是父類(lèi)的方法棍丐。class_addMethod
會(huì)覆蓋父類(lèi)的方法實(shí)現(xiàn),但是不會(huì)替換本類(lèi)已經(jīng)有的方法實(shí)現(xiàn)娄猫。所以先做一層class_addMethod
保證了本類(lèi)本身有原方法的實(shí)現(xiàn)昏鹃。 - 如果本類(lèi)沒(méi)有相應(yīng)的原方法實(shí)現(xiàn),
class_addMethod
會(huì)成功添加一個(gè)原方法贩疙,實(shí)現(xiàn)IMP設(shè)置為新的實(shí)現(xiàn)讹弯。然后通過(guò)class_replaceMethod
在新方法名義下設(shè)置原方法的實(shí)現(xiàn)。 - 如果本類(lèi)有相應(yīng)的原方法實(shí)現(xiàn)这溅,
method_exchangeImplementations
交換兩個(gè)方法的實(shí)現(xiàn)组民。
通常我們方法混寫(xiě)是想要在應(yīng)用程序的整個(gè)生命周期中有效,所以把method swizzling的代碼放在+load
的dispatch once中悲靴,是為了保證它的執(zhí)行是線程安全的,并且只執(zhí)行一次耸三。了解更多關(guān)于+load
然后我們來(lái)調(diào)用一下混寫(xiě)后的方法:
UIControl *ctrl = [[UIControl alloc] init];
ctrl.tag = 10;
UIView *view = [[UIView alloc] init];
view.tag = 100;
控制臺(tái)輸出:
-[UIControl(XXXExtension) xxx_setTag:] tag=10
從輸出中仪壮,可以知道新方法只應(yīng)用于UIControl的對(duì)象睛驳,并不影響其父類(lèi)UIView的對(duì)象膜廊。這就因?yàn)槲覀冎唤粨Q了UIControl類(lèi)的方法爪瓜,明顯UIControl本身沒(méi)有-setTag:
方法,所以會(huì)通過(guò)class_addMethod
添加一個(gè)蝶缀,所以不影響其父類(lèi)UIView翁都。
Method Swizzling 注意事項(xiàng)
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method's arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
- Method swizzling is not atomic
很明顯方法混寫(xiě)的代碼要完整的執(zhí)行柄慰,程序才會(huì)正常執(zhí)行,正如我們?cè)?code>+load方法中執(zhí)行dispatch once藏研。
- Changes behavior of un-owned code
混寫(xiě)的方法不止對(duì)一個(gè)實(shí)例有效蠢挡,是對(duì)目標(biāo)類(lèi)的所有實(shí)例业踏。我們改變了目標(biāo)類(lèi)涧卵,所以swizzling是很重要的事艺演,要十分小心胎撤。
- Possible naming conflicts
命名沖突貫穿整個(gè)Cocoa的問(wèn)題,我們常常在類(lèi)名和類(lèi)別方法名前加上前綴巫俺,所以我們也在新方法的前面加前綴介汹,就像前面代碼里的-xxx_setTag:
嘹承。但如果-xxx_setTag:
在別處也定義了怎么辦叹卷?這個(gè)問(wèn)題不僅僅存在于swizzling坪它,這我們可以用別的變通的方法:
直接用新的 IMP 取代原 IMP 往毡,而不是替換。只需要有全局的函數(shù)指針指向原 IMP 就可以懒震。
static void (*gOriginalSetTagIMP)(id self, SEL _cmd, NSInteger tag);
static void xxxSetTag(id self, SEL _cmd, NSInteger tag) {
// do custom work
NSLog(@"%s tag=%ld", __FUNCTION__, tag);
gOriginalSetTagIMP(self, _cmd, tag);
}
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
gOriginalSetTagIMP = (void *)method_getImplementation(originalMethod);
if(!class_addMethod(self, @selector(setTag:), (IMP)xxxSetTag, method_getTypeEncoding(originalMethod))) {
method_setImplementation(originalMethod, (IMP)xxxSetTag);
}
});
}
注意這里的自定義實(shí)現(xiàn)xxxSetTag
里不能再調(diào)用xxxSetTag
本身了挎狸,不然就真的遞歸了锨匆。因?yàn)檫@里調(diào)用的是函數(shù)恐锣,直接調(diào)用舞痰,不走runtime的消息傳遞了响牛。
或者用OC提供的imp_implementationWithBlock
,直接用block生成對(duì)應(yīng)的實(shí)現(xiàn)IMP:
Method originalMethod = class_getInstanceMethod(self, @selector(setTag:));
IMP originalImp = method_getImplementation(originalMethod);
SEL originalSel = method_getName(originalMethod);
IMP swizzledImp = imp_implementationWithBlock(^(id target, NSInteger tag){
NSLog(@"%s tag=%ld", __FUNCTION__, tag);
void (*func)(id, SEL, NSInteger) = (void(*)(id, SEL, NSInteger))originalImp;
func(target, originalSel, tag);
});
if(!class_addMethod(self, @selector(setTag:), (IMP)swizzledImp, method_getTypeEncoding(originalMethod))) {
method_setImplementation(originalMethod, (IMP)swizzledImp);
}
- Swizzling changes the method's arguments
method swizzling 后的方法,想正常調(diào)用的話贬丛,將是個(gè)問(wèn)題豺憔。
比如如果想直接調(diào)用xxx_setTag:
方法:
[self xxx_setTag:12];
runtime 的做法是:
objc_msgSend(self, @selector(xxx_setTag:), 12);
runtime去尋找xxx_setTag:
的方法實(shí)現(xiàn), _cmd
參數(shù)為xxx_setTag:
恭应,但是事實(shí)上runtime找到的方法實(shí)現(xiàn)是原始的setTag:
的。
解決方法:使用全局的函數(shù)指針I(yè)MP境肾。
- The order of swizzles matters
多個(gè)swizzle方法的執(zhí)行順序也需要注意准夷。那么應(yīng)該是什么順序呢衫嵌,從父類(lèi)->子類(lèi)的順序交換彻秆,還是從父類(lèi)->子類(lèi)的順序?
舉個(gè)例子酒朵,在UIView蔫耽、UIControl、UIButton的分類(lèi)里面分別自定義如下代碼图甜,準(zhǔn)備與原來(lái)的-setTag:
方法進(jìn)行交換:
// UIView+LYHExtension.m
- (void)viewSetTag:(NSInteger)tag {
NSLog(@"%s cmd=%@ tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
return [self viewSetTag:tag];
}
// UIControl+LYHExtension.m
- (void)controlSetTag:(NSInteger)tag {
NSLog(@"%s cmd=%@ tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
return [self controlSetTag:tag];
}
// UIButton+LYHExtension.m
- (void)buttonSetTag:(NSInteger)tag {
NSLog(@"%s cmd=%@ tag=%ld", __FUNCTION__, NSStringFromSelector(_cmd), tag);
return [self buttonSetTag:tag];
}
從父類(lèi)->子類(lèi)的順序進(jìn)行method swizzle:
[UIView swizzle:@selector(setTag:) withSelector:@selector(viewSetTag:)];
[UIControl swizzle:@selector(setTag:) withSelector:@selector(controlSetTag:)];
[UIButton swizzle:@selector(setTag:) withSelector:@selector(buttonSetTag:)];
這里新寫(xiě)了一個(gè)方法-swizzle: withSelector:
黑毅,實(shí)現(xiàn)跟上面+load
方法里的方法交換一樣矿瘦。
交換前:
按父類(lèi)->子類(lèi)的順序交換后:
創(chuàng)建一個(gè)UIButton的實(shí)例,然后調(diào)用一下-setTag:
掘猿,因?yàn)樵谧远x的方法里的實(shí)現(xiàn)是用NSLog
打印當(dāng)前方法信息稠通,以及調(diào)用原來(lái)的方法改橘。那就看一下控制臺(tái):
-[UIButton(XXXExtension) buttonSetTag:] cmd=setTag: tag=100
-[UIControl(XXXExtension) controlSetTag:] cmd=buttonSetTag: tag=100
-[UIView(XXXExtension) viewSetTag:] cmd=controlSetTag: tag=100
用圖表示更清晰,按父類(lèi)->子類(lèi)的順序交換后狮惜,調(diào)用子類(lèi)的-setTag:
方法:
很明顯碾篡,這種從父類(lèi)->子類(lèi)的交換順序开泽,能正常實(shí)現(xiàn)我們想要的流程穆律,也就是子類(lèi)中拿到的方法,是父類(lèi)已經(jīng)swizzle后的代碼剔蹋。
反過(guò)來(lái)辅髓,子類(lèi)->父類(lèi)的順序進(jìn)行method swizzle:
按子類(lèi)->父類(lèi)的順序交換后:
很明顯律想,每層的子類(lèi)(UIButton绍弟、UIControl)都是直接跟擁有原始方法的父類(lèi)(UIView)直接進(jìn)行交換樟遣,不管是否跨層級(jí)身笤。
依然創(chuàng)建一個(gè)UIButton的實(shí)例液荸,調(diào)用一下-setTag:
,控制臺(tái):
-[UIButton(XXXExtension) buttonSetTag:] cmd=setTag: tag=100
控制臺(tái)只打印了UIButton的自定義方法伤柄,沒(méi)有繼承父類(lèi)UIControl和祖父類(lèi)UIView的自定義方法适刀。
按子類(lèi)->父類(lèi)的順序交換后笔喉,調(diào)用子類(lèi)的-setTag:
方法:
多個(gè)有繼承關(guān)系的類(lèi)的對(duì)象swizzle時(shí)常挚,先從父對(duì)象開(kāi)始稽物。 這樣才能保證子類(lèi)方法拿到父類(lèi)中的被swizzle的實(shí)現(xiàn)姨裸。在+(void)load中swizzle不會(huì)出錯(cuò)怨酝,就是因?yàn)閘oad類(lèi)方法會(huì)默認(rèn)從父類(lèi)開(kāi)始調(diào)用农猬。
- Difficult to understand (looks recursive)
新方法的實(shí)現(xiàn)看起來(lái)像遞歸斤葱,但是看看上面已經(jīng)給出的 swizzling 封裝方法, 使用起來(lái)就很易讀懂揍堕。
解決方法:使用全局的函數(shù)指針I(yè)MP汤纸。
- Difficult to debug
使用NSStringFromSelector(_cmd)
打印出的方法名調(diào)用的方法名贮泞,__FUNCTION__
打印出的方法名是真正實(shí)現(xiàn)的方法名。這塊可能會(huì)混亂囊蓝,畢竟交換了方法聚霜,就像A的實(shí)現(xiàn)是B珠叔,B的實(shí)現(xiàn)是A运杭,不是平常看到的代碼那么直接撇眯,這塊需要思考熊榛,一不小心就會(huì)忘了腕巡。
解決方法:充分的文檔,即使只有你一個(gè)人開(kāi)發(fā)豺总。