Always Processing

Objective-C Internals: Class Architecture

Two yellow English Labradors on a work table with pencils, paper, and other design instruments.

Objective-C has an unique class architecture where classes are objects. Its elegant design enables dynamic method dispatch for all object types through the use of a compiler generated metaclass.

Objective-C, like many popular languages, uses a class-based style of object-oriented programming. For the purposes of discussing Objective-C’s class architecture, we’ll focus on class behavior (methods and properties) and set aside class state (i.e., instance variables, which are covered in this post).

Although properties provide access to a class’s state, they are just syntactic sugar for methods in Objective-C. (Another good topic for a future post! 🤣)

The following six lines of code are all we need to explore Objective-C’s class architecture:

@interface MyObject: NSObject
+ (void)classMethod;
- (void)instanceMethod;
@end

MyObject *object = [[MyObject alloc] init];

Method Dispatch

Line 6 instantiates the class MyObject and assigns the new object instance to the variable object. The new object instance has its own state and can respond to messages (i.e. method calls) like -instanceMethod and -init.

Objective-C uses dynamic dispatch[1] for each message send (i.e. for each method call). The runtime finds the method implementation by looking up the selector (i.e. method name) in the class object referenced by the instance’s isa variable. (The first instance variable in all Objective-C objects is the isa pointer, which is automatically inserted by the compiler and initialized by the runtime.)

The use of the term class object in the previous paragraph was intentional: in Objective-C, classes are also objects! This ingenious design is the basis for the class methods language feature (e.g. calling [NSObject alloc] or [MyObject classMethod]): it enables class methods to be fully polymorphic (i.e. a subclass can override a class method), and it erases any runtime distinction between class methods and instance methods (both class and instance methods are dispatched via objc_msgSend).

Because classes are objects, they too have an isa pointer, which references the metaclass. The metaclass provides the selector-to-class method implementation map for the class object in the same way the class object provides the selector-to-instance method implementation map for a class instance (i.e. object).

Inheritance

Any class or metaclass only provides selector-to-method implementation mappings for methods implemented by the class. In the code example above, the class object for MyObject has a mapping for instanceMethod and the metaclass for MyObject has a mapping for classMethod.

MyObject also responds to +alloc and -init, which are implemented in NSObject. Each class object (including each metaclass) has a reference to its super class. When resolving a selector, if the class/metaclass object does not define a mapping, the runtime searches the next super class for a definition. (The runtime will throw an exception if no definition is found at the end of the super class chain, though the runtime also provides escape hatches to handle the scenario.)

Architecture Diagram

The following diagram illustrates the Objective-C class design discussed above:

RootMetaclassRootClasssuperSuperMetaclassSuperClassisaMetaclassClassInstanceisaisaisaisaisasupersupersuperisasuper
  • The object instance has an isa variable that points to the MyClass class object.

  • The MyClass class object has:

    • An isa variable that points to the MyClass metaclass.

    • A super variable that points to the NSObject class object.

  • The MyClass metaclass has:

    • An isa variable that points to the NSObject (root object) metaclass.

    • A super variable that points to the NSObject metaclass.

The diagram also shows a few points not captured by the discussion above:

  • The isa variable of each metaclass points to the metaclass of the root object, including the root object metaclass itself. It makes sense that the isa isn’t nil (an object has to have some type), but I’m not sure why all metaclasses are of the type of the root metaclass as opposed to, say, a type provided by the runtime. I suppose, though, it doesn’t actually matter as the metaclass itself never receives messages.

  • The super class of the root metaclass is the root class object. I was surprised to learn this when researching for the diagram. It’s not immediately clear to me why this link exists—​I thought its super class would be nil.

If anyone has insight into the rationale for these design artifacts, please reach out and let me know!


1. Clang, beginning in either Xcode 11.x or Xcode 12, added support for direct (i.e. static) dispatch. However, I’ve not been able to locate any reference to the feature in Xcode Release Notes or in WWDC sessions.