Notes on the Objective-C Runtime
The Objective-C runtime comes up often in interviews, but it is more than an interview topic. Understanding how the language works underneath the syntax makes it easier to use Objective-C deliberately and to reason about unfamiliar behavior.
How the Runtime Works
Introduction
In a conventional compiled language, source code is translated into assembly and eventually into machine instructions. Objective-C adds a dynamic runtime layer to that process. Its object-oriented constructs are represented through C data structures and runtime functions before becoming executable machine code.
One useful mental model is that the runtime maps object-oriented concepts such as classes, objects, and methods onto procedural C structures and function calls. This article discusses the modern Objective-C 2.0 runtime used by 64-bit Apple platforms. Its concrete layouts are private implementation details and have changed over time, so the structures below are conceptual rather than a stable ABI.
Message Sending
What is message sending?
Calling a method on an Objective-C object is a message-send operation. When the compiler sees [obj method], it turns the expression into a call similar to objc_msgSend(obj, method).
The lookup process
At a high level, message lookup works like this:
- Resolve the object's encoded
isainformation to find its class object. - Check the class cache, then search the class's method metadata for the requested selector.
- If the selector is not present, continue through the superclass chain.
- Once the method is found, invoke its
IMP. - Return the value produced by that implementation.
This process involves the object, its class, and method metadata. A deliberately simplified model looks like this:
instance
└─ encoded isa → class object
├─ superclass
├─ method metadata: selector + type encoding + implementation
└─ lookup cache: selector → implementation
Older runtime headers exposed concrete structures with fields such as methodLists and cache. Those declarations are often reproduced in tutorials, but many are explicitly marked OBJC2_UNAVAILABLE; using them as the layout of a modern class is misleading.
The main concepts used during message sending are:
- Class objects (
objc_class) - Instances (
objc_object) - Metaclasses
- Methods (
objc_method) - Selectors (
SEL) - Implementations (
IMP) - Class caches (
objc_cache) - Categories (
objc_category)
Instances (objc_object)
// Represents an instance of a class.
// A pointer to an instance of a class.
typedef struct objc_object *id;
Conceptually, every instance carries enough isa information for the runtime to resolve its class. On modern 64-bit runtimes that value may be a non-pointer encoding containing flags as well as class information, so “follow the isa pointer” is a useful diagram, not a promise about the raw bits.
Class objects (objc_class)
An Objective-C class is itself an object. Class is an opaque pointer to runtime-managed class metadata. Conceptually, that metadata gives the runtime access to the superclass, instance size, ivars, methods, protocols, and lookup cache, but application code should use public runtime functions rather than depending on the private structure layout.
Class records are emitted into the executable and realized by the runtime as images load. Within one runtime, a realized class object provides the shared identity used to create instances and dispatch instance methods.
Metaclasses
Because a class is an object, it can receive class-method messages. The class object's isa resolves to a metaclass whose method metadata is used for those messages. A metaclass is therefore primarily the dispatch class for a class object; saying that it “creates the class” confuses dispatch metadata with runtime realization.
In a typical NSObject hierarchy, metaclass superclass chains ultimately converge on the root metaclass, while the root metaclass's isa points back to itself.
Conceptual dispatch relationships; modern encoded isa values and private metadata layouts are intentionally omitted.
This is the classic message-send diagram. I find a two-dimensional view of the class and metaclass relationships easier to follow because it makes both axes of the hierarchy visible.
Methods (Method)
A method combines three important pieces of information: its selector, encoded signature, and implementation. The public Method type is opaque:
typedef struct objc_method *Method;
Application code inspects it through functions such as:
method_getNamemethod_getTypeEncodingmethod_getImplementation
Selectors (SEL)
// objc.h
typedef struct objc_selector *SEL;
SEL is Objective-C's type for a selector. A selector acts as the identifier used to distinguish a method name:
@property SEL selector;
A selector is the runtime's interned identity for an Objective-C method name. sel_getName exposes its C-string spelling. You can obtain a selector with @selector() or register a name dynamically with sel_registerName.
Selectors follow two useful rules:
- A class cannot contain duplicate selectors.
- Different classes can use the same selector.
Unlike function overloading in C++, a selector does not distinguish methods only by parameter types. The colons are part of an Objective-C method name, but the runtime selector does not encode argument types. Two methods therefore cannot share the same selector while differing only by types.
Implementations (IMP)
Conceptually, an Objective-C implementation is a function pointer whose first two arguments are the receiver and selector:
typedef id (*IMP)(id, SEL, ...);
The exact C function type must match the method's real return and argument types; the simplified declaration above is not a universal cast for every method. An IMP points to the function body that implements a method.
Class caches
Walking a method list and then the superclass chain for every message would be expensive, especially because applications tend to invoke a small set of methods repeatedly. Classes therefore cache successful lookups.
When a selector is found, the runtime places the result in the class cache. Future sends check that cache before searching the method list again. This optimization follows a simple observation: a method invoked once is likely to be invoked again.
Categories
Compiled categories contribute method and protocol metadata that the runtime attaches to the target class as an image loads. They do not change the already-compiled instance layout, which is why a category can add behavior and protocol conformance but cannot add ordinary instance variables.
Message Forwarding
When ordinary lookup fails, the runtime offers three stages of message forwarding:
- Dynamic method resolution
- A fallback receiver
- Full message forwarding
If all three stages fail, the runtime calls doesNotRecognizeSelector: and raises an unrecognized selector exception.
Dynamic method resolution
The runtime first calls +resolveInstanceMethod: or +resolveClassMethod: and gives the class an opportunity to provide an implementation dynamically. If the class adds a method—for example with class_addMethod—and returns YES, the runtime restarts message lookup. Returning NO moves the process to forwardingTargetForSelector:.
Fallback receiver
If the target implements -forwardingTargetForSelector:, the runtime asks it for another object that can receive the message. This is a lightweight forwarding path: the original object cannot handle the selector, so it nominates another object with compatible behavior.
Full message forwarding
If the earlier stages do not handle the message, the runtime begins full forwarding. It first sends -methodSignatureForSelector: to obtain the argument and return types. Returning nil leads to -doesNotRecognizeSelector:. Returning a valid signature lets the runtime create an NSInvocation representing the original message and pass it to -forwardInvocation:.
Inside forwardInvocation:, the receiver can inspect the invocation and redirect it to an appropriate target.
Runtime Applications
At the time I wrote these notes, I had only started exploring practical runtime techniques. Areas worth studying next include method swizzling, associated objects, dynamic method creation, introspection, and forwarding-based proxies.
References
- Apple Objective-C Runtime Programming Guide
- Apple open-source objc4 runtime
- The declaration of
objc_msgSend:
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
- The declaration of
Class:
typedef struct objc_class *Class;
- For the signature encoding
v@:, see Apple's Type Encodings.