Swift中使用Method Swizzling

Posted by Roylee on 2016-08-04
OC中的 Method Swizzling

iOS中的黑魔法 Method Swizzling经常会用到,在OC中的使用方式是重写类的+ (void)load方法,运行时交换方法

+ (void)_load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        SwizzleMethod([self class],
                        @selector(originalMethod),
                        @selector(trans_originalMethod));
       });
}


void SwizzleMethod(Class c, SEL origSEL, SEL newSEL) {
    Method origMethod = class_getInstanceMethod(c, origSEL);
    Method newMethod = nil;
    if (!origMethod) {
        origMethod = class_getClassMethod(c, origSEL);
        if (!origMethod) {
                return;
        }
        newMethod = class_getClassMethod(c, newSEL);
        if (!newMethod) {
            return;
        }
    }else{
        newMethod = class_getInstanceMethod(c, newSEL);
        if (!newMethod) {
            return;
        }
    }

    // 自身已经有了就添加不成功,直接交换即可
    if(class_addMethod(c, origSEL, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))){
        class_replaceMethod(c, newSEL, method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
    }else{
        method_exchangeImplementations(origMethod, newMethod);
    }
}

其实在load方法中,可以不用使用GCD的dispatch_once方法,因为load本身就是只执行一次(方法交换需要且只能执行一次

Swift中的 Method Swizzling

由于Swift中没有load方法,所以只能写在initialize中,这是调用类中第一个方法前的地方

extension UIScrollView {
    public override static func initialize() {
        struct Static {
            static var token: dispatch_once_t = 0
        }

        // Make sure not subclass
        if self !== UIScrollView.self {
            return
        }

        dispatch_once(&Static.token) {
            let originalSelector = NSSelectorFromString("handlePan:")
            let swizzledSelector = NSSelectorFromString("pm_handlePan:")

            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)

            let didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))

            if didAddMethod {
                class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
                } else {
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        }
    }

    // MARK: - Method Swizzling
    func pm_handlePan(pan: UIPanGestureRecognizer) {
        pm_handlePan(pan)

        if delegate != nil && delegate!.respondsToSelector(NSSelectorFromString("scrollViewDidPan:")) {
            delegate?.performSelector(NSSelectorFromString("scrollViewDidPan:"), withObject: pan)
        }
    }
}

不过,Swift使用Method Swizzling需要满足两个条件

  • 包含 swizzle 方法的类需要继承自 NSObject
  • 需要 swizzle 的方法必须有动态属性(dynamic attribute)

通常添加下,我们都是交换系统类的方法,一般不需要关注着两点。但是如果我们自定义的类,也需要交换方法就要满足这两点了(这种状况是极少的,也是应该避免的

更多详情请查看苹果文档

那么,自定义的类就得满足动态派发的特性

@objc属性将你的Swift API暴露给Objective-C runtime时,不能确保属性、方法、初始器的动态派发。Swift编译器可能为了优化你的代码,而绕过Objective-C runtime。当你指定一个成员变量为dynamic时,访问该变量就总会动态派发了。因为在变量定义时指定dynamic后,会使用Objective-C runtime来派发。

所以,需要对想要替换的方法声明为dynamic。如下:

class MyOwnClass : NSObject {
    dynamic func methodOriginal()->Int{
        return 1
    }
}


extension MyOwnClass {

    //在 Objective-C 中,我们在 load() 方法进行 swizzling。但Swift不允许使用这个方法。
       override class func initialize() {
        struct Static {
            static var token: dispatch_once_t = 0;
        }

        dispatch_once(&Static.token) {
            let originalSelector = Selector("methodOriginal");
            let swizzledSelector = Selector("methodSwizzled");

            let originalMethod = class_getInstanceMethod(self, originalSelector);
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector);

            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }

    func methodSwizzled()->Int{
        // swizzling 后, 该方法就不会递归调用
        return methodSwizzled()+1
    }
}

var c = MyOwnClass()
print(methodOriginal())  //2
print(methodSwizzled())  //1

iOS中的黑魔法 Method Swizzling 不能随便用,只有在很了解并且比较特殊的情况才能使用,否则会有副作用的。