1. 程式人生 > >iOS Swizzle的正確使用方式(原文翻譯)

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.