What is isa, and why does your object’s runtime class sometimes differ from its Swift type?

4.1.4 · L5 · Learning

Predict first

What does this print?

class Model: NSObject { @objc dynamic var n = 0 }
let m = Model()
print(String(describing: object_getClass(m)!).contains("NSKVONotifying"))
let obs = m.observe(\.n) { _, _ in }
print(String(describing: object_getClass(m)!).contains("NSKVONotifying"), type(of: m) == Model.self)
_ = obs
  • false, then true true
  • false, then true false
  • true, then true true

Mental model. The isa pointer is the name badge on an object. KVO quietly swaps it for a badge with extra duties, and only the runtime notices.

Every Objective-C object starts with a pointer, isa, to its class object, which holds the method table, the superclass pointer and the instance layout. The class object is itself an object whose isa points to a metaclass, which holds the class methods. That is how [Foo create] is just another message send, and why +load and +initialize live there.

Because isa is just a pointer, the runtime can change an object’s class at runtime, and it does. When you observe a property with KVO, the runtime creates a subclass named like NSKVONotifying_Model, overrides the setters to post change notifications, and re-points that one object’s isa at it. Swift’s type(of:) keeps reporting Model, but object_getClass(_:) shows the real, swapped class. This is the reason KVO needs @objc dynamic: the setter must be reachable through the message table.

Observing swaps the instance’s runtime class for a generated NSKVONotifying_… subclass, while type(of:) still reports Model.

Common traps

  • Comparing object_getClass(x) == Foo.self after observing. It is a KVO subclass now, so the comparison fails.
  • Assuming type(of:) is the runtime class. It reports the declared class even when isa-swizzled.

They might follow up with

  • What is a metaclass for? It stores class methods, so a class can be sent messages like any object.
  • Where does this connect to swizzling? Both change what a selector resolves to; one swaps a method, the other a whole class.

Related questions

  • 2.4.5 How does KVO work in Swift, and why does it sometimes never fire?
  • 4.6.1 How does method swizzling work, and why does it need dynamic in Swift?
  • 4.4.1 What do @objc, @objcMembers, @objc(name), dynamic and @nonobjc each do?

← What are the stages of message forwarding, and when would you use…