05.MethodSwizzling的问题

使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。 Method swizzling 可以帮助我们写出更好的,更高效的,易维护的代码。但是如果滥用它,也将会导致难以排查的bug。

相关方法

  • class_replaceMethod 替换类方法的定义

    *

  • method_exchangeImplementations 交换 2 个方法的实现

    • 这里相当于调用了两次method_setImplementation

    • 点进方法的注释里面可以看到相关注释

    • 即 A <-> B

  • method_setImplementation 设置 1 个方法的实现

    • 即 A -> B

注意点: 用method_setImplementation的话单方面设置实现有的场景挺实用 functionA 设置了 functionB的实现 那么项目中使用 functionAfunctionB 的地方就会有同样的实现 但是如果用 method_exchangeImplementations 的话,就会有不同的实现

看一下 RNSwizzle 的封装

//
//  RNSwizzle.m
//  MethodSwizzle

#import "RNSwizzle.h"
#import <objc/runtime.h>

@implementation NSObject (RNSwizzle)

+ (IMP)swizzleSelector:(SEL)origSelector 
               withIMP:(IMP)newIMP {
  Class class = [self class];
  Method origMethod = class_getInstanceMethod(class,
                                              origSelector);
  IMP origIMP = method_getImplementation(origMethod);

  if(!class_addMethod(self, origSelector, newIMP,
                      method_getTypeEncoding(origMethod)))
  {
    method_setImplementation(origMethod, newIMP);
  }

  return origIMP;
}

方法交换不是自动的

我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。

改变不是自己的代码

这是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。

想象一下,如果你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。

命名冲突

命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名

+ (void)load {
    [self swizzle:@selector(setFrame:) with:@selector(my_setFrame:)];
}

这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢? 这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:

static void MySetFrame(id self, SEL _cmd, NSRect frame);
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);

static void MySetFrame(id self, SEL _cmd, NSRect frame) {
    // do custom work
    SetFrameIMP(self, _cmd, frame);
}

+ (void)load {
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];
    }

看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。 最后给出一个较完美的swizzle方法的定义:

typedef IMP *IMPPointer;

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation NSObject (FRRuntimeAdditions)

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}
@end

交换方法影响了方法调用

项目中调用不同方法名的问题 上面讨论过了

交换的顺序

  • 多个有继承关系的类的对象swizzle时,先从父对象开始

  • 这样才能保证子类方法拿到父类中的被swizzle的实现

  • 在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用

Last updated