方法交换无法正常工作

8
我想使用方法交换,但无法让简单的示例代码正常运行。我可能误解了这个概念,但据我所知,它允许交换方法的实现。
给定两种方法A和B,我想交换它们的实现,以便调用A会执行B。我找到了一些关于交换方法的例子(example1example2)。我创建了一个新项目并添加了一个类来测试它。
class Swizzle: NSObject
{
    func method()
    {
        print("A");
    }
}

extension Swizzle
{
    override class func initialize()
    {
        struct Static
        {
            static var token: dispatch_once_t = 0;
        }

        // make sure this isn't a subclass
        if (self !== Swizzle.self)
        {
            return;
        }

        dispatch_once(&Static.token)
        {
            let originalSelector = Selector("method");
            let swizzledSelector = Selector("methodExt");

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

            print(method_getImplementation(originalMethod));
            print(method_getImplementation(swizzledMethod));

            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);
            }

            print(method_getImplementation(originalMethod));
            print(method_getImplementation(swizzledMethod));
        }
    }

    func methodExt()
    {
        print("B");
    }
}

然后我尝试使用以下方式执行它:

var s = Swizzle();
s.method();

预期输出是"B",但仍然打印出"A"。从我的代码可以看出,在交换操作之前和之后,我都包含了每个IMP的打印。这些打印显示交换确实发生了,但输出仍然相同。
输出:
0x000000010251a920
0x000000010251ad40
0x000000010251ad40
0x000000010251a920
A

在实施这些更改时,我是否遗漏了任何内容?

PS. 目前使用的是XCode 7.0.1


请查看此答案 http://stackoverflow.com/questions/32917029/swizzling-have-to-be-on-main-thread/32917142#32917142 - Iurie Manea
虽然并没有回答问题,但还是尝试了一下库。Swizzle.self.jr_swizzleMethod(originalSelector, withMethod: swizzledSelector); 也不起作用。 - Kevin
已解决...我正在编写答案...你是在测试 NSHipster 的示例吗? - uraimo
是的,我正在尝试 NSHipster 上的那个。它看起来更为详尽。另外,谢谢 :) - Kevin
1个回答

17
问题在于你的 method() 缺少 dynamic 指令:
class Swizzle: NSObject
{
    dynamic func method()
    {
        print("A")
    }
}

修改声明,就可以使其正常工作。

在 Swift 中使用方法交换时,您的类/方法必须满足以下两个要求:

  • 您的类必须扩展 NSObject
  • 您想要交换的函数必须具有 dynamic 属性

关于为什么需要这样做的完整解释,请查看 使用 Swift 与 Cocoa 和 Objective-C

需要动态分发

使用@objc属性可以将您的Swift API暴露给Objective-C运行时,但它并不保证属性、方法、下标或初始化程序的动态分发。Swift编译器仍然可能对成员访问进行去虚拟化或内联以优化代码性能,绕过Objective-C运行时。当您使用dynamic修饰符标记成员声明时,对该成员的访问始终是动态分派的。因为使用dynamic修饰符标记的声明是使用Objective-C运行时分派的,所以它们隐式地标记了@objc属性。

很少需要要求动态分发。但是,当您知道API的实现在运行时被替换时,必须使用dynamic修饰符。例如,您可以使用Objective-C运行时中的method_exchangeImplementations函数来交换方法的实现,而应用程序正在运行。如果Swift编译器内联了方法的实现或对其进行了去虚拟化访问,则不会使用新的实现。

Swift 3 更新:

GCD 方面有一些变化,dispatch_once 不再可用。为了执行相同的一次性操作,我们可以将代码放在全局静态类常量的初始化块中进行封装。

Swift 语言保证该代码将仅在应用程序生命周期内执行一次。

class TestSwizzling : NSObject {
    dynamic func methodOne()->Int{
        return 1
    }
}

extension TestSwizzling {

    //In Objective-C you'd perform the swizzling in load(), 
    //but this method is not permitted in Swift
    override class func initialize()
    {

        struct Inner {
            static let i: () = {

                let originalSelector = #selector(TestSwizzling.methodOne)
                let swizzledSelector = #selector(TestSwizzling.methodTwo)                 
                let originalMethod = class_getInstanceMethod(TestSwizzling.self, originalSelector);
                let swizzledMethod = class_getInstanceMethod(TestSwizzling.self, swizzledSelector)                
                method_exchangeImplementations(originalMethod, swizzledMethod)
            }
        }
        let _ = Inner.i
    }

    func methodTwo()->Int{
        // It will not be a recursive call anymore after the swizzling
        return methodTwo()+1
    }
}

var c = TestSwizzling()
print(c.methodOne())
print(c.methodTwo())

Swift 2.2 更新:

我已经更新了原始示例,以适应新的#selector属性:

class TestSwizzling : NSObject {
    dynamic func methodOne()->Int{
        return 1
    }
}

extension TestSwizzling {

    //In Objective-C you'd perform the swizzling in load(), 
    //but this method is not permitted in Swift
    override class func initialize()
    {
        struct Static
        {
            static var token: dispatch_once_t = 0
        }

        // Perform this one time only
        dispatch_once(&Static.token)
        {
                let originalSelector = #selector(TestSwizzling.methodOne)
                let swizzledSelector = #selector(TestSwizzling.methodTwo)                 
                let originalMethod = class_getInstanceMethod(self, originalSelector);
                let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)                
                method_exchangeImplementations(originalMethod, swizzledMethod)
        }
    }

    func methodTwo()->Int{
        // It will not be a recursive call anymore after the swizzling
        return methodTwo()+1
    }
}

var c = TestSwizzling()
print(c.methodOne())
print(c.methodTwo())

如果你需要一个示例来进行操作,请查看这个示例项目在github上

1
如果原始方法没有添加“dynamic”,那么我们就没有机会进行交换吗? - KChen
这按预期工作。有没有绕过需要使用“dynamic”关键字的方法,或者在声明后将方法变为动态的方式? - Kevin
我所知道的没有其他方法,如果原始类是用Swift编写的,则需要使用dynamic来禁用内联和其他优化(基于该语言是静态的事实)。显然,在基类上使用交换方法(例如Foundation / UIKit等中编写的objc)时,这不是问题,就像NSHipster在其使用UIViewController的示例中所做的那样。 - uraimo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接