The Objective-C Runtime
Understand the C runtime library (libobjc) that backs every Objective-C object, class, and message send, and see precisely how objects, classes, metaclasses, selectors, and implementations fit together at execution time.
Every lesson so far has treated Objective-C as a language you write and a compiler translates. That picture is accurate but incomplete. Underneath the syntax sits libobjc, a C runtime library that every compiled Objective-C binary links against and that is genuinely running while your program executes — resolving which method a message actually calls, tracking what a class is made of, and letting code inspect or even rewrite that structure while the program is live. This is not a peripheral implementation detail. It is the mechanism that makes method swizzling, key-value observing, dynamic method resolution, message forwarding, and Objective-C's whole dynamic-typing style possible at all. This lesson is flagged as an advanced mandatory module because nearly everything dynamic you will learn later in this course — and nearly every "how does that even work" moment you will have reading real Objective-C code — traces back to the handful of runtime concepts covered here: class objects, metaclasses, the isa pointer, SEL, IMP, method lookup, and the method cache. Take your time with it.
Runtime overview: Objective-C is a language plus a library
When you compile a C program, function calls resolve to fixed addresses (or, for virtual functions in C++, to a vtable slot computed once). Objective-C works differently on purpose. When the compiler sees [receiver message], it does not emit a direct call to a function named message. It emits a call into libobjc, the Objective-C runtime library, handing it the receiver, a name identifying the method, and any arguments. It is libobjc, running as ordinary compiled C code at the moment your program executes, that figures out which actual implementation to run and invokes it. This indirection is what "dynamic" means in the phrase dynamic language applied to Objective-C.
The runtime is not a virtual machine and it is not interpreting your code. Your methods are compiled to real machine code ahead of time, exactly like C functions. What the runtime controls is the mapping from a message name to which piece of compiled code actually runs for a given object, and that mapping can be inspected and changed while the program is running. libobjc exposes this control through a C API — functions with names like class_addMethod, object_getClass, and method_getImplementation — that you can call directly from Objective-C code, and that higher-level Foundation features (like KVO) call on your behalf without you ever seeing it directly.
Keep this overview in mind as an anchor for the rest of the course: method swizzling (a later module) works by asking the runtime to swap which IMP a SEL maps to; dynamic method resolution (the next lesson) works by hooking the moment the runtime fails to find an IMP; message forwarding (also a later module) works by giving the runtime a second and third chance after that failure. All three are runtime features, not language syntax — they are things libobjc lets you do, not things the Objective-C grammar defines.
Class objects and metaclasses
Here is the idea that trips up almost everyone the first time: a class in Objective-C is itself an object at runtime. When you write NSString, you are naming a variable of type Class — a pointer to a class object that lives in memory just like an instance of NSString does. That class object has its own memory, its own isa pointer, and it responds to messages, exactly like any other object. This is why you can send a message directly to a class, such as [NSString stringWithFormat:@"%d", 5]: stringWithFormat: is a method being sent as a message to the NSString class object, not to some special compile-time construct.
If a class is an object, then — by the same rule that says every object's isa points to the class that defines its behavior — the class object's own isa must point to something. That something is the class's metaclass. The metaclass is a second, mostly invisible class object whose job is to hold the implementations of the class's methods, the ones declared with a leading + (class methods) such as stringWithFormat:. You never write "NSString's metaclass" in ordinary code and you never allocate one yourself; the runtime creates exactly one metaclass per class automatically, and it exists purely so that class objects — like any other object — can have their behavior looked up through an isa pointer.
Introduced carefully, this is genuinely coherent: an instance's isa points to its class, which holds instance methods (- methods). A class's isa points to its metaclass, which holds class methods (+ methods). The pattern is identical at both levels; the metaclass is simply "the class of the class." You will rarely interact with a metaclass by name, but understanding that it exists explains why class methods and instance methods can be looked up through exactly the same isa-and-method-list mechanism instead of needing two entirely different lookup systems.
NSObject: the root of the hierarchy and the runtime's bootstrap point
NSObject sits at the top of (almost) every Objective-C class hierarchy, and it plays two roles simultaneously. The familiar role is the one from earlier lessons: it is the base class that supplies memory-management machinery, equality and hashing defaults, description, and the class-introspection methods (isKindOfClass:, respondsToSelector:, and others) that every object inherits. The role this lesson adds is structural: NSObject is also where the metaclass chain bottoms out. Every metaclass's superclass is the metaclass of its class's superclass, all the way up, until you reach NSObject's metaclass — whose own superclass is NSObject itself (the class, not the metaclass). NSObject's metaclass's isa points back to NSObject's own class object, closing the loop.
You do not need to memorize that closing detail to write Objective-C well. What matters practically is simpler: NSObject is the common ancestor that guarantees every object and every class object you work with understands a baseline set of runtime-facing messages — class, superclass, isKindOfClass:, respondsToSelector:, and so on — because those methods are themselves defined using the runtime concepts in this lesson, walking isa and superclass pointers under the hood.
id: the fully dynamic object pointer
id is the type that makes Objective-C's duck-typing-style flexibility possible. Declared as a pointer to an object of unspecified class, a variable of type id can hold a reference to an instance of any class, and the compiler will let you send it any message without complaining at compile time, because it makes no compile-time promise about what class the object actually is. This is different from, say, an NSString * variable, where the compiler checks that the methods you call exist on NSString (or will warn if they do not).
id being untyped at compile time does not mean the object it points to is untyped at runtime. Every object id can point to still has a concrete class and a real isa pointer; id simply defers the question of which class until the message send actually happens, at which point the runtime resolves it using exactly the isa-and-method-list lookup process described later in this lesson. This is why passing the wrong kind of object through an id-typed parameter compiles cleanly but can crash at runtime with unrecognized selector — the safety net that a fixed type would have given you at compile time simply is not there.
SEL and IMP: naming a method versus running it
Two runtime types sit at the center of every message send, and it is worth being precise about the difference. A SEL (selector) is a registered, unique identifier for a method name — not the code that runs, just the name. You saw @selector(...) and SEL variables in the earlier Selectors lesson; the runtime maintains a table mapping every method name string it has seen to a unique SEL value, so that comparing two selectors is a fast pointer or integer comparison rather than a string comparison, and so the same SEL value represents "the method named foo:" consistently across every class that happens to implement a method with that name.
An IMP is the other half: a raw C function pointer to the actual compiled machine code of a specific method implementation. Where a SEL just names "foo:", an IMP is the literal address you would jump to in order to run one particular class's version of foo:. Every method a class implements has, at runtime, an entry pairing a SEL with the IMP that implements it for that class. Looking up a method is, at its core, the process of turning a SEL into the right IMP for a given object's class — which is exactly what the next section walks through.
Method lookup and dispatch
When you write [receiver message:arg], the compiler translates this into a call to the runtime's messaging function, conceptually objc_msgSend(receiver, @selector(message:), arg). objc_msgSend is where lookup and dispatch actually happen, and it proceeds in a predictable order. First, it follows the receiver's isa pointer to find the receiver's class object. Second, it searches that class's own method list for an entry matching the SEL being sent. If it finds one, it has the IMP, and it jumps directly to that function with the receiver and arguments — dispatch is complete.
If the class's own method list does not contain a matching SEL, objc_msgSend does not give up; it follows that class's superclass pointer and searches the superclass's method list, then that class's superclass, and so on, continuing up the chain toward NSObject. The first matching SEL found anywhere in that chain wins, and its IMP is what runs — which is precisely how method inheritance and overriding are implemented at the runtime level: overriding a method in a subclass means the subclass's own method list has an entry for that SEL that is found before the search ever reaches the superclass's entry for the same SEL.
If the search reaches the top of the class hierarchy without ever finding a matching SEL, ordinary dispatch has failed, and the runtime hands control to the dynamic-method-resolution and message-forwarding machinery instead of dispatching a method — covered in the next lesson and in a later module respectively. Only if every one of those recovery steps also fails does the program crash with the familiar unrecognized selector sent to instance error.
objc_msgSend is a plain C function with a signature roughly like id objc_msgSend(id self, SEL op, ...); every message send in a compiled Objective-C program is, at the machine-code level, a call to this function (or one of a small family of variants for special return types). Seeing this explicitly is useful precisely because it demystifies message sending: it is not special syntax invoking magic, it is a C function call into a library, with well-defined lookup rules.
The method cache
Walking up a superclass chain and scanning method lists on every single message send would be slow, especially for methods defined high up the hierarchy (like retain or description, inherited from NSObject) that get called constantly. The runtime avoids repeating that work with a per-class method cache: once objc_msgSend has done the full lookup for a given SEL on a given class and found the IMP, it stores that SEL-to-IMP mapping in a cache attached to the class. The next time any instance of that class receives a message with the same SEL, objc_msgSend checks the cache first, finds the mapping already there, and jumps straight to the IMP — skipping the superclass walk entirely.
This is why the first call to a given method on a given class can be marginally slower than subsequent calls, and why the runtime's performance in practice does not degrade noticeably even in hierarchies many levels deep: the expensive walk-and-search only happens once per class-and-selector combination, not once per call. The cache is invalidated automatically whenever the runtime changes a class's method list at runtime (for example, when class_addMethod or method swizzling modifies it), so a cached lookup never points at a stale IMP.
The isa relationship
Everything above rests on one pointer, so it deserves to be stated with complete precision. Every object — including every class object — carries an isa pointer as effectively its first piece of state. For an ordinary instance, isa points to the class that defines its behavior: the class holding its instance methods, its declared properties, and the protocols it conforms to. For a class object, isa does not point to another ordinary class; it points to that class's metaclass, which holds the class's own class methods.
The diagram below is the shape to keep in your head permanently: an Object's isa leads to its Class, and that Class in turn has its own Superclass pointer (continuing the hierarchy upward) plus its own Methods, Properties, and Protocols — the metadata the runtime consults on every lookup, introspection call, or KVC/KVO operation involving that object.
Class hierarchy, method lists, and properties/protocols as runtime metadata
Two separate pointers keep a class object properly positioned in the runtime's world: isa, which (as just covered) points upward to "what kind of thing is this class object" (its metaclass), and superclass, which points sideways-and-up to the next class in the inheritance chain, carrying method lookup further when a class's own method list comes up empty. Following superclass repeatedly from any class eventually reaches NSObject, whose own superclass pointer is nil, terminating the chain — nil superclass is precisely how the runtime (and your own code, if you walk the chain manually) knows it has reached the root.
Each class object stores its own method list: a table of SEL-to-IMP entries covering only the methods that class itself implements or overrides, not the methods it inherits. This is why the lookup process in the earlier section has to walk the superclass chain at all — a subclass's method list genuinely does not contain entries for inherited methods it does not override; those entries live only on the ancestor class that actually defines them, and the runtime finds them there each time (subject to caching).
Declared @property declarations and adopted protocols are likewise stored as structured metadata on the class object, not just as compiler bookkeeping that vanishes after compilation. The runtime exposes this metadata through introspection functions such as class_copyPropertyList (which returns the list of properties a class declares) and class_copyProtocolList (which returns the protocols it conforms to), both genuine C functions in the libobjc API. This lesson only needs you to know that this metadata exists and is runtime-accessible, not to use these functions fluently — reflecting on classes, properties, and protocols in depth is the subject of its own later module.
Core runtime types and the messaging function
objc_msgSend is declared in <objc/message.h>; you rarely call it directly, the compiler emits calls to it for you. Class is itself defined as a pointer type (to a struct objc_class) — consistent with 'a class is an object.'
// The four runtime primitives introduced in this lesson
id receiver; // fully dynamic object pointer
Class cls; // pointer to a class object
SEL selector; // registered method-name identifier
IMP implementation; // raw C function pointer to a method body
// Conceptually, every message send compiles down to a call like this:
id objc_msgSend(id self, SEL op, ...);
// [obj doSomething:@"x"] is translated by the compiler into:
objc_msgSend(obj, @selector(doSomething:), @"x");Code Examples
NSString here is a Class-typed value (the class object), and sending it stringWithFormat: is dispatched by the runtime exactly like sending a message to an instance — by following the class object's isa to its metaclass and searching there for stringWithFormat:.
Class metaOfString = object_getClass(stringClass);
sel names the method; imp is the actual code that runs it. methodForSelector: performs the same lookup objc_msgSend performs internally and hands you the resulting IMP directly, which is occasionally useful when you plan to call the same method many times and want to skip repeated dispatch overhead.
IMP imp = [str methodForSelector:sel];
class_getSuperclass is the runtime function backing the superclass pointer described in the class-hierarchy section. The loop terminates because NSObject's superclass pointer is nil, which is exactly how the runtime itself knows it has reached the top of the hierarchy.
c = class_getSuperclass(c);
class_copyPropertyList and class_copyProtocolList are genuine libobjc C functions that read the metadata a class object stores about its declared properties and adopted protocols — the same metadata the compiler generated from your @property and @protocol declarations. Both return caller-owned C arrays that must be freed. This example is intentionally brief — deep runtime introspection (enumerating methods, ivars, and building fully dynamic tooling from these APIs) is covered as its own later module.
objc_property_t *props = class_copyPropertyList([Person class], &count);
object_getClass follows isa: on an instance it returns the class; on a class object it returns the metaclass. class_getSuperclass follows the separate superclass pointer, which is what continues inheritance upward. Confusing these two pointers is the single most common source of runtime confusion covered in the mistakes below.
Class classIsa = object_getClass([Person class]); // -> Person's metaclass
// stringWithFormat: is a class method — it is sent to the NSString
// class OBJECT, not to any particular string instance.
NSString *greeting = [NSString stringWithFormat:@"Hello, %@", @"World"];
// Class is a real type you can hold in a variable:
Class stringClass = [greeting class]; // -> NSString
Class metaOfString = object_getClass(stringClass); // -> NSString's metaclassSEL sel = @selector(uppercaseString);
NSString *str = @"sampleapp";
// Look up the IMP for this selector on this object's class:
IMP imp = [str methodForSelector:sel];
// IMP has a C function pointer type; calling it directly means
// casting it to the right signature first:
NSString *(*upperFn)(id, SEL) = (NSString *(*)(id, SEL))imp;
NSString *result = upperFn(str, sel); // "SAMPLEAPP"Class c = [someObject class];
while (c != Nil) {
NSLog(@"%@", NSStringFromClass(c));
c = class_getSuperclass(c);
}
// Prints the class, then its superclass, up through NSObject,
// then stops once class_getSuperclass returns Nil.unsigned int count = 0;
objc_property_t *props = class_copyPropertyList([Person class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = property_getName(props[i]);
NSLog(@"Property: %s", name);
}
free(props);
Protocol * __unsafe_unretained *protos =
class_copyProtocolList([Person class], &count);Person *p = [[Person alloc] init];
Class instanceIsa = object_getClass(p); // -> Person (p's class)
Class classIsa = object_getClass([Person class]); // -> Person's metaclass
Class classSuperclass = class_getSuperclass([Person class]); // -> NSObject (or a parent)The isa relationship
An object's isa points to its class. A class object's own isa (not shown as a separate box here) points to its metaclass, which holds the class's + methods — the same shape one level up.
Object
│
└── isa
↓
Class
│
├── Superclass
├── Methods
├── Properties
└── ProtocolsImportant Rules
Every object, including every class object, has an isa pointer; it is what the runtime follows to find behavior.
A class is itself an object at runtime — an instance of its metaclass — which is why you can send messages directly to a class.
Instance methods live in a class's own method list; class methods live in that class's metaclass's method list.
objc_msgSend follows isa to the class, then searches the class's method list, then walks superclass pointers until a match is found or the chain ends at nil.
The method cache stores SEL-to-IMP mappings per class so repeated sends of the same selector skip the superclass walk after the first lookup.
SEL identifies a method by name; IMP is the actual function pointer that runs when that name is resolved for a specific class.
Why this module exists before the rest of the dynamic-features course
Every advanced Objective-C feature you will meet later — dynamic method resolution, message forwarding, method swizzling, key-value observing, and runtime introspection tooling — is implemented in terms of exactly the concepts in this lesson: isa, class objects, method lists, SEL, and IMP. Framework code you will read in real projects (for example, KVO's trick of creating a private dynamic subclass and swapping an observed object's isa to point at it) only makes sense once isa is something you actually understand as a real pointer with real consequences, not just a word you have heard. This lesson is the foundation the rest of the dynamic-features portion of the course is built on.
Treating id as equivalent to a plain, type-erased C pointer like void *.
Treat id as a pointer to a fully-formed object that still has a real class and a real isa pointer — the runtime always knows the object's true class, even though the compiler does not check it for you at the id call site.
The difference matters at the exact moment you send a message: with id, the compiler skips its usual compile-time method check, but the runtime still performs a full, real isa-based lookup at the moment the message is sent, and can still fail at runtime with unrecognized selector if the actual object does not implement it.
Assuming object_getClass and class_getSuperclass answer the same question.
Use object_getClass to follow isa (instance to class, or class to metaclass); use class_getSuperclass to follow the separate superclass pointer up the inheritance chain.
isa answers 'what kind of object is this' (and for a class object, that answer is its metaclass, not its parent class). superclass answers 'what does this class inherit from.' Swapping them gives you the metaclass when you wanted the parent class, or vice versa.
Believing a subclass's method list contains entries for every method it can respond to, including inherited ones.
Remember that a class's method list only contains methods that class itself implements or overrides; inherited methods are found only by continuing the search up the superclass chain.
This is precisely why method lookup has to walk superclass pointers at all — if every class's method list were a complete flattened copy of everything it could do, there would be no need for the walk, but also no simple way to override a single inherited method.
Assuming the method cache changes which method actually gets dispatched, or that it can return a stale result forever.
Understand the cache only speeds up finding an IMP the runtime already determined was correct; it is invalidated automatically whenever the runtime modifies a class's method list, so it never causes a wrong method to run.
The cache is purely a performance optimization over the same lookup rules described in this lesson, not a separate dispatch mechanism with its own logic — thinking of it as changing behavior rather than just speed leads to unnecessary worry about 'stale' dispatch that the runtime already handles for you.
Confusing a metaclass with 'the superclass's class,' or expecting to interact with metaclasses directly in everyday code.
Treat the metaclass purely as the automatic, mostly-invisible holder of a class's own class methods, created and managed entirely by the runtime — you almost never name or touch a metaclass directly.
The metaclass exists so that class objects can use the exact same isa-and-method-list lookup as instances. It is not a class you subclass, allocate, or reference by name in ordinary Objective-C code; it only matters for understanding how class methods are found.
What does an object's isa pointer point to? A. Its superclass B. The class that defines its behavior C. Its metaclass directly D. A cached IMP
B. The class that defines its behavior — isa points to the object's class (for a class object, that 'class' is its metaclass — but for an ordinary instance, isa points straight to its class).
Why does a class object need its own isa pointer at all? A. It doesn't — only instances have isa B. Because a class is itself an object at runtime, and its isa points to its metaclass, which holds its class methods C. isa on a class is only used for memory management D. Because superclass and isa are the same pointer for class objects
B. Because a class is itself an object at runtime, and its isa points to its metaclass, which holds its class methods — A class object is a real object at runtime; following its isa leads to its metaclass, the place where its class (+) method implementations are stored.
What is the practical difference between a SEL and an IMP? A. They are two names for the same thing B. A SEL identifies a method by name; an IMP is the actual function pointer that runs when a lookup for that name succeeds on a given class C. SEL is used only for class methods, IMP only for instance methods D. IMP is a compile-time construct with no runtime representation
B. A SEL identifies a method by name; an IMP is the actual function pointer that runs when a lookup for that name succeeds on a given class — SEL is a registered name identifier; IMP is the raw C function pointer to the code that actually executes. Method lookup is the process of turning a SEL into the right IMP.
When objc_msgSend can't find a matching SEL in a class's own method list, what does it do next? A. Immediately crashes with unrecognized selector B. Follows the superclass pointer and searches that class's method list, repeating up the chain C. Follows the isa pointer to the metaclass instead D. Returns nil silently without searching further
B. Follows the superclass pointer and searches that class's method list, repeating up the chain — Lookup walks the superclass chain, checking each ancestor's own method list, until a match is found or the chain is exhausted (at which point dynamic resolution and forwarding get a chance before a crash).
What problem does the per-class method cache solve? A. It prevents subclasses from overriding methods B. It avoids repeating the superclass-walk-and-search for a SEL that has already been resolved once for that class C. It stores instance variable values for faster access D. It replaces the need for an isa pointer
B. It avoids repeating the superclass-walk-and-search for a SEL that has already been resolved once for that class — Once objc_msgSend resolves a SEL to an IMP for a class, it caches that mapping so future sends of the same selector to instances of that class skip the superclass walk.
Why is 'id' able to hold a reference to an instance of any class and still be sent any message at compile time? A. Because id objects have no class at runtime B. Because id defers the class check to runtime; the compiler skips its usual method-existence check, but the runtime still resolves the real class via isa when the message is actually sent C. Because id is a subclass of NSObject D. Because the compiler converts every id message into a C function call with no lookup
B. Because id defers the class check to runtime; the compiler skips its usual method-existence check, but the runtime still resolves the real class via isa when the message is actually sent — id makes no compile-time class promise, so the compiler does not check the message against a known set of methods, but at runtime the object still has a genuine isa-defined class, and dispatch still goes through the normal lookup process.
Which runtime functions would you use to read a class's declared properties and adopted protocols? A. objc_msgSend and method_getImplementation B. class_copyPropertyList and class_copyProtocolList C. class_addMethod and class_replaceMethod D. resolveInstanceMethod: and resolveClassMethod:
B. class_copyPropertyList and class_copyProtocolList — class_copyPropertyList and class_copyProtocolList are the libobjc introspection functions that expose a class's property and protocol metadata; the other pairs belong to dispatch and dynamic method resolution, covered elsewhere.
Without running any code, trace through what objc_msgSend does for the message [myButton setTitle:@"OK"] if setTitle: is defined on UIControl (an ancestor of UIButton) and not overridden by UIButton itself. Describe, step by step, which pointers are followed and which method lists are searched, in order, until the IMP is found.
1) objc_msgSend follows myButton's isa to its class, UIButton. 2) It searches UIButton's own method list for setTitle: and does not find it, because UIButton does not override setTitle:. 3) It follows UIButton's superclass pointer to UIControl. 4) It searches UIControl's method list and finds setTitle:, obtaining its IMP. 5) It calls that IMP with myButton and the selector and argument. 6) On any future setTitle: send to a UIButton instance, the method cache on UIButton short-circuits steps 2-4.
Start at myButton's isa, and remember that a class's own method list only holds what that class itself implements or overrides.