What happens when you send a message in Objective-C, and what if nothing responds?

4.1.1 · L2 · Interview

Predict first

What does this print?

print(NSObject().responds(to: Selector(("nope"))))
  • true
  • false
  • Crash

Predict first

What happens?

NSObject().perform(Selector(("nope")))
  • Returns nil
  • Crashes with unrecognized selector

Mental model. A message is a letter addressed to an object. The post office (the runtime) checks the object’s own mailbox, then its parents’, and only then returns it as undeliverable.

[obj doThing:x] compiles to a call to objc_msgSend(obj, @selector(doThing:), x). The runtime reads the object’s class pointer (isa), looks the selector up in that class’s method cache, then its method list, then each superclass in turn, and finally jumps to the implementation it found. Hits in the cache make the common case fast. The selector is just an interned string, and the receiver is typed id, so the compiler cannot know which implementation will run.

If no class in the chain implements the selector, the runtime does not fail at once. It runs the forwarding machinery (see the forwarding question) and, if that also declines, throws “unrecognized selector sent to instance”. Sending a message to nil is legal and returns zero or nil, which is why Objective-C code is full of unguarded calls that Swift would force you to unwrap. In Swift you can ask first with responds(to:).

responds(to:) only asks. It returns false for a selector no class in the chain implements, without throwing.

perform actually sends the message. With nothing to handle it (and no forwarding), the runtime raises “unrecognized selector”.

Common traps

  • “Messaging nil crashes.” In Objective-C it silently returns zero. The crash comes from messaging a real object that lacks the selector.
  • Assuming a call site is bound at compile time. The target is chosen at runtime, so it can change (swizzling, KVO).

They might follow up with

  • What makes the lookup fast? A per-class cache of recently used selectors, filled on first use.
  • How does this differ from Swift’s default? Swift calls are usually resolved statically or through a vtable. Only @objc dynamic members use objc_msgSend.

Related questions

  • 4.1.3 What are the stages of message forwarding, and when would you use them?
  • 4.1.4 What is isa, and why does your object’s runtime class sometimes differ from its Swift type?
  • 1.11.1 Static vs dynamic dispatch in Swift?

What is a selector, how does #selector map Swift names to it, and… →