前方Swizzling事故多發(fā)箭阶,請小心慎行

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ù)處理的“小聰明”鲫剿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市稻轨,隨后出現(xiàn)的幾起案子灵莲,更是在濱河造成了極大的恐慌,老刑警劉巖殴俱,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件政冻,死亡現(xiàn)場離奇詭異枚抵,居然都是意外死亡,警方通過查閱死者的電腦和手機明场,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門俄精,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人榕堰,你說我怎么就攤上這事竖慧。” “怎么了逆屡?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵圾旨,是天一觀的道長。 經(jīng)常有香客問我魏蔗,道長砍的,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任莺治,我火速辦了婚禮廓鞠,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谣旁。我一直安慰自己床佳,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布榄审。 她就那樣靜靜地躺著砌们,像睡著了一般。 火紅的嫁衣襯著肌膚如雪搁进。 梳的紋絲不亂的頭發(fā)上浪感,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天,我揣著相機與錄音饼问,去河邊找鬼影兽。 笑死,一個胖子當(dāng)著我的面吹牛莱革,可吹牛的內(nèi)容都是我干的峻堰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼驮吱,長吁一口氣:“原來是場噩夢啊……” “哼茧妒!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起左冬,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤新娜,失蹤者是張志新(化名)和其女友劉穎冯丙,沒想到半個月后瞻佛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡狰腌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了牧氮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片琼腔。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖踱葛,靈堂內(nèi)的尸體忽然破棺而出丹莲,到底是詐尸還是另有隱情,我是刑警寧澤尸诽,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布甥材,位于F島的核電站,受9級特大地震影響性含,放射性物質(zhì)發(fā)生泄漏洲赵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一商蕴、第九天 我趴在偏房一處隱蔽的房頂上張望叠萍。 院中可真熱鬧,春花似錦绪商、人聲如沸苛谷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抄腔。三九已至,卻和暖如春理张,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背绵患。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工雾叭, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人落蝙。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓织狐,卻偏偏與公主長得像,于是被迫代替她去往敵國和親筏勒。 傳聞我的和親對象是個殘疾皇子移迫,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容