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. Modern Objective-C uses the modern runtime, which is available to 64-bit applications on iOS and macOS 10.5 and later.
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:
- Follow the object's
isapointer to find its class object. - Search the class object's method list 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 the class's methods. The following runtime structures describe those pieces:
// Object
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
// Class
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // Removed in Objective-C 2.0
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
// Method list
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;
int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;
// Method
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
};
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.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
// A pointer to an instance of a class.
typedef struct objc_object *id;
Class objects (objc_class)
An Objective-C class is itself an object. The Class type is a pointer to an objc_class structure. That structure contains the information a class needs: its superclass, name, version, instance size, ivars, methods, cache, and adopted protocols. This data is commonly described as the class's metadata.
The presence of an isa field is important: it confirms that a class is also an object. Class objects are created during compilation, are used to create instances, and behave as singletons at runtime.
Metaclasses
Instances are created by class objects, so what creates a class object? That role belongs to the metaclass referenced by the class object's isa pointer.
Because a class is an object, it can receive messages in the form of class-method calls. Its isa pointer therefore needs to reference an objc_class structure containing those class methods. A metaclass stores the information required to create class objects and dispatch class methods. In an NSObject hierarchy, metaclasses ultimately converge on the NSObject metaclass, while the root metaclass's isa points back to itself.

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 (objc_method)
A method is conceptually similar to a function. The runtime represents it with objc_method:
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
};
Like Class, Method is a pointer to a structure. The relevant fields are:
SEL method_name: the method's selectorchar *method_types: the encoded method signatureIMP method_imp: the function that implements the method
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 maps to a C string containing the method name. You can obtain one with the compiler expression @selector() or with the runtime function 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)
The runtime defines IMP as follows:
typedef id (*IMP)(id, SEL, ...);
This typedef describes a function pointer returning id and receiving at least an object and selector. An IMP points to the function body that implements a method. Together, the selector and implementation let the runtime locate and invoke the corresponding behavior efficiently.
Class caches (objc_cache)
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 (objc_category)
The runtime declares categories with a structure similar to the following:
// https://opensource.apple.com/source/objc4/objc4-371.1/runtime/runtime.h.auto.html
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
};
Category points to an objc_category structure containing the category name, target class, instance methods, class methods, and protocols. This explains why a category can extend a class with methods and protocol conformance.
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
- A systematic Runtime introduction by jackyshan_ on Juejin
- 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.