All writing

Understanding the Objective-C Runtime

Essential iOS foundations

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 isa information 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.

Objective-C instance, class, metaclass, and superclass relationshipsConceptual 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_getName
  • method_getTypeEncoding
  • method_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

OBJC_EXPORT id objc_msgSend(id self, SEL op, ...);
  • The declaration of Class:
typedef struct objc_class *Class;