Method Swizzling,methodswizzling
文章來自小笨狼的iOS部落格,一直覺得csdn的部落格UI不太好看,看部落格不太爽,所以自己搭建了一個部落格。歡迎各位去連結中看我的部落格。也歡迎大家加QQ群討論iOS技術問題
----------------
Time Flies,好久沒寫部落格了,最近一直在玩設計模式,而設計模式這個東西比較大,自認為還沒到將他們寫出來的時候,等再過一陣吧。正好這幾天看了一個好玩的小東西,覺得不錯,所以分享一下。
緣起
相信大家都用過NSMutableDictionary的-setObject:forKey:方法,使用這個方法當object為nil時,將會崩潰。我們寫代碼時,每次都需要對nil進行判斷,非常麻煩。而且有時候忘了對nil進行判斷,很可能就bug留到上線。上線App中經常會遇到這種崩潰
這時候就想用一個方法,替代-setObject:forKey:,當obj為非nil時正常使用-setObject:forKey:,為nil時不執行任何操作。
對於改變一個方法的實現,有三種方案:override,caregory,swizzling
1.Override
重寫是改變一個方法實現的最普遍的用法,首先建立一個CustomDictionary繼承於NSMutableDictionary,然後重寫-setObject:forKey:,在使用的時候我們使用CustomDictionary.
這樣雖然能達到目的,但也非常麻煩,首先我們必須建立一個新的類繼承於NSMutableDictionary,並對所有使用NSMutableDictionary的老代碼,都得進行修改。如果團隊來了新人,必須得叮囑他不能使用NSMutableDictionary,而要用CustomDictionary……
my god,這也太麻煩了,pass~
2.Category
category是OC中非常棒的東西,傳說其他語言都沒有,那麼讓我們看看使用category怎麼樣?
首先category中的這個方法不能跟-setObject:forKey:重名,不然我們就沒法調用系統的這個-setObject:forKey:了,所以我們命名為-safeSetObject:forKey:,在這個方法中對-setObject:forKey進行重寫。
這個方案比重寫好一些,至少我們不需要建立新的類,並且也完全能達到目的,不過對於老代碼的修改,和對新人的囑咐還是必不可少……
其實我們還有更完美的方案
3.Swizzling
iOS中Method Swizzling不太常見,讓我們來詳細看看什麼是Method Swizzling。
一個方法分為2部分:方法名和方法實現,也即是SEL和IMP。一般情況每個方法都會有固定的SEL和IMP,不可分割。
每個類裡面都有一個方法列表,存放著SEL和IMP的映射關係(圖片來源):
Method Swizzling就是將其中2個方法的SEL和IMP對換一下(圖片來源):
讓本來調用selectorC的方法,去執行selectorN的實現。
下面我們來看一下要實現Method Swizzling,到底要做些什嗎?
首先需要建立一個NSMutableDictionary的category,將用來替換-setObject:forKey:的方法寫進去:
@implementation NSMutableDictionary(Override)- (void)overrideSetObject:(id)anObject forKey:(id <NSCopying>)aKey;{ if (anObject) { /** 注意:必須調用自己的方法名 */ [self overrideSetObject:anObject forKey:aKey]; }}@end
大家看到這肯定會非常驚訝,在-overrideSetObject:forKey:方法中竟然調用了自己的方法名,這不是死迴圈了嗎?
不要忘了,這個方法是會Swizzling的,也就是說方法內部調用的這個方法名,會指向系統的實現。這裡沒看明白的朋友,可以跳過,後面我會詳細給大家說清楚
方法有了,現在我們該Swizzling了。
首先要注意的是,Swizzling必須要在執行方法之前,不然方法都執行完了,再Swizzling就沒意義了,其次需要看看要不要考慮多線程的問題。
一般情況下,Swizzling的代碼我們都放在+load方法中,因為+load方法是在啟動時執行的(-application:didFinishLaunchingWithOptions:之前),肯定會在使用之前。並且也不用考慮多線程。
+ (void)load{ /** 擷取SEL和Method */ SEL originSel = @selector(setObject:forKey:); Method originMethod = class_getInstanceMethod([self class], originSel); SEL overrideSel = @selector(overrideSetObject:forKey:); Method overrideMethod = class_getInstanceMethod([self class], overrideSel); /** 分2種case:*/ if (class_addMethod([self class], originSel, method_getImplementation(overrideMethod), method_getTypeEncoding(originMethod))) { /** case1:NSMutableDictionary中沒有-setObject:forKey:的實現 */ class_replaceMethod([self class], overrideSel, method_getImplementation(originMethod), method_getTypeEncoding(originMethod)); }else{ /** case2:NSMutableDictionary中有-setObject:forKey:的實現 */ method_exchangeImplementations(originMethod, overrideMethod); }
對於Swizzling的實現,我們需要分2種case來處理:
case1:NSMutableDictionary中沒有-setObject:forKey:的實現,也許-setObject:forKey:是NSMutableDictionary的父類實現的,我們沒有原始碼,任何情況都可能發生。這種case下。我們需要在NSMutableDictionary中插入-setObject:forKey:方法,再替換他們的IMP部分
case2:NSMutableDictionary中已經存在-setObject:forKey:的實現了,這時候我們就只需要直接替換他們的IMP就ok了
那麼怎麼知道NSMutableDictionary中是否已經存在-setObject:forKey:了呢?很簡單。使用class_addMethod()方法將-setObject:forKey:插入其中,若原來Class未實現,則會插入成功,返回YES,否則返回NO。具體的替換操作的方法我就不詳細解釋了,大家看名字也應該能看明白。
有人可能會問,為什麼要考慮case1這種情況呢?直接全部使用case2不行嗎?答案是不行!因為class_getInstanceMethod()會尋找父類,若原Class未實現,則會替換掉了父類的方法。這並不是我們想要的。
這裡替換掉父類的方法也許影響不大,不過為了保險起見,我們最好還是只替換掉所需Class的方法
這是Swizzling之後的流程圖:
- 外部調用-setObject:forKey:的SEL,會跳轉到-overrideSetObject:forKey:的IMP
- 在-overrideSetObject:forKey:的IMP中我們會調用-overrideSetObject:forKey:的SEL
- -overrideSetObject:forKey:的SEL對應著-setObject:forKey:的IMP
這時候大家應該明白了前面提到的問題:為什麼-overrideSetObject:forKey:的實現中會調用自己了吧?
Finally
Swizzling確實有風險,使用需要謹慎。不過也不能因噎廢食,擔心他的風險而就完全棄之不用,只要我們完全瞭解他的原理,就可以根據具體情況分析是否使用。在這裡,我個人覺得完全可以使用Swizzling替換掉系統的-setObject:forKey:方法,解決線上App,對nil使用-setObject:forKey:崩潰的問題
參考:
Method Replacement for Fun and Profit
Objective-C的hook方案(一): Method Swizzling