100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Objective-C Runtime and Dynamism

How Objective-C's message-passing runtime enables introspection, method swizzling, and associated objects, and the risks that come with that power.

Practical Objective-CAdvanced11 min readJul 10, 2026
Analogies

What the Runtime Actually Does

Every Objective-C method call, written as '[receiver message:argument]', compiles down to a call to 'objc_msgSend(receiver, @selector(message:), argument)' — a C function that looks up the correct implementation to run at the moment the call happens, not at compile time. This lookup walks the receiver's class (and, if not found, its superclasses) searching its method dispatch table for the selector, caches the result for speed on subsequent calls, and then jumps to the matching 'IMP' (a plain C function pointer). Because this resolution happens dynamically, the same source-level message send can invoke completely different code depending on the object's actual runtime class, and — as later sections cover — the lookup itself can be intercepted and redirected.

🏏

Cricket analogy: It's like a captain calling 'bowl the next over' without specifying which bowler by name — the actual decision of who runs in happens at the moment of the call based on the current bowling rotation, not fixed weeks in advance when the squad was named, exactly like objc_msgSend resolving the implementation at call time.

Method Swizzling

Because method dispatch is just a table lookup from selector to IMP, that table can be modified at runtime — a technique called method swizzling. Using 'class_getInstanceMethod' to obtain 'Method' references for two selectors and 'method_exchangeImplementations' to swap their IMPs, you can make calls to one selector actually execute the code originally written for another, most commonly done in a category's '+load' method so the swap happens exactly once, early, before any instances are created. This is the mechanism behind analytics SDKs that transparently log every 'viewDidAppear:' call across an app without modifying a single line of the app's own view controllers — they swizzle UIViewController's implementation to call their logging code and then invoke the original implementation.

🏏

Cricket analogy: It's like secretly swapping which bowler's run-up and action gets triggered whenever the umpire calls 'over' — from that point on, calling for the 'next over' silently invokes a different bowler's actual delivery, exactly how swizzling swaps which IMP actually runs for a given selector.

objectivec
@implementation UIViewController (AnalyticsSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewDidAppear:);
        SEL swizzledSelector = @selector(analytics_viewDidAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        method_exchangeImplementations(originalMethod, swizzledMethod);
    });
}

- (void)analytics_viewDidAppear:(BOOL)animated {
    [self analytics_viewDidAppear:animated]; // now calls the ORIGINAL implementation
    NSLog(@"Screen appeared: %@", NSStringFromClass([self class]));
}

@end

Introspection with the Runtime API

Objective-C's dynamism extends to rich runtime introspection: 'respondsToSelector:' asks whether an object's class implements a given selector before you risk calling it, 'isKindOfClass:' and 'isMemberOfClass:' check an object's position in the class hierarchy, and lower-level functions like 'class_copyPropertyList' and 'class_copyMethodList' let you enumerate every property or method a class declares, which is exactly how tools like NSCoding's automatic archivers and JSON-mapping libraries implement generic serialization without you writing per-property code. This same introspection machinery underlies Key-Value Coding: 'valueForKey:' doesn't require a compile-time-known property; it resolves the accessor method or, failing that, the instance variable, by name at runtime.

🏏

Cricket analogy: It's like a team analyst checking a player's full statistical record — every format they've played, every role they've filled — before deciding whether to select them for a specific role, rather than assuming based on reputation alone; respondsToSelector: performs that same real-time capability check before risking a call.

Key-Value Observing (KVO) is itself implemented using runtime dynamism: when you call addObserver:forKeyPath:options:context: on an object, the runtime creates a private dynamic subclass of that object's class at runtime, isa-swizzles the instance to that subclass, and overrides the relevant setter to inject will/didChangeValueForKey: calls — all without you writing a single line of subclassing code.

Associated Objects

Objective-C categories let you add methods to an existing class, even one you don't own like NSObject or UIView, but categories famously cannot add instance variables — there's no room in the class's fixed memory layout to insert new storage after the fact. The runtime works around this with associated objects: 'objc_setAssociatedObject(object, key, value, policy)' attaches arbitrary extra state to any object instance at runtime, keyed by a unique pointer (typically a static 'const void *' constant), with a memory-management policy like 'OBJC_ASSOCIATION_RETAIN_NONATOMIC' mirroring a normal strong property; 'objc_getAssociatedObject' retrieves it later. This is exactly how libraries add a de-facto 'property' to a category, such as attaching a completion-handler block to a UIButton instance without subclassing it.

🏏

Cricket analogy: It's like a stadium unable to add a new permanent seat mid-tournament because the stands are fixed, so instead they issue a separate wristband tied to your existing ticket that grants extra access — objc_setAssociatedObject attaches that same extra 'wristband' of data to an object without altering its fixed original layout.

Method swizzling and heavy runtime introspection are powerful but risky: swizzling in a '+load' method that runs multiple times (e.g., from being applied in more than one category) can double-swap implementations back to their original state or crash; swizzled methods must always call through to the original implementation unless intentionally suppressing it; and Apple's App Review has historically flagged apps that swizzle private, undocumented UIKit selectors as attempting to access private API. Reserve swizzling for well-understood, narrowly scoped cross-cutting concerns like analytics or logging, not general application logic.

  • Every Objective-C message send compiles to objc_msgSend, which resolves the selector to an IMP dynamically at call time, not compile time.
  • Method swizzling exchanges the IMPs of two selectors via method_exchangeImplementations, typically done once in a category's +load.
  • respondsToSelector:, isKindOfClass:, and class_copyPropertyList/class_copyMethodList enable generic introspection without compile-time type knowledge.
  • Key-Value Coding's valueForKey: resolves accessors or ivars by name string at runtime rather than requiring compile-time property references.
  • KVO is implemented via runtime isa-swizzling to a private dynamic subclass, entirely transparent to the observed object's own code.
  • Associated objects (objc_setAssociatedObject/objc_getAssociatedObject) let categories attach extra per-instance state that the fixed class layout can't otherwise accommodate.
  • Swizzling and introspection are powerful but carry real risks: double-swapping, missing calls to the original implementation, and App Store scrutiny of private API misuse.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ObjectiveCStudyNotes#ObjectiveCRuntimeAndDynamism#Objective#Runtime#Dynamism#Actually#StudyNotes#SkillVeris#ExamPrep