Objective-C中如何正确的进行Swizzling

1,486 阅读6分钟

原文链接


前言       

       此篇文章是上周看DZNEmptyDataSet源码的时候看到了一段比较有意思的代码,而自己之前关于Swizzling一直看的是mattt大神的Method Swizzling版本(AFNetworking使用的是此种方式进行的Swizzling);刚好DZNEmptyDataSet的作者贴出了源博主的Blog地址,这里翻译出来与大家分享。首先贴出来DZNEmptyDataSet关于Swizzling的源码实现:

 // We add method sizzling for injecting -dzn_reloadData implementation to the native -reloadData implementation
    [self swizzleIfPossible:@selector(reloadData)];
    
    // Exclusively for UITableView, we also inject -dzn_reloadData to -endUpdates
    if ([self isKindOfClass:[UITableView class]]) {
        [self swizzleIfPossible:@selector(endUpdates)];
    }

- (void)swizzleIfPossible:(SEL)selector
{
    // Create the lookup table

    Class baseClass = dzn_baseClassToSwizzleForTarget(self);
    NSString *key = dzn_implementationKey(baseClass, selector);
    NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey];
    
    // If the implementation for this class already exist, skip!!
    if (impValue || !key || !baseClass) {
        return;
    }
    
    // Swizzle by injecting additional implementation
    Method method = class_getInstanceMethod(baseClass, selector);
    IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation);
    
    // Store the new implementation in the lookup table
    NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass,
                                   DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector),
                                   DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]};
    [_impLookupTable setObject:swizzledInfo forKey:key];
}

void dzn_original_implementation(id self, SEL _cmd)
{
    // Fetch original implementation from lookup table
    Class baseClass = dzn_baseClassToSwizzleForTarget(self);
    NSString *key = dzn_implementationKey(baseClass, _cmd);
    
    NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key];
    NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey];
    
    IMP impPointer = [impValue pointerValue];
    
    // We then inject the additional implementation for reloading the empty dataset
    // Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time.
    [self dzn_reloadEmptyDataSet];
    
    // If found, call original implementation
    if (impPointer) {
        ((void(*)(id,SEL))impPointer)(self,_cmd);
    }
}


/************以下是翻译内容(英文不错的可以直接点击标题处的原文链接) *************/

      Swizzling是通过将方法的实现替换为另一个方法实现来改变方法的功能,这一替换通常是在运行时执行的。有很多不同的需求可能需要使用到Swizzling,如:自省(introspection)、重写默认的行为(overriding default behavior)、或者动态加载方法(maybe even dynamic method loading)。我已经看了很多博客讨论Objective-C的Swizzling,并且他们很多人都推荐了一些非常糟糕的做法。如果您正在编写独立的应用程序程序,这些不好的做法并不是什么大不了的事,但如果您正在为第三方人员编写框架,那么Swizzling可能会弄乱一些可以正常运行的基础执行。那么在Objective-C中Swizzling的正确方法是什么呢?

      让我们从基础开始。当我们说Swizzling的时候通常意味着用自定义的方法去替换源方法的行为,并且通常我们会在自己定义的方法内部调用源方法。Objective-C允许使用Objective-C运行时提供的方法进行上面的操作。在运行时中,Objective-C方法表示了一个名为Method的C结构体;这个结构体的定义如下:           

struct objc_method    
         SEL method_name         OBJC2_UNAVAILABLE;  
         char *method_types      OBJC2_UNAVAILABLE;  
         IMP method_imp          OBJC2_UNAVAILABLE;
}

      method_name是方法选择器,*method_types是参数和返回值类型编码的C字符串,method_imp是指向实际函数的函数指针(关于IMP稍后会有更多的讨论)。

      我们可以使用下面的方法访问此对象:

Method class_getClassMethod(Class aClass, SEL aSelector);
Method class_getInstanceMethod(Class aClass, SEL aSelector);

      通过访问对象的Method结构体可以访问更改其底层实现。method_imp是IMP类型,他被定义为id(*IMP)(id, SEL, ...)或者是一个以对象指针、选择器和一个附加变量列表作为参数,并返回一个对象指针的函数。这可以通过IMP method_setImplementation(Method method,IMP imp)来改变。通过method_setImplementation()传递替换的实现,imp,并传入一个你想要改变的方法的结构体,method,同时此方法会返回与method相关联的源IMP实现。这才是Swizzling的正确方式。

 什么是不正确的swizzle方式呢?

       这是一种常用的调配方法。 虽然看起来很简单 - 将一种方法的实现与另一种方法交换 - 但有一些非显而易见的影响。

void method_exchangeImplementations(Method m1, Method m2)

      为了理解这些影响,我们来看看在调用这个函数之前和之后m1和m2的结构。

 Method m1 { //this is the original method. we want to switch this one with
             //our replacement method
      SEL method_name = @selector(originalMethodName)
      char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
      IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }

Method m2 { //this is the swizzle method. We want this method executed when [MyClass
             //originalMethodName] is called
       SEL method_name = @selector(swizzle_originalMethodName)
       char *method_types = “v@:”
       IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }

      这些是我们调用函数之前的方法结构。 生成这些结构的Objective-C代码将如下所示:

@implementation MyClass
     - (void) originalMethodName //m1
     {
              //code
     }
     - (void) swizzle_originalMethodName //m2
     {
             //…code?
            [self swizzle_originalMethodName];//call original method
            //…code?
     }
 @end

      然后我们调用下面代码:

m1 = class_getInstanceMethod([MyClass class], @selector(originalMethodName));
m2 = class_getInstanceMethod([MyClass class], @selector(swizzle_originalMethodName));
method_exchangeImplementations(m1, m2)

       现在方法结构体将会如下所示:

Method m1 { //this is the original Method struct. we want to switch this one with
             //our replacement method
     SEL method_name = @selector(originalMethodName)
     char *method_types = “v@:“ //returns void, params id(self),selector(_cmd)
     IMP method_imp = 0x1234AABA (MyBundle`[MyClass swizzle_originalMethodName])
 }

Method m2 { //this is the swizzle Method struct. We want this method executed when [MyClass
            //originalMethodName] is called
     SEL method_name = @selector(swizzle_originalMethodName)
     char *method_types = “v@:”
     IMP method_imp = 0x000FFFF (MyBundle`[MyClass originalMethodName])
 }

      注意,如果我们想要执行原始方法代码,我们必须调用 - [self swizzle_originalMethodName],但这会导致_cmd值传递给原始方法代码,现在变为@selector(swizzle_originalMethodName),如果方法代码 取决于_cmd是方法的原始名称(originalMethodName)。 这种混乱的方式(下面的例子)阻碍了程序的正常运行,这应该避免。

- (void) originalMethodName //m1
 {
          assert([NSStringFromSelector(_cmd) isEqualToString:@“originalMethodNamed”]); //this fails after swizzling //using
          //method_exchangedImplementations()
          //…
 }

      现在让我们看看使用method_setImplementation()以适当的方式进行Swizzling。

正确的swizzle方式

      创建一个符合IMP定义的C函数,来替代创建一个Objective-C的函数-[(void) swizzle_originalMethodName]。

void __Swizzle_OriginalMethodName(id self, SEL _cmd)
 {
      //code
 }

      我们可以将这个函数作为IMP来执行:

IMP swizzleImp = (IMP)__Swizzle_OriginalMethodName;

      并且这允许我们将这个IMP传递给method_setImplementation()

method_setImplementation(method, swizzleImp);

      并且method_setImplementation()返回原始的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;
 }
@end

      执行下面的代码进行验证:

SwizzleExampleClass* example = [[SwizzleExampleClass alloc] init];
int originalReturn = [example originalMethod];
[example swizzleExample];
int swizzledReturn = [example originalMethod];
assert(originalReturn == 1); //true
assert(swizzledReturn == 2); //true

      总之,为了避免与其他第三方SDK发生冲突,请不要使用Objective-C方法和method_swapImplementations()混合使用,而应使用C函数和method_setImplementation(),将这些C函数转换为IMP。 这样可以避免与Objective-C方法一起提供的所有额外信息包裹,例如新的选择器名称。 如果你想调整,最好的结果是不留痕迹。 †不要忘记,所有Objective-C方法都会传递2个隐藏参数:对self(id self)和方法的选择器(SEL _cmd)的引用。  如果它返回一个空白,您可能不得不对IMP调用进行处理。 这是因为ARC假定所有的IMP都返回一个id,并且会试图保留void和原始类型。

IMP anImp; //represents objective-c function
          // -UIViewController viewDidLoad;
 ((void(*)(id,SEL))anImp)(self,_cmd); //call with a cast to prevent
                                     // ARC from retaining void.