SwizzleHelper

SwizzleHelper 是一个 Swift 包,它能帮助你在 Swift 中更轻松地实现方法调剂(也称“猴子补丁”) Objective-C 方法,现在它还支持将键值对附加到 Objective-C 的 classclass 的实例。

方法调剂 (Swizzling)

方法调剂是一种在 Objective-C 中有时会使用的实践,它通过将方法的实现替换为另一个方法来将功能挂钩到没有提供这些功能的类中。通常,替换实现会调用之前的实现,可以在执行自身操作之前或之后调用,以保留现有功能。这通常不在 Swift 中完成,即使在 Objective-C 中也是一种极端方法,但它可能是一个有用且强大的工具。例如,我在我的 CustomToolTip 包中,结合使用 NSView 的扩展,添加了能够将自定义工具提示附加到任何 NSView 的功能,这些工具提示可以将任何 NSView 作为其内容,而无需对任何内容进行子类化或将现有对象包装在特殊的“可提示”对象中。 效果是你可以像将 String 分配给标准 toolTip 一样,将工具提示视图分配给 customToolTip 属性,而 toolTipNSView 开箱即用的支持。 没有方法调剂,我不可能让它如此易于使用。

这仅适用于 @objc 方法,这意味着你无法调剂 Swift 原生方法(嗯,你可以在某种程度上这样做,但这涉及到一些特别邪恶和棘手的操作,涉及到动态链接器,或者要调剂的方法必须标记为 dynamic,这意味着作者专门计划了该方法的调剂)。 另一个需要注意的是,当你在 Swift 中直接调用方法时,它可能不会通过 Obj-C 消息传递机制,这意味着如果你用另一个名为 bar 的方法调剂 foo 的实现,当你在 Objective-C 中调用 foo 时,它实际上会调用你为 bar 编写的实现,因为你用它替换了 foo 的实现。 但是,当你在 Swift 中调用 foo 时,它很可能仍然会调用原始的 foo。 这是因为编译器通常可以在编译时解析应该调用的确切方法,但调剂是一个运行时的事情。 为了保证在 Swift 中获得与在 Objective-C 中相同的行为,你必须使用 NSObjectperformSelector 方法。 因此,我认为最好将调剂限制为由 AppKit 或 UIKit 调用的方法,而不是直接由你的代码调用的方法,例如 NSResponderNSView 的方法,这些方法被调用以响应事件或布局更改。 例如,CustomToolTip 调剂 updateTrackingAreasmouseEnteredmouseExitedmouseMoved。 它所做的其他一切都是用纯原生 Swift 方法完成的。

我不希望鼓励任何人将方法调剂作为第一个解决方案,因为当你这样做时,你正在干扰事物的“自然顺序”。 很容易以难以调试的方式破坏事物。 我可能会在将来的某个时候在这里提供更多“操作方法”部分,但目前代码的文档非常详尽,带有文档注释。 因此,如果作为最后的手段,或者仅仅出于个人兴趣,你决定尝试一下,我会将你指向 NSObject+Swizzling.swift 文件中的 replaceMethod(_:with:)callReplacedMethod(for:) 方法。 在你这样做之前,花一些时间阅读各种关于调剂的在线文章,这些文章通常都是关于 Objective-C 的,但原理完全相同。 还要了解最广泛的方法也微妙地错误。 我建议将这篇 博客 加入你的阅读清单,以了解原因。

此软件包的当前状态仅比 CustomToolTip 所需的多一点。 它仅处理转发到采用零个参数或单个 NSObject 参数并且不返回任何内容的替换方法实现,或者不采用参数并返回 NSObject 的替换方法实现。 我会根据需要扩展它;但是,如果你需要更多,你需要在 swizzleHelper.mswizzleHelper.h 中添加函数 callIMP... 函数,以及在 NSObject+Swizzling.swift 中添加相应的 callReplacedMethod... 方法。

如果这个 repo 帮助你完成了方法调剂工作,并且你发现对于你的用例,你必须添加转发调用或任何其他与方法调剂直接相关的内容,请考虑贡献你代码的这一部分,以帮助下一个程序员。 虽然我们不应将方法调剂推广为“首选”解决方案,但对于那些必须这样做的人,我们至少可以组装正确且可靠地在 Swift 中完成它所需的内容,并在过程中让世界变得不那么容易出错。

关联值 (Associated Values)

关联值是可以与 NSObject 的特定子类或任何 NSObject 的实例关联的值。 它们作为键值对工作,使用 String 作为键。 在你可以使用属性的情况下,你可能应该这样做。 但是,Swift extension 不能具有存储的属性,这就是关联值派上用场的地方,因为你可以使用计算属性来设置和获取关联值,使其行为就像你确实拥有存储的属性一样。

你可以通过 associatedValues 实例属性来做到这一点。

extension NSView
{
    // This will effectively add a stored property to all NSViews
    var shouldBlurContents: Bool
    {
        get { associatedValues["shouldBlurContents"] as? Bool ?? false }
        set { associatedValues["shouldBlurContents"] = newValue }
    }
}

你也可以在类上使用关联值;但是,它们的行为不是多态的。

class BaseClass: NSObject { }
class Subclass: BaseClass { }

BaseClass.associatedValues["animal"] = "dog"
BaseClass.associatedValues["plant"] = "hibiscus"
Subclass.associatedValues["animal"] = "cat"
Subclass.associatedValues["protozoa"] = "amoeba"

// prints "dog"
print("\(BaseClass.associatedValues["animal"] as? String ?? "nil")")

// prints "hibiscus"
print("\(BaseClass.associatedValues["plant"] as? String ?? "nil")")

// prints "nil"
print("\(BaseClass.associatedValues["protozoa"] as? String  ?? "nil")")

// prints "cat"
print("\(Subclass.associatedValues["animal"] as? String  ?? "nil")")

// prints "nil"
print("\(Subclass.associatedValues["plant"] as? String  ?? "nil")")

// prints "amoeba"
print("\(Subclass.associatedValues["protozoa"] as? String  ?? "nil")")

// class associated values must be retrieved by the class, not an instance of it.
let object = Subclass()

// All of these print "nil"
print("\(object.associatedValues["animal"] as? String  ?? "nil")")
print("\(object.associatedValues["plant"] as? String  ?? "nil")")
print("\(object.associatedValues["protozoa"] as? String  ?? "nil")")

如果你想要 static/class 属性的多态行为,你可以通过在子类中使用 super.associatedValues 来自己实现它。