使用 Method Swizzling 扩充方法

Objective-C 运行时允许你在运行时更改方法的实现。这称为方法调配,通常用于交换两种方法的实现。例如,如果交换方法 foobar,则发送消息 foo 现在将执行 bar 的实现,反之亦然。

此技术可用于扩充或修补无法直接编辑的现有方法,例如系统提供的类的方法。

在以下示例中,-[NSUserDefaults synchronize] 方法被扩充以打印原始实现的执行时间。

重要提示: 许多人尝试使用 method_exchangeImplementations 进行调配。但是,如果你需要调用要替换的方法,这种方法很危险,因为你将使用与预期接收的不同的选择器来调用它。因此,你的代码可能会以奇怪和意想不到的方式中断 - 特别是如果多方以这种方式调整对象。相反,你应该总是使用 setImplementation 与 C 函数一起调配,允许你用原始选择器调用方法。

#import "NSUserDefaults+Timing.h"
#import <objc/runtime.h> // Needed for method swizzling

static IMP old_synchronize = NULL;

static void new_synchronize(id self, SEL _cmd);

@implementation NSUserDefaults(Timing)

+ (void)load
{
    Method originalMethod = class_getInstanceMethod([self class], @selector(synchronize:));
    IMP swizzleImp = (IMP)new_synchronize;
    old_synchronize = method_setImplementation(originalMethod, swizzleImp);
}
@end

static void new_synchronize(id self, SEL _cmd);
{
    NSDate *started;
    BOOL returnValue;

    started = [NSDate date];

    // Call the original implementation, passing the same parameters
    // that this function was called with, including the selector.
    returnValue = old_synchronize(self, _cmd);

    NSLog(@"Writing user defaults took %f seconds.", [[NSDate date] timeIntervalSinceDate:started]);

    return returnValue;
}

@end

如果你需要调用一个带参数的方法,你只需将它们作为附加参数添加到函数中。例如:

static IMP old_viewWillAppear_animated = NULL;
static void new_viewWillAppear_animated(id self, SEL _cmd, BOOL animated);

...

Method originalMethod = class_getClassMethod([UIViewController class], @selector(viewWillAppear:));
IMP swizzleImp = (IMP)new_viewWillAppear_animated;
old_viewWillAppear_animated = method_setImplementation(originalMethod, swizzleImp);

...

static void new_viewWillAppear_animated(id self, SEL _cmd, BOOL animated)
{
    ...

    old_viewWillAppear_animated(self, _cmd, animated);

    ...
}