What is isa, and why does your object’s runtime class sometimes differ from its Swift type?
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.selfafter 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…