iOS Swizzle的正確使用方式(原文翻譯)
通常在執行時,Swizzle是通過用一個方法的實現來替換另一個方法的實現來運作的。運用Swizzle可能是因為不同的需求:重寫預設方法,甚至是動態的方法載入。我曾經看到很多發出來的部落格上討論Swizzle,他們很多都提供了一些相當不好的用法。這些用法在你獨自寫專案的時候用起來無傷大雅,但是如果你在為一個第三方開發者提供framework的時候,Swizzle可能會讓本應該執行順利的部分出現混亂。所以,在OC中怎樣才是Swizzle的正確使用方式呢?
讓我們從基礎說起,當我說起Swilling的時候通常是用我自定義的方法來替代原有的方法,然後在自定義的方法裡呼叫原有的方法。OC在Runtime裡是允許這樣操作的。在執行時,OC的方法methods是以C語言的結構體形式出現的,一個被定義為struct objc_method
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
method_name就是當前呼叫方法的selector對應的名字,*method_types是c編碼的字串型別的引數和返回值,method_imp
是當前函式的指標(我們待會會討論更對關於IMP的問題)。
你可以用下面的方法拿到這個物件(在OC執行時有很多拿到他們的渠道):
Method class_getClassMethod(Class aClass,SEL aSelector); Method class_getInstanceMethod(Class aClass,SEL aSelector);
拿到Method就可以拿到Mehod內部的結構體從而改變他們內部的實現。method_imp是IMP型別,定義為 id(*IMP)(id,SEL,...),也是一個帶有指標、selector和一串作為引數的帶有編號變數的函式。用IMP method_setImplemention(Method method,IMP imp)可以改變method_imp,引數imp是Method結構體裡面的,是方法的實現,method是你想要改變的方法,然後再返回跟method對應的原生IMP,這是Swizzle的正確用法。
Swizzle的不正確用法是什麼?
下面是Swizzle的常用用法,當直接用一個方法來代替另一個方法實現的時候,會帶來一些不易察覺的影響。
void method_exchangeImplementation(Method m1,MNethod m2)
為了弄清楚這些影響,讓我們來看一下m1和m2在被呼叫前後的結構。
Method m1 { //這是原始的方法,我們想要把這個方法跟替換方法交換
SEL method_name = @selector(originalMethodName)
char *method_types = "[email protected]:" //返回為空,引數為 id(self),selector(_cmd)
IMP method_imp = 0x000FFF
}
Method m2 { //這是進行交換的方法,我們想在原生方法呼叫的時候執行這個方法
SEL method_name = @selector(swizzle_originalMethodName)
char *method_types = "[email protected]:" //返回為空,引數為 id(self),selector(_cmd)
IMP method_imp = 0x1234AABA
}
以上是方法未呼叫之前的結構,OC程式碼編譯這些結構就是這樣:
@implementation MyClass
- (void)originalMethodName //m1
{
//code
}
- (void)swizzle_originalMethodName //m2
{
//...code?
[self swizzle_originalMethodName]; //呼叫原生方法
//...code?
}
@end
然後我們呼叫:
m1 = class_getInstanceMethod([MyClass class],@selector(originalMethodName))
m2 = class_getInstanceMethod([MyClass class],@selector(swizzle_originalMethodName))
method_exchangeImplementation(m1,m2)
現在方法看起來將會是這樣:
Method m1 { //這是原始的方法,我們想要把這個方法跟替換方法交換
SEL method_name = @selector(originalMethodName)
char *method_types = "[email protected]:" //返回為空,引數為 id(self),selector(_cmd)
IMP method_imp = 0x1234AABA
}
Method m2 { //這是進行交換的方法,我們想在原生方法呼叫的時候執行這個方法
SEL method_name = @selector(swizzle_originalMethodName)
char *method_types = "[email protected]:" //返回為空,引數為 id(self),selector(_cmd)
IMP method_imp = 0x000FFF
}
兩個方法的IMP地址交換了一下,也就是說只改變了IMP。注意到如果我們想要執行原生方法我們得呼叫 [self swizzle_originalMethodName],如果原生方法依賴於_cmd 作為方法名,這將導致傳給原生方法的_cmd的值變成@selector(swizzle_originalMethodName)。這種swizzle的方式(下面有例子)已經對正常的函式編碼帶來了混亂,是應該避免的。
- (void)originalMethodName //m1
{
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethodName"]) //用method_exchangedImplementations() 進行swizzle將會失敗
}
現在我們看一下用method_exchangedImplementations()函式進行swizzle的正確用法。
正確的swizzle方法
用C函式定義一個IMP方法來代替新建立的OC函式-(void)swizzle_originalMethodName
void _Swizzle_originalMethodName(id self,SEL _cmd)
{
//code
}
我們可以把這個C函式轉換成一個IMP:
IMP swizzleImp = (IMP)_Swizzle_originalMethodName;
然後可以把swizzleImp傳給method_setImplementation( ):
method_setImplemention(method ,swizzleImp);
上面這個方法返回的是原生的IMP:
IMP originalImp = method_setImplementation(method,swizzleImp);
現在,originalImp可以用來呼叫原生方法了:
originalImp(self,_cmd);
這裡有一個例子:
@interface SwizzleExampleClass : NSObject
- (void) swizzleExample;
- (int) originalMethod;
@end
static IMP __original_Method_Imp;
int _replacement_Method(id self, SEL _cmd)
{
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
//code
int returnValue = ((int(*)(id,SEL))__original_Method_Imp)(self, _cmd);
return returnValue + 1;
}
@implementation SwizzleExampleClass
- (void) swizzleExample //call me to swizzle
{
Method m = class_getInstanceMethod([self class],
@selector(originalMethod));
__original_Method_Imp = method_setImplementation(m,
(IMP)_replacement_Method);
}
- (int) originalMethod
{
//code
assert([NSStringFromSelector(_cmd) isEqualToString:@"originalMethod"]);
return 1;
}
測試一下就能看出來:
SwizzleExampleClass* example = [[SwizzleExampleClass alloc] init];
int originalReturn = [example originalMethod];
[example swizzleExample];
int swizzledReturn = [example originalMethod];
assert(originalReturn == 1); //true
assert(swizzledReturn == 2); //true
總而言之,為了避免與其他第三方SDK造成混亂,不要用OC的方法和method_swapImplementations()來進行swizzle,而是把IMP轉換成C函式。這將避免OC自身的方法帶來的額外煩惱的資訊(可以理解為OC的語言特性帶來的問題:譯者注),比如新的方法名。如果你想用Swizzle,最好的結果就是不留下痕跡。
不要忘記,所有的OC方法傳遞了兩個隱藏的引數:對self的引用(id self),和方法名selector(SEL _cmd)。
如果IMP的呼叫返回值為空void,那你不得不注意了。因為ARC假定所有的IMP返回一個id,將會嘗試引用一個空的和原始型別
IMP anImp; //represents objective-c function
// -UIViewController viewDidLoad;
((void(*)(id,SEL))anImp)(self,_cmd); //call with a cast to prevent
// ARC from retaining void.