Objective-C 中的 Method Swizzling 是一項(xiàng)異常強(qiáng)大的技術(shù)节吮,它可以允許我們動態(tài)地替換方法的實(shí)現(xiàn)脖隶,實(shí)現(xiàn)?Hook?功能,是一種比子類化更加靈活的“重寫”方法的方式。
Method Swizzling 的原理
Method Swizzling 是一把雙刃劍赎瑰,使用得當(dāng)可以讓我們非常輕松地實(shí)現(xiàn)復(fù)雜的功能薄声,而如果一旦誤用当船,它也很可能會給我們的程序帶來毀滅性的傷害。但是我們大可不必驚慌默辨,在了解了它的實(shí)現(xiàn)原理后德频,我們就可以“信手拈來”了。
我們先來了解下 Objective-C 中方法?Method?的數(shù)據(jù)結(jié)構(gòu):
本質(zhì)上廓奕,它就是?struct method_t?類型的指針抱婉,所以我們重點(diǎn)看下結(jié)構(gòu)體?method_t?的定義。在結(jié)構(gòu)體?method_t?中定義了三個(gè)成員變量和一個(gè)成員函數(shù):
1. name?表示的是方法的名稱桌粉,用于唯一標(biāo)識某個(gè)方法,比如?@selector(viewWillAppear:)?衙四;
2. types?表示的是方法的返回值和參數(shù)類型(詳細(xì)信息可以查閱蘋果官方文檔中的?Type Encodings)铃肯;
3. imp?是一個(gè)函數(shù)指針,指向方法的實(shí)現(xiàn)传蹈;
4. SortBySELAddress?顧名思義押逼,是一個(gè)根據(jù)?name?的地址對方法進(jìn)行排序的函數(shù)。
由此惦界,我們也可以發(fā)現(xiàn) Objective-C 中的方法名是不包括參數(shù)類型的挑格,也就是說下面兩個(gè)方法在 runtime 看來就是同一個(gè)方法:
而下面兩個(gè)方法卻是可以共存的:
因?yàn)閷?shí)例方法和類方法是分別保存在類對象和元類對象中的,更多詳情可以查看《Objective-C 對象模型》沾歪。
原則上漂彤,方法的名稱?name?和方法的實(shí)現(xiàn)?imp?是一一對應(yīng)的,而 Method Swizzling 的原理就是動態(tài)地改變它們的對應(yīng)關(guān)系灾搏,以達(dá)到替換方法實(shí)現(xiàn)的目的挫望。
Method Swizzling 有什么用
說了這么多,到底 Method Swizzling 有什么用呢狂窑?表猴急哈媳板,我們接下來看個(gè)例子就明白了。用過友盟統(tǒng)計(jì)的同學(xué)應(yīng)該知道泉哈,要實(shí)現(xiàn)頁面的統(tǒng)計(jì)功能蛉幸,我們需要在每個(gè)頁面的?view controller?中添加如下代碼:
要達(dá)到這個(gè)目的,我們有兩種比較常規(guī)的實(shí)現(xiàn)方式:
1. 直接修改每個(gè)頁面的?view controller?代碼丛晦,簡單粗暴奕纫;
2. 子類化?view controller?,并讓我們的?view controller?都繼承這些子類采呐。
第 1 種方式的缺點(diǎn)是不言而喻的若锁,這樣做不僅會產(chǎn)生大量重復(fù)的代碼,而且還很容易遺漏某些頁面斧吐,非常難維護(hù)又固;第 2 種方式稍微好一點(diǎn)仲器,但是也同樣需要我們子類化?UIViewController?、UITableViewController?和?UITabBarController?等不同類型的?view controller?仰冠。
也許你跟我一樣陷入了思考乏冀,難道就沒有一種簡單優(yōu)雅的解決方案嗎?答案是肯定的洋只,Method Swizzling 就是解決此類問題的最佳方式辆沦。
Method Swizzling 實(shí)踐
下面我們就以替換?viewWillAppear?方法為例談?wù)?Method Swizzling 的最佳實(shí)踐,話不多說识虚,直接上代碼:
解析:在上面的代碼中有三個(gè)關(guān)鍵點(diǎn)需要引起我們的注意:
1. 為什么是在?+load?方法中實(shí)現(xiàn) Method Swizzling 的邏輯肢扯,而不是其他的什么方法,比如?+initialize?等担锤;
2. 為什么 Method Swizzling 的邏輯需要用 dispatch_once 來進(jìn)行調(diào)度蔚晨;
3. 為什么需要調(diào)用?class_addMethod?方法,并且以它的結(jié)果為依據(jù)分別處理兩種不同的情況肛循。
下面我們就一起來分析下這三個(gè)為什么到底是為了什么铭腕?
第 1 個(gè)為什么:看過文章《Objective-C +load vs +initialize》?就會知道,+load?和?+initialize?是 Objective-C runtime 會自動調(diào)用的兩個(gè)類方法多糠。但是它們被調(diào)用的時(shí)機(jī)卻是有差別的累舷,+load?方法是在類被加載的時(shí)候調(diào)用的,而?+initialize?方法是在類或它的子類收到第一條消息之前被調(diào)用的夹孔,這里所指的消息包括實(shí)例方法和類方法的調(diào)用被盈。也就是說?+initialize?方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個(gè)類或它的子類發(fā)送消息析蝴,那么這個(gè)類的?+initialize?方法是永遠(yuǎn)不會被調(diào)用的害捕。此外?+load?方法還有一個(gè)非常重要的特性,那就是子類闷畸、父類和分類中的?+load?方法的實(shí)現(xiàn)是被區(qū)別對待的尝盼。換句話說在 Objective-C runtime 自動調(diào)用?+load?方法時(shí),分類中的?+load?方法并不會對主類中的?+load?方法造成覆蓋佑菩。綜上所述盾沫,+load?方法是實(shí)現(xiàn) Method Swizzling 邏輯的最佳“場所”。
第 2 個(gè)為什么:我們上面提到殿漠,+load?方法在類加載的時(shí)候會被 runtime 自動調(diào)用一次赴精,但是它并沒有限制程序員對?+load?方法的手動調(diào)用。什么绞幌?你說不會有程序員這么干蕾哟?那可說不定,我還見過手動調(diào)用?viewDidLoad?方法的程序員,就是介么任性谭确。而我們所能夠做的就是盡可能地保證程序能夠在各種情況下正常運(yùn)行帘营。
第 3 個(gè)為什么:我們使用 Method Swizzling 的目的通常都是為了給程序增加功能,而不是完全地替換某個(gè)功能逐哈,所以我們一般都需要在自定義的實(shí)現(xiàn)中調(diào)用原始的實(shí)現(xiàn)芬迄。所以這里就會有兩種情況需要我們分別進(jìn)行處理:
第 1 種情況:主類本身有實(shí)現(xiàn)需要替換的方法,也就是?class_addMethod?方法返回?NO?昂秃。這種情況的處理比較簡單禀梳,直接交換兩個(gè)方法的實(shí)現(xiàn)就可以了:
第 2 種情況:主類本身沒有實(shí)現(xiàn)需要替換的方法,而是繼承了父類的實(shí)現(xiàn)肠骆,即?class_addMethod?方法返回?YES?算途。這時(shí)使用?class_getInstanceMethod?函數(shù)獲取到的?originalSelector?指向的就是父類的方法,我們再通過執(zhí)行?class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));將父類的實(shí)現(xiàn)替換到我們自定義的?mrc_viewWillAppear?方法中蚀腿。這樣就達(dá)到了在?mrc_viewWillAppear?方法的實(shí)現(xiàn)中調(diào)用父類實(shí)現(xiàn)的目的郊艘。
看到這里,相信你對 Method Swizzling 已經(jīng)有了一定的了解唯咬,那么接下來就請你自己親自試一試吧,you should give it a try yourself 畏浆。
總結(jié)
Method Swizzling 是一種黑魔法胆胰,我們在使用它時(shí)需要加倍小心。
參考鏈接
http://nshipster.com/method-swizzling/?
https://www.mikeash.com/pyblog/friday-qa-2010-01-29-method-replacement-for-fun-and-profit.html