coding 的演示功能不讓用,原來搭建的博客訪問不了了。索性將全部博客遷移到簡(jiǎn)書,這篇是舊文章,歡迎大家以后來簡(jiǎn)書看我的博客
上一篇博客中我們簡(jiǎn)單介紹了Method Swizzling,看過上一篇文章也許大家會(huì)覺得Method Swizzling is so easy,不過事情沒那么簡(jiǎn)單。有很多細(xì)節(jié)任然需要我們注意!??!
細(xì)節(jié)
在ARC之前,我們經(jīng)常備受內(nèi)存管理的困擾,有時(shí)候一些對(duì)象莫名其妙就被釋放了,都不知道在哪兒釋放的。在任何對(duì)象dealloc的時(shí)候都打印一個(gè)log,這樣只要看log就知道在哪兒被釋放了。由于對(duì)象的類眾多,重寫dealloc工作量巨大,所以我們決定用Swizzling。
上代碼~~~
@implementation NSObject(Swizzling)
void methodSwizzling(Class class,SEL originSel,SEL overrideSel)
{
Method originMethod = class_getInstanceMethod(class, originSel);
Method overrideMethod = class_getInstanceMethod(class, overrideSel);
if (class_addMethod(class,
originSel,
method_getImplementation(overrideMethod),
method_getTypeEncoding(originMethod)))
{
/** case1:NSMutableDictionary中沒有-setObject:forKey:的實(shí)現(xiàn) */
class_replaceMethod(class,
overrideSel,
method_getImplementation(originMethod),
method_getTypeEncoding(originMethod));
}else{
/** case2:NSMutableDictionary中有-setObject:forKey:的實(shí)現(xiàn) */
method_exchangeImplementations(originMethod, overrideMethod);
}
}
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
methodSwizzling([NSObject class], @selector(dealloc), @selector(swzzling_dealloc));
});
}
- (void)swzzling_dealloc
{
printf("我在這里被釋放了");
[self swzzling_dealloc];
}
@end
例子必須在非ARC下運(yùn)行,因?yàn)锧selector(dealloc)在ARC下編譯器會(huì)報(bào)錯(cuò)
例子有些簡(jiǎn)單和幼稚,現(xiàn)實(shí)中可能沒有這樣的需求,不過這都不是重點(diǎn),重點(diǎn)是……
1. +load VS +initialize
有沒有人想過為什么Swizzling的代碼要放在+load中?why?
=> 因?yàn)镾wizzling的代碼必須要在方法執(zhí)行之前,否則方法都執(zhí)行了,再Swizzling就沒有意義了,而+load方法是main函數(shù)之前調(diào)用的(class添加runtime中的時(shí)候調(diào)用),所以可以保證在方法執(zhí)行前Swizzling……
+initialize方法也可以保證在方法執(zhí)行前調(diào)用,那是否可以將Swizzling的代碼放在這個(gè)里面?
=> 放在+initialize里面大多數(shù)時(shí)候是不會(huì)有問題的,不過這樣做會(huì)有風(fēng)險(xiǎn)。我們知道,+initialize方法是在Class接到第一個(gè)消息之前執(zhí)行,也就是說,多個(gè)有繼承關(guān)系的類,他們的+initialize方法執(zhí)行的順序取決于具體的代碼,哪個(gè)類先接收到第一個(gè)消息,哪個(gè)類的+initialize方法就先執(zhí)行。如果這幾個(gè)類都對(duì)同一個(gè)Method執(zhí)行了Swizzling,這就會(huì)導(dǎo)致他們行為的不確定性,我們不知道哪個(gè)Swizzling先執(zhí)行,從而可能會(huì)產(chǎn)生隱藏的難以發(fā)現(xiàn)的bug。而+load執(zhí)行的順序是確定的。父類的+load先執(zhí)行,之后才執(zhí)行子類
2. dispatch_once
顯而易見,如果多個(gè)線程同時(shí)執(zhí)行同一段Swizzling的代碼,有可能造成混亂,產(chǎn)生我們不希望的結(jié)果,所以一般Swizzling的代碼都需要放在dispatch_once中,不過由于系統(tǒng)保證了+load方法不會(huì)同時(shí)執(zhí)行多次,所以在+load中不加dispatch_once也影響不大,不過為了養(yǎng)成良好的代碼系統(tǒng),加上dispatch_once是最好的
3. Swizzling Class Method
一直以來,我們都僅僅是對(duì)InstanceMethod(實(shí)例方法)進(jìn)行Swizzling,如果我們需要對(duì)Class Method(類方法)進(jìn)行Swizzling,那該怎么做呢?
首先我們來看看答案吧:
void classMethodSwizzling(Class class,SEL originSel,SEL overrideSel)
{
Class metalClass = object_getClass(class);
methodSwizzling(metalClass, originSel, overrideSel);
}
我們可以看到,只需要將原來的Class替換成metalClass即可對(duì)ClassMethod進(jìn)行Swizzling。
but why?
要解釋這個(gè),首先我們需要了解一下Class。

Instance,Class,MetalClass的關(guān)系如圖所示,下面2點(diǎn)我需要解釋一下:
- Instance的class指針指向Class,Class的class指針指向MetalClass。簡(jiǎn)單來說可以理解為MetalClass的實(shí)例為Class,Class的實(shí)例為真正的實(shí)例Instance。
- MetalClass的結(jié)構(gòu)與Class的結(jié)構(gòu)完全相同,他們的區(qū)別只是Class中存放實(shí)例方法,MetalClass中存放類方法。
所以首先,object_getClass()方法傳入一個(gè)實(shí)例對(duì)象,返回實(shí)例對(duì)象的Class。由1可知,傳入class,返回MetalClass。
由于Class中只存放實(shí)例方法,所以要對(duì)類方法進(jìn)行Swizzling必須要在MetalClass上進(jìn)行,并且MetalClass和Class的結(jié)構(gòu)完全相同,所以只需要將原來的Class替換成metalClass即可對(duì)ClassMethod進(jìn)行Swizzling。
Danger
大家可能都知道Method Swizzling是有風(fēng)險(xiǎn)的,但是具體風(fēng)險(xiǎn)在哪里,可能大家就不太明確了。我查找了一些資料,收集了一些Swizzling存在風(fēng)險(xiǎn)的地方,如果大家還發(fā)現(xiàn)其他的風(fēng)險(xiǎn),請(qǐng)聯(lián)系我。
1.Refused by AppStore
危險(xiǎn)系數(shù):★★★★★
遭遇概率:★☆☆☆☆
曾經(jīng)出現(xiàn)過由于Swizzling系統(tǒng)API而被AppStore拒絕的事情,不過我在網(wǎng)上查了很長時(shí)間,這個(gè)事件僅發(fā)生了一次,并且后面的人做同樣的事并沒有遭到拒絕,這個(gè)應(yīng)該也和審核的人有關(guān)。一般情況下應(yīng)該是不會(huì)被拒的,所以這個(gè)危險(xiǎn)系數(shù)極高,但是遭遇概率也非常低。事件的詳細(xì)情況看這里
2.Class Cluster
危險(xiǎn)系數(shù):★★★★☆
遭遇概率:★★☆☆☆
類族的概念在上一篇博客里說起過。簡(jiǎn)單的說,類族就是表面上我們似乎只是在用一個(gè)類,例如NSString(NSNumber,NSArray,NSDictionary等類似),實(shí)際我們使用的是他們的子類,如:__NSCFConstantString,NSPathStore2,NSBigMutableString等,由于這些子類并不公開,我們不知道到底有多少子類,以后還會(huì)不會(huì)增加子類,從而Swizzling的時(shí)候無法對(duì)所有子類覆蓋,導(dǎo)致可能有的情況下使用NSString的方法是Swizzling過的,有的情況下調(diào)用的是未Swizzling的方法。所以對(duì)于這種情況,建議放棄使用Method Swizzling。
3. Swizzling changes the method's arguments
危險(xiǎn)系數(shù):★★☆☆☆
遭遇概率:★★★★☆
Swizzling改變了方法的參數(shù)。我們知道,當(dāng)我們調(diào)用一個(gè)方法時(shí)[obj test],系統(tǒng)會(huì)將其轉(zhuǎn)化為消息發(fā)送objc_msgSend(obj,@selector(test)),并將obj和SEL傳送到方法中,在方法中,可以通過_cmd獲取傳入的SEL,通過剛剛的傳統(tǒng)Swizzling方法Swizzling過后,傳入原函數(shù)的SEL改變了,若原函數(shù)中使用了_cmd,就有可能發(fā)生錯(cuò)誤。幸好,這個(gè)風(fēng)險(xiǎn)是可以避免的,通過以下的修改即可避免這個(gè)風(fēng)險(xiǎn)。
- (void)swzzling_dealloc
{
printf("我在這里被釋放了");
// [self swzzling_dealloc];
IMP originImp = class_getMethodImplementation([self class], @selector(swzzling_dealloc));
originImp(self,_cmd);
}
我們直接調(diào)用IMP,將正確的_cmd傳入其中即可。
直接調(diào)用IMP的寫法有些粗魯,過幾天有時(shí)間可以研究一下將其封裝成一個(gè)方法,再過來更新
更新:由于IMP傳入的參數(shù)是可變長參數(shù),因此封裝的方法傳入的參數(shù)必須也為可變長參數(shù),然而目前無法做到將可變長參數(shù)專遞給另一函數(shù),所以這個(gè)地方暫時(shí)無法封裝成一個(gè)方法。如果你有什么其他辦法可以做到,請(qǐng)聯(lián)系我
4.others
這里還列出了一些其他的風(fēng)險(xiǎn),念茜大神在這里對(duì)他進(jìn)行了翻譯,我就不在這里贅述了。感興趣的朋友可以看看,里面還提供了另外一種Method Swizzling的方法,同樣也可以解決3中這個(gè)問題。
參考
Method Swizzling in codeproject