Objective-C的方法替換
(Method Replacement for Fun and Profit)
本文將要討論Objective-C中的方法替換(method replacement)和swizzling(移魂大法)。
重寫類的方法(Overriding Methods)
Overriding methods在任何面向對象語言中都很常見,主要用於子類化中。在子類中復寫一個方法,然後在子類的實例就可以使用這個被重寫的方法。
對於一個你無法控制其實例化(instantiation)的類,有時你或許會想復寫它的某個方法,雖然有點瘋狂。子類化可做不到,因為你沒有機會子類化你的子類。
偽裝(Posing)
Posing是個很有趣的技術,不過已經過時了,因為64位和iPhone環境下的Objective-C Runtime中不再支持它了. 通過這個偽裝(posing),你可子類化,然後將這個子類偽裝成它的父類。像變魔術一般,Runtime會讓這個子類應用於各處,這時方法復寫又有了用處。既然被拋棄了,也就不必多費口舌了。
歸類(Categories)
使用歸類(category)的技術,可以方便地為一個已經存在的類復寫其方法:
@implementationNSView(MyOverride)
- (void)drawRect: (NSRect)r
{ www.2cto.com
// 這個會替換掉通常使用的-[NSView drawRect:]
[[NSColor blueColor]set];
NSRectFill(r);
}
@end
這種方法其實僅僅適用於復寫目標類的父類中實現的函數。如果直接復寫目標類中的方法,使用歸類會帶來兩個問題:
它無法調用方法的之前的實現。替換掉後,之前的實現就被完全改寫了。但大部分情況下,只是想增加些功能,並不期望完全替代。
如果被多個category復寫,運行時(runtime)並不保證哪個真正會被使用到。
Swizzling (譯為“移魂大法”比較合適,就是太誇張了!)
使用一個稱為swizzling的技術,可以為歸類(category)解決上面兩個問題,既可以調用舊的實現,又可以避免多個category帶來的不確定性。它的秘訣是使用一個不同的函數名來復寫,然後由運行時(runtime)交換它們。
首先,用一個不同的名字復寫:
@implementationNSView(MyOverride)
- (void)override_drawRect: (NSRect)r
{
// 調用舊的實現。因為它們已經被替換了
[self override_drawRect: r];
[[NSColor blueColor]set];
NSRectFill(r);
}
@end
(譯注:呵呵,不知道你是不是和我一樣,初次看到代碼還以為是個遞歸調用呢。) 其實是這個新的方法在執行時已經和原先的函數對調了(現在還沒做到,往下看!)。在運行時,調用 override_drawRect: 方法其實就是調用舊的實現。
接下來,你還要寫些代碼才能完成交換:
voidMethodSwizzle(Class c,SEL origSEL,SEL overrideSEL)
{
Method origMethod = class_getInstanceMethod(c, origSEL);
Method overrideMethod= class_getInstanceMethod(c, overrideSEL);
周全起見,有兩種情況要考慮一下。第一種情況是要復寫的方法(overridden)並沒有在目標類中實現(notimplemented),而是在其父類中實現了。第二種情況是這個方法已經存在於目標類中(does existin the class itself)。這兩種情況要區別對待。
(譯注: 這個地方有點要明確一下,它的目的是為了使用一個重寫的方法替換掉原來的方法。但重寫的方法可能是在父類中重寫的,也可能是在子類中重寫的。)
對於第一種情況,應當先在目標類增加一個新的實現方法(override),然後將復寫的方法替換為原先(的實現(original one)。
運行時函數class_addMethod 如果發現方法已經存在,會失敗返回,也可以用來做檢查用:
if(class_addMethod(c, origSEL, method_getImplementation(overrideMethod),method_getTypeEncoding(overrideMethod)))
{
如果添加成功(在父類中重寫的方法),再把目標類中的方法替換為舊有的實現:
class_replaceMethod(c,overrideSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
}
(譯注:addMethod會讓目標類的方法指向新的實現,使用replaceMethod再將新的方法指向原先的實現,這樣就完成了交換操作。)
如果添加失敗了,就是第二情況(在目標類重寫的方法)。這時可以通過method_exchangeImplementations來完成交換:
else
{
method_exchangeImplementations(origMethod,overrideMethod);
}
}
對於第二種情況,因為class_getInstanceMethod 會返回父類的實現,如果直接替換,就會替換掉父類的實現,而不是目標類中的實現。(詳細的函數說明在這裡)
舉個具體的例子, 假設要替換掉-[NSView description]. 如果NSView 沒有實現-description (可選的) 那你就可會得到NSObject的方法。如果調用method_exchangeImplementations , 你就會把NSObject 的方法替換成你的代碼。這應該不會是你想要的吧?
最後在一個合適位置調用一下就可以了。比如在一個+load 方法中調用:
+ (void)load
{
MethodSwizzle(self,@selector(drawRect:),@selector(override_drawRect:));
}
直接重寫(Direct Override)
前面的內容確實有些難懂。Swizzling的概念的確顯得有些古怪,特別是在函數中轉來轉去的,多少讓人有些思維扭曲的感覺。我下面要介紹一個更為簡潔,也更容易理解和實現的方式。
這種方式不再需要保存舊有的方法,也不必動態的區分[self override_drawRect: r] 。我們從頭實現。
相對於將原有的方法存放於一個新的方法中,這裡使用一個全局指針來保存:
void (*gOrigDrawRect)(id,SEL, NSRect);
然後在+load 裡賦值:
+ (void)load
{
Method origMethod = class_getInstanceMethod(self,@selector(drawRect:));
gOrigDrawRect = (void*)method_getImplementation(origMethod);
(我喜歡把它轉換為 void *,因為比那些又長又奇怪的函數指針好輸入多了。)
然後像前面介紹的那樣用新的實現替換掉就可以了。因為class_replaceMethod本身會嘗試調用class_addMethod和method_setImplementation,所以直接調用class_replaceMethod就可以了。
實現如下:
Method origMethod =class_getInstanceMethod(self, @selector(drawRect:));
gOrigDrawRect = (void *)class_replaceMethod(self,@selector(drawRect:), (IMP)OverrideDrawRect,method_getTypeEncoding(origMethod))
最後實現復寫方法。和之前不同的是,這裡是一個方法,而不是方法:
staticvoidOverrideDrawRect(NSView*self,SEL _cmd, NSRect r)
{
gOrigDrawRect(self,_cmd, r);
[[NSColor blueColor]set];
NSRectFill(r);
}
當然,這個方法不是那麼優雅,不過我認為它更易於運用。
溫馨提示(The Obligatory Warning)
復寫不是你自家的類是危險的! 盡量避免這麼做,要不然就盡最大的可能細心處理。