overview
在iOS开发中,有时我们会有这样的需求:想改变某一个系统类的实现,但在项目中逐一修改工作量太过于庞大,这时Method Swizzling就显现出了ta的强大。

Method Swizzling是针对Objective-C中selector实现hook的方法,基于OC中的动态特性runtime,达到替换方法的目的。

基本原理

想理解Method Swizzling就需要了解Objective-C的基础消息机制。OC中的每一个类都有一个Dispatch Table,将每个方法的名字SEL(一个C字符串)与方法的实现IMP(指向方法实现函数开始位置的指针)分别对应起来。当我们执行方法A时,OC就会从Dispatch Table中找出方法A所对应的函数IMP,如果找到则执行函数内的代码,如下:
overview
Method Swizzling其实就是利用OC的动态特性,利用runtime把方法A的实现与方法B的实现进行交换。
例如:我希望在每个视图控制器的-viewWillAppear执行后,在控制台输出@"yoho, swift~"。那么就可以用一个新的方法-cr_viewWillAppear来替换系统API,cr_viewWillAppear中打印类名,通过Method Swizzling替换后,当前类的Dispatch Table如下:
overview
Swizzling后,当前我们在-viewWillAppear中调用[super viewWillAppear:animated];时,则会调用重定向后-cr_viewWillAppear方法的实现;在-cr_viewWillAppear中调用[self cr_viewWillAppear:animated];时,则会调用系统-viewWillAppear的实现。这也是为什么在Swizzling后不会出现递归的根本原因。

具体实现

  • Method Swizzling是非原子性的,因此,应该放在dispatch_once中执行,确保Swizzling代码只执行一次。同样的,包含Method Swizzling的dispatch_once代码,应该放在+(void)load方法中调用。(之所以不选择+initialize方法,是因为+load会在类初始加载时调用,而+initialize方法当前类的实例方法或类方法被调用时才执行,会增加程序中的不确定因素,可能导致并发问题),实例方法的例子:
#import <objc/runtime.h>

@implementation UIViewController (CRSwizzling)

+ (void)load {
    [self cr_swizzlingMethod:@selector(viewWillAppear:)
                  withAltSEL:@selector(cr_viewWillAppear:)];
}

#pragma mark - Method Swizzling
+ (void)cr_swizzlingMethod:(SEL)originSEL withAltSEL:(SEL)altSEL {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = originSEL;
        SEL swizzledSelector = altSEL;

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        if (didAddMethod) {

            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }

    });
}

#pragma mark - Custom Method
- (void)cr_viewWillAppear:(BOOL)animated {
    [self cr_viewWillAppear:animated];
    NSLog(@"yoho, swift~");
}

@end
  • 当有继承关系的类存在时,Swizzling会从父类开始。例如一个视图控制器SubViewController继承于ViewController,那么当SubViewController需要走Swizzling方法时,发现已经在父类中使用dispatch_once执行过了,那么SubViewController的-viewWillAppear就会在调用[super viewWillAppear:animated];时,执行父类Swizzling时走过的步骤。
    显示viewController的时候,控制台输出:
    overview
    显示SubViewController的时候,控制台输出:
    overview

如上讲解的demo在->这里

注意事项 & 讨论

Swizzling是基于runtime的一种巴拉巴拉魔法,所以最好遵循一些原则,避免出现魔法四溢的现象。

  • 需要调用父类原始方法。毕竟我们不知道闭源的原始方法内部实现是什么╮(╯_╰)╭
  • 给重定向的方法加前缀。类似于示例demo中的cr_xx,避免出现冲突。
  • 按需使用。对于runtime的巴拉巴拉魔法,用好了是利刃可以势如破竹,用不好反而容易伤到自己。

推荐

其实如上这种,通过Method Swizzling动态更改方法的方式被称为——Aspect Oriented Programming(面向切面编程),简称AOP。
关于Objective-C中的AOP应用,GitHub上已经有了很棒的库——Aspects。Aspects通过runtime封装了一套很完善的hook方案,在日常项目中如果需要使用Method Swizzling,可以直接用CocoaPods导入Aspects,使用起来也非常便捷。

以上。

参考资源:

Method Swizzling
Objective-C的hook方案(一): Method Swizzling
What are the Dangers of Method Swizzling in Objective C?