How does method swizzling work, and why does it need dynamic in Swift?

4.6.1 · L3 · Interview

Predict first

What does this print?

class Greeter: NSObject {
    @objc dynamic func hello() -> String { "hello" }
    @objc dynamic func hi() -> String { "hi" }
}
let a = class_getInstanceMethod(Greeter.self, #selector(Greeter.hello))!
let b = class_getInstanceMethod(Greeter.self, #selector(Greeter.hi))!
method_exchangeImplementations(a, b)
print(Greeter().hello())
  • hello
  • hi

Predict first

What does this print when the methods are only @objc, not dynamic?

class Greeter: NSObject {
    @objc func hello() -> String { "hello" }
    @objc func hi() -> String { "hi" }
}
let a = class_getInstanceMethod(Greeter.self, #selector(Greeter.hello))!
let b = class_getInstanceMethod(Greeter.self, #selector(Greeter.hi))!
method_exchangeImplementations(a, b)
print(Greeter().hello(), Greeter().perform(#selector(Greeter.hello))?.takeUnretainedValue() as Any)
  • hi Optional(hi)
  • hello Optional(hi)
  • hello Optional(hello)

Mental model. Swapping two names in a phone directory. Anyone who looks up the first name now reaches the second person, but people who memorised the number still call the original.

A class’s method table maps selectors to implementations (IMP function pointers). Swizzling exchanges two entries with method_exchangeImplementations (or replaces one with class_replaceMethod). After the swap, sending the original selector runs the other implementation. The familiar trick is to swap a system method with your own: inside your replacement you call your own selector, which now points at the original, so you wrap the old behaviour (logging, analytics) without recursion. It is usually done once, early, in +load.

It only affects calls that go through the table. A Swift call to a method that is merely @objc uses a vtable or a direct call, so it does not see the swap, while perform(_:) from Objective-C does. Mark the method dynamic so Swift callers also go through objc_msgSend. Swift’s optimiser can also inline and devirtualise final or whole-module-visible methods, which silently defeats a swizzle. So swizzling Swift is fragile, and swizzling Objective-C framework methods is where it is mostly used.

The two implementations were exchanged, so asking for hello now runs the code that was written for hi.

The Swift call bypasses the method table and still runs hello. The runtime lookup through perform sees the swap.

Common traps

  • Swizzling a non-dynamic Swift method. Swift callers keep calling the original and see no effect.
  • Swizzling more than once. A second exchange undoes the first. Guard it with a static let or dispatch_once.

They might follow up with

  • Why call your own selector inside the swizzled method? After the exchange it points to the original implementation.
  • What is the difference between exchange and replace? Exchange keeps both implementations reachable. Replace loses the old one unless you saved its IMP.

Related questions

  • 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?
  • 4.6.2 Why is method swizzling considered a footgun, and what do you do instead?
  • 4.1.4 What is isa, and why does your object’s runtime class sometimes differ from its Swift type?

Why is method swizzling considered a footgun, and what do you do… →