警惕swizzling

ofollow,noindex">原文連結
不知道什麼時候開始,只要使用了 swizzling
都能被解讀成是 AOP
開發,開發者張口嘴就是 runtime
,將其高高捧起,稱之為 黑魔法
;以專案中各種 method_swizzling
為榮,卻不知道這種做法破壞了程式碼的整體性,使關鍵邏輯支離破碎。本文基於 iOS界的毒瘤 一文,從另外的角度談談為什麼我們應當 警惕
呼叫順序性
呼叫順序性
是連結文章講述的的核心問題,它會破壞方法的原有執行順序,導致意料之外的錯誤。先從一段簡單的程式碼聊起:
@interface SLTestObject: NSObject @end @implementation SLTestObject - (instancetype)init { self = [super init]; return self; } @end void testIsSelectorSame() { Method allocate1 = class_getClassMethod([NSObject class], @selector(alloc)); Method allocate2 = class_getClassMethod([SLTestObject class], @selector(alloc)); Method initialize1 = class_getInstanceMethod([NSObject class], @selector(init)); Method initialize2 = class_getInstanceMethod([SLTestObject class], @selector(init)); assert(allocate1 == allocate2 && initialize1 != initialize2); }
這段程式碼的目的是證明一個定論:
如果子類沒有重寫父類宣告的方法,在子類物件呼叫該方法時,執行的是父類實現的程式碼
基於這一定論,假定一個場景:現在通過無埋點方案統計使用者進入和離開 Controller
次數:
@implementation UIViewController (SLCount) + (void)load { sl_swizzle([self class], @selector(viewWillAppear:), @selector(sl_viewWillAppearI:)); sl_swizzle([self class], @selector(viewDidDisappear:), @selector(sl_viewDidDisappearI:)); } - (void)sl_viewWillAppearI: (BOOL)animated { [SLControllerCounter countControllerEnter: [self class]]; [self sl_viewWillAppearI: animated]; } - (void)sl_viewDidDisappearI: (BOOL)animated { [SLControllerCounter countControllerLeave: [self class]]; [self sl_viewDidDisappearI: animated]; } @end
由於 UIViewController
是所有控制器的父類,所以理論上只要 swizzle
這個類就能統計到所有控制器的資訊。同時專案中存在一個定製的基礎控制器 SLBaseViewController
存在這麼一段程式碼:
@implementation SLBaseViewController (SLCount) + (void)load { sl_swizzle([self class], @selector(viewWillAppear:), @selector(sl_viewWillAppearII:)); sl_swizzle([self class], @selector(viewDidDisappear:), @selector(sl_viewDidDisappearII:)); } - (void)sl_viewWillAppearII: (BOOL)animated { [self prepareRequest]; [self sl_viewWillAppearII: animated]; } - (void)sl_viewDidDisappearII: (BOOL)animated { [self sl_viewDidDisappearII: animated]; [self cancelAllRequests]; } @end
但是這兩段程式碼卻在特定的場景下發生 crash
,發生異常的原因在於子類在沒有重寫方法的情況下,子類先於父類進行了 swizzle
的操作。 iOS
使用中方法名稱 SEL
和方法實現 IMP
是分開存放的,使用結構體 Method
將兩者關聯到一起:
typedef struct Method { SEL name; IMP imp; } Method;
交換方法會將兩個 method
中的 imp
進行交換。而在理想情況下,父類先於子類完成了 swizzle
,原有方法儲存了 swizzle
之後的 imp
,這時候子類再進行 swizzle
就能正確呼叫。下圖示識了 SEL
和 IMP
的關聯,箭頭表示 IMP
的呼叫次序:

但是如果子類的 swizzle
發生的更早,這時候 viewWillAppear
對應的 imp
已經被修改,父類再進行 swizzle
的時候,呼叫次序已經出錯:

解決方式也並不複雜,包括:
- 在
swizzle
之前先addMethod
,保證子類不沿用父類的預設實現 - 每次呼叫通過
sel
去獲取imp
執行
具體的實現程式碼可以參考 iOS界的毒瘤 的解決方案
行為衝突
在 OOP
的設計中,將描述物件抽象成類,將物件行為抽象成介面。從工程師的角度來說,職責單一的介面更利於迭代維護。類一旦設計好,應當不改動或者少改動介面。對於設計良好的介面來說, swizzle
很可能直接破壞了整個介面的行為:

舉個例子, crash防護
是當下被追捧的工具,但其中 KVO
的防護或許是一種很爛的手段。從實現來說,為了避免 KVO
導致的迴圈引用,需要在引用關係的中間插入一個 weakProxy
來做防護,因此監聽程式碼實際上可以轉換成:
// 表面程式碼 [observedObj addObserver: self forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; // 實際效果 WeakProxy *proxy = [WeakProxy new]; proxy.client = self; [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil];
為什麼說這種設計很爛的?一旦客戶端出現這樣的程式碼:
- (void)dealloc { ...... [observedObj removeObserver: self forKeyPath: keyPath]; }
通常情況下,以現在的多數 防護工具
的實現,都會發生崩潰。對於 swizzle
程式碼外的使用者來說,或許根本不清楚 observer
早已發生了轉移,導致了原有的正確調用出錯。解決方案之一是對 remove
介面同樣進行 swizzle
,使得兩次呼叫的監聽物件配套:
- (void)sl_removeObserver: (id)observer forKeyPath: (NSString *)keyPath { [self sl_removeObserver: observer.proxy forKeyPath: keyPath]; }
然而這樣做之後,首先 KVO
的行為已經被修改,介面被破壞可能導致潛在的隱患。其次,如果存在多個防護工具,如果按照 weakProxy
的實現,那麼一旦有 2
個或者更多的防護時, KVO
功能將失效:
OneWeakProxy *proxy = [OneWeakProxy new]; proxy.client = self; [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil]; TwoWeakProxy *proxy = [TwoWeakProxy new]; proxy.client = self;/// self is OneWeakProxy [observedObj addObserver: proxy forKeyPath: keyPath options: NSKeyValueObservingOptionNew context: nil];
在第二次生成 WeakProxy
後並呼叫方法後, OneWeakProxy
建立的物件被釋放。如果要避免多個防護工具對流程造成干擾,還需要做更多額外的工作。況且一旦有其中一個沒有完美實現,整套 防護機制
可能就直接崩潰失效了,因此 KVO防護
不見得是一種好手段

程式碼整體性
以上面例子來說, KVO
是 NSObject
這個基類提供的能力,由於 子類預設沿用父類的方法實現
這一原則,這種方法的 swizzle
實際上影響了全部的物件,例如下面的程式碼實際上效果是完全一樣的:
/// swizzle 1 void swizzleTableView() { Method ori = class_getClassMethod([UITableView class], @selector(addObserver:forKeyPath:options:context:)); Method cus = class_getClassMethod([UITableView class], @selector(sl_addObserver:forKeyPath:options:context:)); method_exchange(ori, cus); } /// swizzle 2 void swizzleObj() { Method ori = class_getClassMethod([NSObject class], @selector(addObserver:forKeyPath:options:context:)); Method cus = class_getClassMethod([NSObject class], @selector(sl_addObserver:forKeyPath:options:context:)); method_exchange(ori, cus); }
而第一個方法由於預設實現是 NSObject
的,因此一旦發生了 swizzle
所有的物件都會生效,這存在兩個問題:
- 非
UITableView
物件依舊受到了KVO
的攔截影響 - 沒有
sl_addObserver:forKeyPath:options:context:
的物件會發生崩潰
另一方面,類的介面設計總是偏向於 裝扮模式
的思維,不同層級的類物件在自己的方法被呼叫起時會執行自身特有的工作,這種設計讓繼承有足夠的靈活性,從 viewDidLoad
的實現程式碼可見一斑:
- (void)viewDidLoad { [super viewDidLoad]; /// setup work }
換句話說,以這種 裝扮模式
思維來構建的程式碼,如果中間的一個方法被影響甚至破壞了,在中間的這個類開始往下將呈現塌式破壞,可以想象如果 UIView
一旦出錯,應用幾乎喪失展示控制元件的能力。但假如確實需要 swizzle
的中間環節,必須保證 swizzle
不對或者儘量少地對子類物件造成影響