Swizzling作為運行時黑魔法,確實為很多問題的分析和處理提供了便利嘹叫,本文首先援引他山之石記錄Swizzling的具體用法和模板罩扇,進而通過一組實驗分析了其在工程實踐中的危險性怕磨。
Method Swizzling原理
Method Swizzing是發(fā)生在運行時的,主要用于在運行時將兩個Method進行交換员帮,我們可以將Method Swizzling代碼寫到任何地方捞高,但是只有在這段Method Swilzzling代碼執(zhí)行完畢之后互換才起作用渣锦。而且Method Swizzling也是iOS中AOP(面相切面編程)的一種實現(xiàn)方式,我們可以利用蘋果這一特性來實現(xiàn)AOP編程辈讶。
Method Swizzling本質(zhì)上就是對IMP和SEL進行交換娄猫。
一般我們使用都是新建一個分類媳溺,在分類中進行Method Swizzling方法的交換。交換的代碼模板如下:
#import <objc/runtime.h>
@implementation UIViewController (Swizzling)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
// When swizzling a class method, use the following:
// Class class = object_getClass((id)self);
SEL originalSelector = @selector(viewWillAppear:);
SEL swizzledSelector = @selector(xxx_viewWillAppear:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class,
originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod) {
class_replaceMethod(class,
swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
#pragma mark - Method Swizzling
- (void)xxx_viewWillAppear:(BOOL)animated {
[self xxx_viewWillAppear:animated];
NSLog(@"viewWillAppear: %@", self);
}
@end
Method Swizzling可以在運行時通過修改類的方法列表中selector對應(yīng)的函數(shù)或者設(shè)置交換方法實現(xiàn),來動態(tài)修改方法蝎困『坛耍可以重寫某個方法而不用繼承,同時還可以調(diào)用原先的實現(xiàn)蒲稳。所以通常應(yīng)用于在category中添加一個方法。
注意要點:
1.Swizzling應(yīng)該總在+load中執(zhí)行
Objective-C在運行時會自動調(diào)用類的兩個方法+load和+initialize江耀。+load會在類初始加載時調(diào)用剩胁, +initialize方法是以懶加載的方式被調(diào)用的,如果程序一直沒有給某個類或它的子類發(fā)送消息祥国,那么這個類的 +initialize方法是永遠不會被調(diào)用的昵观。所以Swizzling要是寫在+initialize方法中,是有可能永遠都不被執(zhí)行系宫。
和+initialize比較+load能保證在類的初始化過程中被加載索昂。
關(guān)于+load和+initialize的比較可以參看這篇文章《Objective-C +load vs +initialize》
2.Swizzling應(yīng)該總是在dispatch_once中執(zhí)行
Swizzling會改變?nèi)譅顟B(tài)建车,所以在運行時采取一些預(yù)防措施扩借,使用dispatch_once就能夠確保代碼不管有多少線程都只被執(zhí)行一次。這將成為Method Swizzling的最佳實踐缤至。
這里有一個很容易犯的錯誤,那就是繼承中用了Swizzling领斥。如果不寫dispatch_once就會導(dǎo)致Swizzling失效嫉到!
舉個例子,比如同時對NSArray和NSMutableArray中的objectAtIndex:方法都進行了Swizzling月洛,這樣可能會導(dǎo)致NSArray中的Swizzling失效的何恶。
可是為什么會這樣呢?
原因是嚼黔,我們沒有用dispatch_once控制Swizzling只執(zhí)行一次细层。如果這段Swizzling被執(zhí)行多次,經(jīng)過多次的交換IMP和SEL之后唬涧,結(jié)果可能就是未交換之前的狀態(tài)疫赎。
比如說父類A的B方法和子類C的D方法進行交換,交換一次后碎节,父類A持有D方法的IMP捧搞,子類C持有B方法的IMP,但是再次交換一次狮荔,就又還原了胎撇。父類A還是持有B方法的IMP,子類C還是持有D方法的IMP殖氏,這樣就相當(dāng)于咩有交換晚树。可以看出受葛,如果不寫dispatch_once题涨,偶數(shù)次交換以后偎谁,相當(dāng)于沒有交換,Swizzling失效纲堵!
3.Swizzling在+load中執(zhí)行時巡雨,不要調(diào)用[super load]
原因同注意點二,如果是多繼承席函,并且對同一個方法都進行了Swizzling铐望,那么調(diào)用[super load]以后,父類的Swizzling就失效了茂附。
4.上述模板中沒有錯誤
有些人懷疑我上述給的模板可能有錯誤正蛙。在這里需要講解一下。
在進行Swizzling的時候营曼,我們需要用class_addMethod先進行判斷一下原有類中是否有要替換的方法的實現(xiàn)乒验。
如果class_addMethod返回NO,說明當(dāng)前類中有要替換方法的實現(xiàn)蒂阱,所以可以直接進行替換锻全,調(diào)用method_exchangeImplementations即可實現(xiàn)Swizzling。
如果class_addMethod返回YES录煤,說明當(dāng)前類中沒有要替換方法的實現(xiàn)鳄厌,我們需要在父類中去尋找。這個時候就需要用到method_getImplementation去獲取class_getInstanceMethod里面的方法實現(xiàn)妈踊。然后再進行class_replaceMethod來實現(xiàn)Swizzling了嚎。
這是Swizzling需要判斷的一點。
還有一點需要注意的是廊营,在我們替換的方法- (void)xxx_viewWillAppear:(BOOL)animated中歪泳,調(diào)用了[self xxx_viewWillAppear:animated];這不是死循環(huán)了么?
其實這里并不會死循環(huán)赘风。
由于我們進行了Swizzling夹囚,所以其實在原來的- (void)viewWillAppear:(BOOL)animated方法中,調(diào)用的是- (void)xxx_viewWillAppear:(BOOL)animated方法的實現(xiàn)邀窃。所以不會造成死循環(huán)荸哟。相反的,如果這里把[self xxx_viewWillAppear:animated];改成[self viewWillAppear:animated];就會造成死循環(huán)瞬捕。因為外面調(diào)用[self viewWillAppear:animated];的時候鞍历,會交換方法走到[self xxx_viewWillAppear:animated];這個方法實現(xiàn)中來,然后這里又去調(diào)用[self viewWillAppear:animated]肪虎,就會造成死循環(huán)了劣砍。
所以按照上述Swizzling的模板來寫,就不會遇到這4點需要注意的問題啦扇救。
================不知道這個分割線夠不夠長==============
以上精彩論述摘自神經(jīng)病院Objective-C Runtime出院第三天——如何正確使用Runtime
誠然這也是我一直使用的Swizzling模板刑枝,不過當(dāng)讀至上文有關(guān)繼承中的Swizzle時香嗓,忽然才意識到自己從未對此問題進行過細(xì)致的思考和實驗,直觀上覺得即便使用dispatch_once装畅,也無法限制繼承關(guān)系中各個子類的Swizzle不會交叉在一起靠娱。
閑話少說,一組實驗如下:
- 實驗1
Class A
|---MethodA
Class B : ClassA
|---MethodA(重寫)
Category A
|---+ (void)load 中進行Swizzle: A switch with B
|---MethodB : 調(diào)用MethodB
Category B
|---+ (void)load 中進行Swizzle: A switch with C
|---MethodC : 調(diào)用MethodC
Invoker:
ClassB *B = [ClassB new];
[B MethodA];
本次實驗得到的結(jié)果為:
打印順序:MethodA->MethodC掠兄,
由此可見像云,子類重寫的MethodA方法得到調(diào)用,之后正常打印MethodC中后面的信息蚂夕。
- 實驗2
Class A
|---MethodA
Category A
|---+ (void)load 中進行Swizzle: A switch with B
|---MethodB : 調(diào)用MethodB
Category B
|---+ (void)load 中進行Swizzle: A switch with C
|---MethodC : 調(diào)用MethodC
Invoker:
ClassB *B = [ClassB new];
[B MethodA];
本次實驗在子類中不去重寫父類方法迅诬。
本次實驗得到的結(jié)果為:
打印順序: MethodA ->MethodB->MethodC,
由此可見婿牍,父類和子類的Swizzle都生效了侈贷。
結(jié)論
- class_getInstanceMethod在本類中沒有找到相應(yīng)SEL的實現(xiàn)會去父類中繼續(xù)查找。(本句結(jié)論文檔可查牍汹,看似廢話但對于實驗結(jié)果的理解頗為重要)
- 父類Load方法先于子類Load方法執(zhí)行铐维,雖然load方法底層直接是C的實現(xiàn)柬泽,不遵循消息傳遞機制慎菲,但如果父類子類都實現(xiàn)load了的話,其執(zhí)行順序還是確定的锨并。
- A先與B交換露该,A再與C交換。造成的結(jié)果是SEL A -> IMP C, SEL C -> IMP B, SEL B -> IMP A. 由此才會在子類調(diào)用MethodA時第煮,出現(xiàn)MethodC ->MethodB->MethodA的情形解幼,導(dǎo)致執(zhí)行順序為A->B->C.
危險慎行
由于Swizzle屬于運行時黑魔法,其影響是全局有效包警,不利于在調(diào)試是發(fā)現(xiàn)問題撵摆,其較為松散的代碼關(guān)系和不太透明的底層執(zhí)行順序容易造成不可預(yù)期的邏輯錯誤,如本文實驗中的繼承關(guān)系害晦。因此在項目實際應(yīng)用中特铝,Swizzle最好只在調(diào)試,埋點等場景中使用壹瘟,盡量不要用它來做一些方便業(yè)務(wù)處理的“小聰明”鲫剿。