Objective-C’s Message Passing: When Method Calls Aren’t Really Method Calls
When you write code in Java or C++, calling a method seems straightforward—the compiler knows exactly which piece of code will run. But Objective-C does something fundamentally different. Instead of calling methods, it sends messages. This distinction might sound like semantics, but it fundamentally changed how iOS developers built applications for over two decades.
1. The Smalltalk Heritage: Where It All Began
To understand Objective-C’s approach, we need to travel back to the 1970s at Xerox PARC, where researchers developed Smalltalk, one of the first truly object-oriented programming languages. Smalltalk introduced a revolutionary concept: objects don’t execute methods directly—they receive and respond to messages.
In the early 1980s, Brad Cox and Tom Love created Objective-C by combining the efficiency of C with Smalltalk’s elegant message-passing model. The goal was simple yet ambitious: bring the power of object-oriented programming to systems-level development without sacrificing performance. When Steve Jobs founded NeXT after leaving Apple, he chose Objective-C as the foundation for the NeXTSTEP operating system. This decision would shape the future of Apple’s platforms—when Apple acquired NeXT in 1997, Objective-C became the backbone of macOS and iOS development.
2. Messages vs. Methods: Understanding the Difference
In traditional object-oriented languages like Java or C++, when you call a method, the compiler resolves which code to execute at compile time. This is called static binding or early binding. The compiler checks that the method exists, verifies the parameter types, and links the call directly to the implementation.
Objective-C takes a completely different approach. When you write something like this:
[myObject performAction:someParameter];
You’re not calling a method—you’re sending a message. The object myObject receives the message performAction: with someParameter as an argument. The actual method to execute isn’t determined until runtime.
2.1 How Message Passing Works Under the Hood
Every Objective-C object contains a pointer to its class, and every class maintains a dispatch table—essentially a dictionary that maps method names (called selectors) to their implementations. When a message is sent, the runtime system performs these steps:
- Step 1: The runtime receives the message and looks up the object’s class
- Step 2: It searches the class’s dispatch table for the selector
- Step 3: If found, it calls the corresponding function; if not, it checks the superclass
- Step 4: This continues up the inheritance chain until the method is found or message forwarding kicks in
This dynamic resolution happens through the objc_msgSend function—a highly optimized C function that handles every message sent in an Objective-C program. The Apple documentation provides detailed insights into this mechanism.
3. Dynamic Features That Changed iOS Development
The runtime resolution of messages enables powerful features that are impossible or extremely difficult in statically-typed languages like Java:
3.1 Method Swizzling
Because methods are resolved at runtime, you can actually swap method implementations while the program is running. This technique, called method swizzling, allows developers to modify the behavior of existing classes—even system frameworks—without access to their source code. While this can be controversial, it enabled crucial debugging tools and dynamic behavior modifications that made iOS development more flexible.
3.2 Dynamic Type Checking
Objective-C allows you to check whether an object responds to a particular message before sending it. This enables polymorphism in ways that go beyond traditional inheritance:
if ([myObject respondsToSelector:@selector(someMethod)]) { [myObject someMethod]; }
3.3 Message Forwarding
Perhaps most powerfully, when an object receives a message it doesn’t recognize, instead of immediately crashing, Objective-C gives the object multiple chances to handle it. The object can forward the message to another object, dynamically add the method, or handle the situation gracefully. This enables sophisticated design patterns like proxies and delegates that became fundamental to iOS development.
4. The NSInvocation System: Messages as Objects
Objective-C takes message passing even further with NSInvocation. This system allows you to package a message—including the target, selector, and arguments—into an object. You can store these message objects, pass them around, and invoke them later. This proved essential for implementing features like undo/redo functionality, queued operations, and distributed computing.
5. The Performance Trade-off
All this flexibility comes at a cost. Every message send requires a runtime lookup, which is slower than a direct function call. The chart below illustrates the performance differences between dispatch mechanisms:
While message passing is slower than direct calls or vtable lookups, Apple’s engineers optimized objc_msgSend extensively. The runtime caches method lookups, and modern processors handle the indirection efficiently. For most applications, message passing overhead is negligible compared to actual work being done—rendering graphics, processing data, or handling network requests.
| Dispatch Type | Resolution Time | Flexibility | Used By |
|---|---|---|---|
| Direct Call (C) | Compile-time | None | C, Swift structs |
| Vtable (C++) | Compile-time table lookup | Limited | C++, Swift classes |
| Message Passing | Runtime dictionary lookup | Very High | Objective-C, Smalltalk |
6. Why Swift Moved Away: The Modern Alternative
When Apple introduced Swift in 2014, they made a deliberate choice to move away from universal message passing. Swift primarily uses vtable dispatch for classes and direct dispatch for structs and final methods. This decision reflects changing priorities:
6.1 Safety Over Flexibility
Swift emphasizes compile-time safety. The compiler catches type errors, missing methods, and other issues before code runs. Message passing’s flexibility meant these errors only appeared at runtime, leading to crashes that could have been prevented.
6.2 Performance for Modern Apps
As apps became more complex and performance-critical—especially for games, AR applications, and real-time processing—the overhead of message passing became more noticeable. Swift’s static dispatch enables aggressive compiler optimizations like inlining and dead code elimination.
6.3 The Best of Both Worlds
Interestingly, Swift didn’t completely abandon dynamic dispatch. Classes that inherit from Objective-C classes still use message passing, and you can mark methods with the @objc dynamic attribute to enable runtime features when needed. This pragmatic approach lets developers choose performance or flexibility based on their specific needs.
Key Insight: Swift’s approach acknowledges that while message passing is powerful, most code doesn’t need that level of dynamism. By making static dispatch the default and dynamic dispatch opt-in, Swift optimizes for the common case while preserving flexibility when necessary.
7. The Legacy and Lessons
Although Swift has largely replaced Objective-C for new development, message passing’s influence remains profound. Many of iOS’s most powerful frameworks—from Core Data to UIKit—were designed around message passing’s capabilities. Design patterns like delegation, target-action, and key-value observing all emerged from this dynamic foundation.
For developers maintaining legacy Objective-C code or working with mixed Swift/Objective-C codebases, understanding message passing remains essential. More broadly, Objective-C’s story illustrates a fundamental trade-off in language design: the balance between runtime flexibility and compile-time safety, between expressive power and predictable performance.
8. What We’ve Learned
Objective-C’s message passing system, inherited from Smalltalk, fundamentally changed how methods work by resolving calls at runtime rather than compile time. This enabled powerful dynamic features like method swizzling, message forwarding, and the NSInvocation system that made iOS development uniquely flexible for over 20 years.
However, this flexibility came with performance costs and reduced compile-time safety. As apps grew more complex and performance-critical, Apple created Swift with static dispatch as the default, preserving dynamic capabilities as an opt-in feature. This shift represents the evolution from “flexibility first” to “safety and performance first” in modern application development.
The message passing model taught us that there’s no single “best” approach to method dispatch—the right choice depends on your priorities, whether that’s runtime flexibility, compile-time safety, or raw performance. Understanding these trade-offs helps developers make informed decisions in any programming language.



