1. 關(guān)于Method swizzling的兩種寫法。
簡(jiǎn)單實(shí)現(xiàn):
void simple_swizzle(Class class, SEL original, SEL swizzle) {
Method originalMethod = class_getInstanceMethod(class, original);
Method swizzleMethod = class_getInstanceMethod(class, swizzle);
method_exchangeImplementations(originalMethod, swizzleMethod);
}
需要注意的是,class_getInstanceMethod如果沒(méi)在當(dāng)前類中查找到對(duì)應(yīng)方法那么就會(huì)在整個(gè)繼承體系中查找。如果明確知道originalMethod是當(dāng)前類實(shí)現(xiàn)的方法,這種方式可以正常工作。當(dāng)originalMethod是父類的方法時(shí),swizzle后父類調(diào)用此方法會(huì)因?yàn)檎也坏阶宇惖膶?shí)現(xiàn)然后報(bào)錯(cuò)。
如果originalMethod==nil說(shuō)明沒(méi)有找到這個(gè)方法,會(huì)導(dǎo)致swizzle失敗。
最佳實(shí)踐:
void best_swizzle(Class class, SEL original, SEL swizzle) {
Method originalMethod = class_getInstanceMethod(class, original);
Method swizzleMethod = class_getInstanceMethod(class, swizzle);
// 嘗試向子類添加original方法,對(duì)應(yīng)的方法實(shí)現(xiàn)為swizzle后的實(shí)現(xiàn)。
// didAddMethod == NO時(shí),說(shuō)明當(dāng)前類存在originalMethod,處理方式和simple_swizzle一樣。
// didAddMethod == YES時(shí),說(shuō)明當(dāng)前類沒(méi)有實(shí)現(xiàn)originalMethod,已成功添加
BOOL didAddMethod = class_addMethod(class, original, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
if (didAddMethod) {
// 這里將swizzle方法的實(shí)現(xiàn)replace成父類的實(shí)現(xiàn)
class_replaceMethod(class, swizzle, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzleMethod);
}
// 這里處理 originalMethod==nil 的情況,說(shuō)明之前在didAddMethod == YES的分支里repalce失敗。在這里直接將swizzleMethod的實(shí)現(xiàn)用一個(gè)block替換
if (!originalMethod) {
method_setImplementation(swizzleMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
NSLog(@"original method not implemented");
}));
}
}
一般情況下,如果明確需要swizzle的方法是當(dāng)前類的方法,那么直接用第一種方式即可,這種方式建議用在一些通用的,需要滿足高度容錯(cuò)度的場(chǎng)景,比如作為SDK的一部分發(fā)布出去的時(shí)候。
2. 關(guān)于多次swizzle后所有swizzle的方法是否都會(huì)被執(zhí)行的問(wèn)題。
一直有一個(gè)困惑,對(duì)同一個(gè)方法多次methods swizzing之后到底會(huì)發(fā)生什么,執(zhí)行的順序又是怎么樣的。趁著周末時(shí)間來(lái)測(cè)試驗(yàn)證一下。
創(chuàng)建Coder類,提供一個(gè)叫做coding的方法。
@interface Coder : NSObject
- (void)coding;
@end
@implementation Coder
- (void)coding {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
}
@end
創(chuàng)建用于swizzle的category,在category的load方法中連續(xù)swizzle5次。
@interface Coder (language)
@end
@implementation Coder (language)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
coder_bestSwizzle(self, @selector(coding), @selector(coding_objc));
coder_bestSwizzle(self, @selector(coding), @selector(coding_kotlin));
coder_bestSwizzle(self, @selector(coding), @selector(coding_cpp));
coder_bestSwizzle(self, @selector(coding), @selector(coding_java));
coder_bestSwizzle(self, @selector(coding), @selector(coding_swift));
});
}
- (void)coding_objc {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_objc];
}
- (void)coding_kotlin {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_kotlin];
}
- (void)coding_cpp {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_cpp];
}
- (void)coding_java {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_java];
}
- (void)coding_swift {
NSLog(@"\n %s", __PRETTY_FUNCTION__);
[self coding_swift];
}
@end
新建coder對(duì)象,調(diào)用coding方法
- (void)coderTest {
Coder *coder = Coder.new;
[coder coding];
}
打印如下:
2018-03-17 20:17:30.093302+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_swift]
2018-03-17 20:17:30.093449+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_java]
2018-03-17 20:17:30.093568+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_cpp]
2018-03-17 20:17:30.093675+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_kotlin]
2018-03-17 20:17:30.093805+0800 MyOCDemoProject[36165:5131009]
-[Coder(objc) coding_objc]
2018-03-17 20:17:30.093915+0800 MyOCDemoProject[36165:5131009]
-[Coder coding]
如預(yù)期的所有方法都hook成功,并且按照FILO(first in last out)的順序調(diào)用了一遍。
那么這個(gè)順序?yàn)槭裁词沁@樣的呢?看下面的示意圖就知道了
-
開(kāi)始時(shí)的SEL和IMP的對(duì)應(yīng)關(guān)系
FFC0929C-7071-458A-8998-F57481129BB7.png -
swizzle
coding_objc&coding之后的對(duì)應(yīng)關(guān)系,IMP指針互換
-
swizzle
coding_kotlin&coding之后的對(duì)應(yīng)關(guān)系
因?yàn)橹癱oding的IMP指針已經(jīng)指向了coding_objc的實(shí)現(xiàn),所以在method_exchangeImplementations時(shí)實(shí)際上交換的是coding方法的當(dāng)前IMP指針而不是原始方法實(shí)現(xiàn)對(duì)應(yīng)的IMP指針
384BCB75-6289-4720-97B9-E3B9D833BB2C.png -
后續(xù)的swizzle動(dòng)作也是如此,
method_exchangeImplementations實(shí)際上互換的對(duì)象是前一次swizzle后coding方法對(duì)應(yīng)IMP指針而不是coding方法原始實(shí)現(xiàn)的IMP指針。
最終SEL和IMP對(duì)應(yīng)關(guān)系如下圖所示,并且[coder coding]的整個(gè)執(zhí)行的路徑也在圖中用白線標(biāo)明。
48DCBC28-73A7-4510-809D-A142072033EA.png
看到這里也就不難理解為什么最后會(huì)形成FILO的調(diào)用順序了。
需要注意的是:
- 正常來(lái)說(shuō)swizzle不會(huì)如本文那樣集中在同一個(gè)category的load方法中。方法的實(shí)際執(zhí)行順序就要具體類加載的順序。
- 在使用runtime替換系統(tǒng)的方法的實(shí)現(xiàn)的目的大多是為了hook系統(tǒng)方法方法,插入自己的實(shí)現(xiàn)。如果你的目的是hook,那么務(wù)必要記住在你替換的實(shí)現(xiàn)里,調(diào)用自己一次。否則就會(huì)導(dǎo)致上圖執(zhí)行路徑中,執(zhí)行到你的實(shí)現(xiàn)時(shí)從IMP連到SEL的線斷開(kāi),系統(tǒng)方法得不到調(diào)用。
參考文章:
Runtime中Swizzle時(shí)你可能沒(méi)注意到的問(wèn)題 - 簡(jiǎn)書(shū)
Objective-C Method Swizzling 的最佳實(shí)踐



