I finally learned where Objective-C methods are stored after I wrote the wrong capability check for a plugin system. The loader received a plugin class and needed to ask whether it supported an optional class-level feature. I searched the ordinary class object for that selector, got NULL, and disabled a feature the plugin actually implemented. The bug was small, but it exposed a gap in my mental model: an Objective-C class is also an object, and its class methods are found through a meta-class.
I built the accompanying command-line project around that failure. It checks instance methods, class methods, inherited methods, and direct method lists separately. That distinction matters because Runtime APIs with similar names do not always answer the same question.
The Plugin Check That Returned the Wrong Answer
The plugin class had an instance method named renderReport: and a class method named supportsPrettyPrinting. My first check used class_getInstanceMethod(pluginClass, @selector(supportsPrettyPrinting)). That API starts instance-method lookup from the ordinary class object, so it correctly returned NULL. I had asked the wrong object.
The fix was not to special-case the plugin name. I needed to decide whether I was checking behavior on an instance or behavior on the class itself. For a class method, the corresponding method lookup starts from the class object’s meta-class. The public convenience function class_getClassMethod expresses that intent directly.
Objects, Classes, and Meta-Classes
A normal instance points to its class. That class provides the instance-method behavior used when a message is sent to the instance. The class object is itself an Objective-C object, and object_getClass(pluginClass) returns its meta-class. Methods declared with + are represented as instance methods of that meta-class.
Class pluginClass = [JSONReportPlugin class];
Class metaClass = object_getClass(pluginClass);
Method instanceMethod =
class_getInstanceMethod(pluginClass, @selector(renderReport:));
Method classMethod =
class_getInstanceMethod(metaClass,
@selector(supportsPrettyPrinting));
This does not mean methods are copied into every object. Instances hold their per-object state, while the method behavior belongs to the relevant class structure. I confirmed that by creating ten thousand plugin instances and seeing the same renderReport: implementation address for each one. I did inspect fields such as class_rw_t while debugging, but none of that layout was needed in the loader that shipped.
Direct Method Lists and Inherited Lookup Are Different
Another source of confusion is the word “stored.” class_copyMethodList returns the methods attached directly to the class passed to it; it does not return a flattened list of every inherited method. In contrast, class_getInstanceMethod can find a method through superclass lookup. Both results can be correct at the same time.
unsigned int count = 0;
Method *methods = class_copyMethodList(pluginClass, &count);
for (unsigned int index = 0; index < count; index++) {
NSLog(@"%s", sel_getName(method_getName(methods[index])));
}
free(methods);
In the demo, JSONReportPlugin inherits renderReport: from BaseReportPlugin. The subclass’s direct method list does not contain that selector, yet class_getInstanceMethod(JSONReportPlugin.class, @selector(renderReport:)) succeeds. I use direct-list inspection for diagnostics and inventory, but capability checks normally need inherited lookup or respondsToSelector:.
The Meta-Class Superclass Chain I Verify
The earlier version of this article said a subclass meta-class points directly to the superclass’s ordinary class object. That is not correct for a normal subclass. A subclass meta-class has the superclass’s meta-class as its superclass. This lets class methods participate in inheritance in the same general way that instance methods do.
Class pluginClass = [JSONReportPlugin class];
Class metaClass = object_getClass(pluginClass);
Class metaSuperclass = class_getSuperclass(metaClass);
NSLog(@"%@", class_isMetaClass(metaSuperclass) ? @"YES" : @"NO");
// YES: this is BaseReportPlugin's meta-class
The root of the graph has special relationships, which is why simplified diagrams often become misleading. For everyday code, I rarely need to walk to the root. The durable rules are enough: instance messages start with a class, class messages start with a meta-class, and each chain supports inherited lookup.
Why Class Methods Cannot Read Instance State
An instance method receives an instance as self, so it can access that object’s ivars. A class method receives the class object as self. There is no particular plugin instance from which it could read _report or another instance variable. This is a receiver difference, not simply a rule imposed by syntax.
When a class method needs instance data, I pass the data explicitly, create or receive an instance, or move the behavior to an instance method. Using global state to bridge the gap usually makes tests and concurrency harder. For plugin metadata such as an identifier or a supported-format flag, a class method is reasonable because the answer belongs to the plugin type rather than one plugin object.
What Method Caches Do and Do Not Guarantee
Method dispatch uses caches as an optimization, but I do not design application behavior around a particular bucket layout, starting capacity, or load factor. Those are implementation details and can change by architecture or operating-system release. It is also too broad to say that every call always inserts an entry. Cache state can be populated, invalidated, or rebuilt as the Runtime manages classes and method changes.
When I profiled the loader, selector lookup was not the slow part; report rendering dominated the trace. That result stopped me from adding an IMP cache that the feature did not need. The final code kept the ordinary method queries, while the cache inspection stayed in the small command-line experiment where it was useful.
Run the Complete Method-Location Demo
The companion repository is a Foundation-only macOS command-line project. It verifies an instance method on the class object, a class method on the meta-class, an inherited instance method, and the difference between direct method-list inspection and inherited lookup. It also confirms that the subclass meta-class’s superclass is another meta-class.
git clone https://github.com/2252408699/objc-method-location-inspector-demo.git
cd objc-method-location-inspector-demo
make run
A successful run prints six PASS lines and ends with All method-location checks passed. The project requires macOS and the Xcode Command Line Tools, but no simulator or third-party dependency. Run make clean to remove the executable.
The Runtime Questions I Ask in Code Review
- Is the receiver an instance or a class object?
- Does the check need direct declarations or inherited capability?
- Would
respondsToSelector:communicate the intent more clearly? - Is the code using a documented Runtime API?
- Does a category or dynamically added method affect the assumption?
- Is private cache or class-layout knowledge being treated as a guarantee?
Before changing the plugin loader, I ran the demo with an inherited instance method and a class method that shared a similar name. The six checks caught the original mistake immediately: direct-list inspection answered where a method was declared, while inherited lookup answered whether the receiver could use it. That distinction is now covered by the executable test instead of being left as a diagram in my notes.
Complete source code: objc-method-location-inspector-demo on GitHub
