背景
? ?有一天贡这,項(xiàng)目里要替換整個(gè)項(xiàng)目里一個(gè)方法的所有的實(shí)現(xiàn),老大說(shuō):“如果是你你怎么辦厂榛?這就像個(gè)面試題”盖矫,我在那里回答"繼承"丽惭、methodSignature、refactor辈双、一堆答案沒(méi)有一個(gè)命中责掏。老大給了我一個(gè)methodSwizzing的連接,于是就有了這個(gè)文章
問(wèn)題
一個(gè)常見(jiàn)的場(chǎng)景就是湃望,當(dāng)我們想對(duì)大量同樣的方法執(zhí)行某個(gè)相同的操作换衬。例如我們想在每個(gè)ViewController的viewDidLoad方法中添加某行代碼,當(dāng)然我們可以通過(guò)繼承证芭、讓UIViewController瞳浦、UINavigationController等都實(shí)現(xiàn)這個(gè)方法。這樣废士,它們的子類也就會(huì)執(zhí)行這個(gè)方法叫潦。但是,這樣會(huì)導(dǎo)致我們需要在各種ViewController中添加代碼官硝,而且如果有的子類并沒(méi)有調(diào)用父類的viewDidLoad矗蕊,那就覆蓋不到這個(gè)子類。此外泛源,在視圖控制器的生命周期拔妥,響應(yīng)事件,繪制視圖等場(chǎng)景中达箍,都會(huì)由這個(gè)需求没龙,這個(gè)時(shí)候method swizzling 就能夠?yàn)殚_(kāi)發(fā)帶來(lái)很好的作用。
示例代碼
?最常見(jiàn)的hook代碼
+ (void)swizzMethod:(SEL)origSel altMethod:(SEL)altSel {
????????Method?origMethod?=class_getInstanceMethod(self,origSel);
????????Method altMethod = class_getInstanceMethod(self, altSel);
????????BOOL didAddMethod =?class_addMethod([self class],origSel, ????????method_getImplementation(altMethod),method_getTypeEncoding(altMethod));
????????if (didAddMethod) {
????????????class_replaceMethod([self class], ????????????altSel,method_getImplementation(origMet),method_getTypeEncoding(origMet))
????????????} else {
????????????method_exchangeImplementations(class_getInstanceMethod(self, origSel_), ????????????class_getInstanceMethod(self, altSel_));
????????? }
}
代碼解釋
我看到這個(gè)代碼的時(shí)候缎玫,除了認(rèn)得字母外硬纤,一無(wú)所知??所以我首先查了下里面的每行代碼做了什么。
首先代碼中使用到了class_getInstanceMethod(self, origSel)這個(gè)方法用來(lái)返回一個(gè)class中指定的實(shí)例方法赃磨,如果class或者它的superclass不存在對(duì)應(yīng)的實(shí)例方法筝家,就會(huì)返回NULL
然后代碼中使用了class_addMethod這個(gè)方法用來(lái)給一個(gè)class添加一個(gè)指定名稱和實(shí)現(xiàn)的方法: BOOL?class_addMethod(Class cls, SEL name, IMP imp, const char *types);
a)???方法一共有四個(gè)參數(shù):class是要添加方法的class,name是方法的name邻辉,imp是方法的實(shí)現(xiàn),實(shí)現(xiàn)里至少需要兩個(gè)參數(shù)self和_cmd溪王。?types是描述參數(shù)類型的字符串構(gòu)成的一個(gè)數(shù)組
b)???方法會(huì)重寫superclass's?實(shí)現(xiàn),但是不會(huì)替換這個(gè)類里已有的實(shí)現(xiàn)
c)???一個(gè)Objective-C?方法就是一個(gè)至少有兩個(gè)參數(shù)的c方法。例如對(duì)于一個(gè)給定的方法:
????????i.?void myMethodIMP(id self, SEL _cmd){ // implementation ....}
d)???我們可以像下面的方法一樣動(dòng)態(tài)的將它以resolveThisMethodDynamicall名字添加到class中
????????i.?class_addMethod([self class], @selector(resolveThisMethodDynamically), (IMP) myMethodIMP, "v@:");
知道了以上兩個(gè)基礎(chǔ)值骇,就可以看到莹菱。實(shí)際上methodSwizzling是先添加了名稱是被替換方法的一個(gè)指定的替換后的方法
然后方法調(diào)用了一個(gè)class_replaceMethod函數(shù),函數(shù)共四個(gè)參數(shù),第一個(gè)參數(shù)是要修改的方法所在的類吱瘩,第二個(gè)參數(shù)是要替換實(shí)現(xiàn)的函數(shù)名稱道伟,之后分別傳入方法實(shí)現(xiàn)和方法參數(shù)的字符串?dāng)?shù)組。同時(shí)方法會(huì)返回被替換函數(shù)的之前實(shí)現(xiàn)。
a)???如果函數(shù)名稱指定的方法之前不存在蜜徽,函數(shù)就會(huì)像method_add一樣為類添加一個(gè)方法祝懂。
b)???如果函數(shù)名稱指定的方法存在,函數(shù)就像method_setImplementation替換函數(shù)的實(shí)現(xiàn)
?知道了這點(diǎn)就可以看到拘鞋,方法替換函數(shù)第二步在成功添加了被替換函數(shù)同名的替換方法后砚蓬,會(huì)將替換函數(shù)名對(duì)應(yīng)的函數(shù)實(shí)現(xiàn)替換為之前的函數(shù)實(shí)現(xiàn)。如果添加函數(shù)不成功盆色,也就是類之前就存在一個(gè)同名的函數(shù)實(shí)現(xiàn)時(shí)會(huì)調(diào)用method_exchangeImplementations函數(shù)怜械,函數(shù)接收兩個(gè)?Method?類型參數(shù)。然后交換它們的實(shí)現(xiàn)傅事。所以缕允,methodSwizzling實(shí)際上相當(dāng)于將添加后的函數(shù)實(shí)現(xiàn)和原先的函數(shù)實(shí)現(xiàn),也就是IMP指針進(jìn)行了替換蹭越。
上述的代碼是寫在類本身的障本,當(dāng)然我們也可以在category中對(duì)某個(gè)類進(jìn)行方法替換,那么需要在load方法中調(diào)用上面swizzMethod:(SEL)origSel altMethod:(SEL)altSel相似的函數(shù)响鹃。只不過(guò)其中的實(shí)現(xiàn)需要改成先調(diào)用兩次class_addMethod將被替換函數(shù)和替換函數(shù)都加入到類中驾霜,然后,調(diào)用method_exchangeImplementations方法將兩個(gè)函數(shù)的實(shí)現(xiàn)進(jìn)行替換买置。
注意事項(xiàng)
首先看看load?和?initialize方法:swizzling?應(yīng)該只在?+load?中完成粪糙。
a)???在?Objective-C?的運(yùn)行時(shí)中,每個(gè)類有兩個(gè)方法都會(huì)自動(dòng)調(diào)用忿项。+load?是在一個(gè)類被初始裝載時(shí)調(diào)用蓉冈,也就是當(dāng)代碼還沒(méi)有被load的時(shí)候代碼從文件夾的代碼加載到運(yùn)行的程序中時(shí)候會(huì)調(diào)用load方法
b)???+initialize?是在應(yīng)用第一次調(diào)用該類的類方法或?qū)嵗椒ㄇ罢{(diào)用的。兩個(gè)方法都是可選的轩触,并且只有在方法被實(shí)現(xiàn)的情況下才會(huì)被調(diào)用寞酿。
只調(diào)用一次
swizzling方法會(huì)替換所有同名的方法的實(shí)現(xiàn),所以脱柱,應(yīng)該確保這個(gè)方法只執(zhí)行一次伐弹,不會(huì)發(fā)生多次替換的情況,這個(gè)時(shí)候就可以用dispatch_once榨为,這個(gè)可以滿足所有的需求惨好,而且?dispatch_once也是初始化一個(gè)單例方法的標(biāo)準(zhǔn)方法,但是這種方式進(jìn)行的更改僅限于method所在的類,比如在UIView的子類B中進(jìn)行方法替換随闺,那么方法替換將僅限于B及B的子類日川。
Selector、SEL和IMP的概念
Selector?是一個(gè)在運(yùn)行時(shí)被注冊(cè)(或映射)的C類型字符串板壮。Selector由編譯器產(chǎn)生并且在當(dāng)類被加載進(jìn)內(nèi)存時(shí)由運(yùn)行時(shí)自動(dòng)進(jìn)行名字和實(shí)現(xiàn)的映射逗鸣。Method是用來(lái)表示函數(shù)定義的類型的一個(gè)結(jié)構(gòu)體合住,IMP是一個(gè)函數(shù)指針绰精,該方法的第一個(gè)參數(shù)指向調(diào)用方法的自身(即內(nèi)存中類的實(shí)例對(duì)象撒璧,若是調(diào)用類方法,該指針則是指向元類對(duì)象?metaclass)笨使。第二個(gè)參數(shù)是這個(gè)方法的名字?selector卿樱,該方法的真正參數(shù)緊隨其后。
防止遞歸
使用的時(shí)候硫椰,在交換方法實(shí)現(xiàn)后記得要調(diào)用原生方法的實(shí)現(xiàn)繁调,除非確定可以不用調(diào)用原生實(shí)現(xiàn),如果不調(diào)用靶草,可能會(huì)導(dǎo)致一些底層實(shí)現(xiàn)錯(cuò)誤例如下面這段代碼蹄胰。
a)???- (void) altSel {
???[self altSel];
}
b)???看起來(lái)這里會(huì)發(fā)生遞歸調(diào)用,但是 實(shí)際上因?yàn)橥鈱拥腶ltSel已經(jīng)替換成了origSel所以實(shí)際上是origSel在調(diào)用altSel?但是如果里邊換成[self origSel]?就會(huì)發(fā)生遞歸調(diào)用
防止父類和子類同時(shí)替換
應(yīng)該在替換的方法前增加前綴奕翔,如果不在load方法中添加替換裕寨,而是在caterory中執(zhí)行,因?yàn)閏ategory中的方法是在Runtime加載的時(shí)候加到類的MethodList,這就會(huì)導(dǎo)致如果altSel重名之后派继,SEL和IMP不匹配宾袜,導(dǎo)致hook的結(jié)果不對(duì)
被hook的方法應(yīng)該是類自身的方法,如果把繼承的IMP copy到自身上面會(huì)存在問(wèn)題驾窟,父類的方法應(yīng)該在調(diào)用的時(shí)候使用庆猫,不應(yīng)該swizzling的時(shí)候copy到子類,如果父類也hock了方法绅络。子類也hook了方法月培,父類hook的方法可能由于compileSourc中子類category順序在父類category之前,而導(dǎo)致只調(diào)用子類的hook方法,或者兩者都不調(diào)用恩急,舉個(gè)形象的例子节视, UIView有一個(gè)方法,實(shí)現(xiàn)叫做A假栓, UIView將方法的實(shí)現(xiàn)替換成了B寻行,這個(gè)時(shí)候,UIView的子類又去替換這個(gè)A原先方法名的實(shí)現(xiàn)匾荆,那么它替換的將是B拌蜘。也就是UIView的子類就沒(méi)有A的實(shí)現(xiàn)了。