Runtime Introspection: Examining Classes and Objects at Runtime
Learn the C-level Objective-C runtime functions used to inspect classes, methods, properties, and protocols at runtime, and connect them to the everyday high-level introspection methods — respondsToSelector:, isKindOfClass:, and isMemberOfClass: — that every Objective-C developer relies on.
Every earlier lesson in this course has treated the Objective-C runtime as a machine that quietly does its job underneath your code: it resolves [obj doSomething] into a lookup in a class's method table and calls the right implementation. This lesson turns that machine into something you can talk to directly. The Objective-C runtime is not a black box — it exposes a full C API, declared in <objc/runtime.h>, that lets you ask a class its name, walk its inheritance chain, list every method and property it implements, and inspect the requirements of a protocol, all without writing a single line of Objective-C message syntax. On top of that low-level API sits a small set of everyday Objective-C methods — respondsToSelector:, isKindOfClass:, isMemberOfClass: — that you have almost certainly already typed without thinking much about what they call underneath. By the end of this lesson you'll see the C runtime functions and the high-level methods as one continuous toolkit, not two unrelated topics, and you'll know precisely when to reach for which.
Why Introspection Exists
Introspection is the ability of a program to examine its own structure while it runs — to ask 'what class is this object, really?' or 'does this class have a method called that?' instead of assuming the answer was fully known at compile time. Objective-C was built for this kind of dynamism from day one, because its message-sending model (covered in the Dynamic Messaging lesson) already defers the question of 'which method actually runs' until runtime. Introspection is simply the natural extension of that idea: if the runtime decides which method to call at runtime, it makes sense that your code can ask the runtime what it knows, at runtime too.
This matters in real programs far more often than it first appears. Frameworks that accept arbitrary delegate objects need to check which optional methods a delegate actually implements before calling them. Serialization and object-copying code needs to enumerate a class's properties without the programmer hand-listing them. Debugging tools, crash reporters, and even Key-Value Coding itself lean on these same runtime functions to do their work. Introspection is the machinery that makes Objective-C's dynamism inspectable and controllable, rather than just mysterious.
The runtime API divides naturally into two layers, and this lesson covers both, moving from the lower layer up to the layer you'll use constantly. The lower layer is a set of plain C functions — class_getName, object_getClass, class_getSuperclass, class_copyMethodList, class_copyPropertyList, and the protocol-metadata functions — that operate directly on the runtime's own data structures (Class, Method, objc_property_t, Protocol *). The higher layer is a handful of Objective-C instance methods, inherited from NSObject, that wrap these C functions into convenient, everyday message sends: respondsToSelector:, isKindOfClass:, and isMemberOfClass:.
Naming and Identity: class_getName and object_getClass
class_getName is the simplest introspection function in the whole API: given a Class, it returns that class's name as a plain C string (const char *). It's most useful for logging, debugging, and diagnostic output, where you want to print exactly what kind of object you're dealing with without hand-writing a switch over known types. NSLog(@"%s", class_getName([obj class])); prints something like NSMutableArray or Person — whatever the actual runtime class name is, even for classes you didn't write and don't have source for.
object_getClass takes an *instance* (or, more precisely, any id, including a class object, since classes are objects too — covered in the Class Methods lesson) and returns its class. At first glance this looks identical to sending the -class message, [obj class], which NSObject also implements and which most objects also just return their true class from. The difference matters in one specific, important edge case: -class is an ordinary Objective-C method, which means it can be overridden. Code that performs *isa-swizzling* — a technique used by some mocking frameworks, KVO's own internal implementation, and certain debugging tools — dynamically changes an object's isa pointer to a different, often dynamically-created subclass, and such code sometimes also overrides -class on that subclass to lie and report the *original* class name, so that well-behaved calling code doesn't notice the swap.
object_getClass, by contrast, reads the object's isa pointer directly at the C level — it cannot be fooled by an overridden -class method, because it doesn't send a message at all; it inspects the object's memory layout. This makes object_getClass the more reliable, 'ground truth' way to ask what an object's actual runtime class is, which is exactly why low-level runtime tooling and frameworks that need to see through isa-swizzling use object_getClass instead of [obj class]. For everyday application code that isn't fighting isa-swizzling, [obj class] and object_getClass(obj) behave identically and either is fine — but understanding the distinction is what separates 'knows the API' from 'understands why the API has two ways to ask the same question'.
Walking the Class Hierarchy: class_getSuperclass
class_getSuperclass takes a Class and returns its immediate superclass as a Class, or Nil if the class passed in is a root class (like NSObject itself, which has no superclass). This lets you walk a class's inheritance chain programmatically, one link at a time, entirely in code — no need to already know a class's ancestry from documentation or source.
A common pattern is a small loop that walks from a class all the way up to the root, printing or collecting each ancestor's name along the way — useful for debugging an unfamiliar class hierarchy, or for generic code (like a custom object inspector or a serialization library) that needs to know, for example, whether a class descends from NSManagedObject or UIView without hard-coding that specific check using isKindOfClass: on an instance you might not have yet.
This function is also exactly what powers many higher-level Objective-C features under the hood. When you call isKindOfClass: (covered later in this lesson), the runtime is effectively doing this same walk — starting at the object's class and following class_getSuperclass links upward, comparing each ancestor against the target class, until it either finds a match or runs out of superclasses.
Enumerating Methods and Properties
class_copyMethodList answers the question 'what methods does this class actually implement?' by returning a heap-allocated C array of Method values — one per method the class directly implements (not counting inherited methods from superclasses, which you'd have to walk up separately using class_getSuperclass to collect). It takes the class and an unsigned int *outCount pointer that it fills in with the array's length. Because the array is allocated on the heap by the runtime, the caller owns it and is responsible for calling free() on it when done — a detail that's easy to forget and worth calling out explicitly, since forgetting it is a real, if small, memory leak.
class_copyPropertyList follows the identical shape and the identical ownership contract: it takes a class and an outCount pointer, and returns a heap-allocated array of objc_property_t values describing every @property the class declares, which likewise must be free()'d by the caller. From each objc_property_t you can recover the property's name (property_getName) and its full attribute-encoding string (property_getAttributes), which packs together its type, its atomic/nonatomic setting, its ownership qualifier (strong, weak, copy, assign), and whether it's read-only — all as a single compact C string that frameworks like serializers and object-mappers parse to auto-generate code.
Both functions exist because Objective-C classes are, underneath the language's syntax, just C data structures the runtime maintains — a Class has a method list and a property list sitting in memory, and these two functions are direct, low-level accessors onto those lists. This is the same underlying data that Xcode's own tooling, NSObject's description-building machinery, and third-party model-mapping libraries (which auto-populate an object's properties from a JSON dictionary) read to do their work — nothing about them is 'private' or off-limits; they're the same public introspection surface every Objective-C program can use.
• class_copyMethodList(cls, &count) — array of Method, caller must free(). • class_copyPropertyList(cls, &count) — array of objc_property_t, caller must free(). • Both list only what the class itself declares, not what it inherits — walk class_getSuperclass separately to collect inherited members.
Protocol Metadata
Protocols (covered in an earlier module) are themselves inspectable at runtime, using a parallel set of functions. class_copyProtocolList takes a class and returns the list of protocols that class declares conformance to (again as a heap array the caller must free()), letting you ask 'does this class claim to implement <NSCoding>?' entirely in code, without needing conformsToProtocol: and a hard-coded protocol name known in advance.
protocol_getMethodDescription goes a level deeper: given a Protocol * and a selector, along with flags for whether you're asking about a required-vs-optional method and an instance-vs-class method, it returns a struct objc_method_description describing that specific method's name and type-encoding string, *if* the protocol declares it. This is precisely the mechanism that lets delegate-pattern code verify, at runtime, exactly what a protocol requires — before ever having a concrete conforming object in hand.
Protocol introspection matters because protocols, unlike classes, have no implementation of their own to inspect — a protocol is purely a contract, a list of method signatures with no bodies. protocol_getMethodDescription is how you read that contract programmatically, which is exactly what tools like Xcode's own conformance checking and runtime delegate-validation code do under the hood.
Method Signatures and Type Encodings
Every Method obtained from class_copyMethodList carries a type-encoding string, retrievable with method_getTypeEncoding, that describes the method's full signature — its return type and every parameter type — as a single compact C string using Objective-C's type-encoding format. This is the same encoding format that powers @encode(), NSMethodSignature, and key-value coding's type checks.
The encoding uses short letter codes for primitive types (i for int, f for float, d for double, c for char/BOOL, v for void), @ for an object pointer, : for a selector (SEL), # for a class (Class), and ^ prefixed to another code for a pointer to that type. A method declared as - (int)addNumber:(int)a toNumber:(int)b; encodes roughly as i24@0:8i16i20 — the leading i is the return type, followed by digit-annotated offsets for the hidden self (@), the hidden _cmd selector (:), and each explicit parameter, packed together into one string the runtime can parse without needing full compiler-level type information at runtime.
You will rarely decode one of these strings by hand in application code — but recognizing the format when you see it in a debugger, in NSMethodSignature output, or in runtime-manipulation code (like method swizzling, which relies on matching signatures) is a genuinely useful skill, and it demystifies what Method and SEL actually are: not magic, just C data the runtime maintains and reads.
The Everyday High-Level Methods
Everything above is C-level machinery you'll reach for occasionally, mostly when writing frameworks, tools, or genuinely dynamic code. But NSObject wraps a slice of this same machinery into three ordinary Objective-C instance methods that you will use constantly, in completely everyday application code — and understanding them as thin wrappers over the runtime functions above makes them far less mysterious.
respondsToSelector: asks an object, at runtime, whether it implements a given method — if ([delegate respondsToSelector:@selector(controllerDidFinish:)]) { [delegate controllerDidFinish:self]; }. This is the standard, safe way to call an *optional* protocol method: calling a method an object doesn't implement crashes with 'unrecognized selector' (as covered in the Dynamic Messaging lesson), so checking first with respondsToSelector: avoids that crash entirely. Underneath, this is effectively consulting the same method-list data that class_copyMethodList exposes — it's asking 'is there an entry for this selector anywhere in this object's class or its superclasses' method lists?'
isKindOfClass: asks whether an object is an instance of a given class, *or any subclass of it* — if ([shape isKindOfClass:[UIView class]]) is true for a UIView instance, and equally true for a UIButton instance, since UIButton is a UIView subclass. Underneath, this is exactly the class_getSuperclass-walking loop described earlier: start at the object's actual class, and walk upward comparing against the target class until you find a match or run out of ancestors. Because it honors inheritance, isKindOfClass: is the check you want for essentially all everyday polymorphic type-checking — 'is this thing usable as a UIView' should say yes for every UIView subclass, not just literal UIView instances.
isMemberOfClass: vs. isKindOfClass:
isMemberOfClass: is the stricter sibling: it asks whether an object is an instance of *exactly* the given class, with no allowance for subclasses. [button isMemberOfClass:[UIView class]] is NO for a UIButton instance, even though UIButton is unquestionably a kind of UIView — because isMemberOfClass: refuses to walk up the hierarchy at all; it does one direct comparison against the object's own immediate class and stops there.
The contrast is worth internalizing precisely because it's easy to reach for the wrong one. isKindOfClass: answers 'can I treat this object as a UIView?' — the question you almost always actually want to ask, because polymorphism is the point of object-oriented design: code written against UIView should work correctly for every subclass, without special-casing each one. isMemberOfClass: answers a much narrower question — 'is this object *precisely* this exact class, not a subclass, not a variant?' — which is occasionally exactly what you need (for instance, a factory or cache that must distinguish between a base class and a specialized subclass and treat them differently), but is the wrong tool for ordinary type-checking.
As a rule, default to isKindOfClass: unless you have a specific, deliberate reason to exclude subclasses. Reaching for isMemberOfClass: out of habit silently breaks polymorphism: code that works today against a base class can mysteriously stop working the moment someone introduces a subclass, purely because a stray isMemberOfClass: check rejected it. That's a subtle, hard-to-spot bug class worth actively avoiding.
Core Runtime Introspection API
Anything returned by a class_copyXList function is a heap allocation you must free() when done. object_getClass reads the isa pointer directly and cannot be fooled by an overridden -class method. isKindOfClass: walks the superclass chain; isMemberOfClass: does a single, exact comparison.
#import <objc/runtime.h>
// Naming and identity
const char *class_getName(Class cls);
Class object_getClass(id obj);
Class class_getSuperclass(Class cls);
// Enumeration (caller must free() the returned array)
Method *class_copyMethodList(Class cls, unsigned int *outCount);
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount);
Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount);
// Protocol metadata
struct objc_method_description protocol_getMethodDescription(
Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod);
// Method signature (type encoding)
const char *method_getTypeEncoding(Method m);
// High-level NSObject wrappers used every day
- (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;Code Examples
class_getName turns a Class into a plain C string for logging. object_getClass reads the object's isa pointer directly rather than sending a -class message — for an ordinary object like this one, the two approaches agree, but object_getClass is the one that can't be fooled by an overridden -class implementation.
const char *name = class_getName([p class]);
This loop walks the class hierarchy manually using class_getSuperclass, one link at a time, stopping when it reaches Nil (the root class has no superclass). This is conceptually the exact same walk isKindOfClass: performs internally when checking whether an instance's class is, or descends from, some target class.
cls = class_getSuperclass(cls);
class_copyMethodList and class_copyPropertyList both return heap-allocated arrays that the caller is responsible for freeing — forgetting the free() calls here would leak memory every time this code runs. method_getTypeEncoding recovers each method's full type-encoding string, the same compact format @encode() produces.
Common mistake: Forgetting to call free(methods) and free(props) after using the returned arrays — both functions document that the caller owns the returned memory.
respondsToSelector: guards the call to an @optional protocol method, avoiding an unrecognized-selector crash for delegates that don't implement it. logShapeType: contrasts isKindOfClass: (true for UIView and every subclass, like UIButton) against isMemberOfClass: (true only for a literal, exact UIView instance) — passing a UIButton would log the first message but not the second. Expected result: Usable as a UIView (could be UIView or any subclass) Is some UIView subclass, or not a UIView at all
Person *p = [[Person alloc] init];
const char *name = class_getName([p class]);
NSLog(@"Class name: %s", name);
// Prints: Class name: Person
Class actualClass = object_getClass(p);
NSLog(@"object_getClass: %@", actualClass);
// Prints: object_getClass: PersonClass cls = [UIButton class];
NSMutableString *chain = [NSMutableString string];
while (cls != Nil) {
[chain appendFormat:@"%s -> ", class_getName(cls)];
cls = class_getSuperclass(cls);
}
[chain appendString:@"Nil (root reached)"];
NSLog(@"%@", chain);
// Prints something like:
// UIButton -> UIControl -> UIView -> UIResponder -> NSObject -> Nil (root reached)unsigned int count = 0;
Method *methods = class_copyMethodList([Person class], &count);
for (unsigned int i = 0; i < count; i++) {
SEL selector = method_getName(methods[i]);
const char *encoding = method_getTypeEncoding(methods[i]);
NSLog(@"%@ -> %s", NSStringFromSelector(selector), encoding);
}
free(methods); // required: class_copyMethodList allocates on the heap
// --- Properties, same shape and same ownership contract ---
unsigned int propCount = 0;
objc_property_t *props = class_copyPropertyList([Person class], &propCount);
for (unsigned int i = 0; i < propCount; i++) {
const char *propName = property_getName(props[i]);
NSLog(@"Property: %s", propName);
}
free(props); // same rule: caller owns and must free the array// A delegate pattern that safely calls an OPTIONAL protocol method,
// and distinguishes exact-class checks from polymorphic ones.
@protocol DownloadDelegate <NSObject>
@required
- (void)downloadDidFinish:(NSData *)data;
@optional
- (void)downloadDidUpdateProgress:(float)progress;
@end
- (void)reportProgress:(float)progress {
if ([self.delegate respondsToSelector:@selector(downloadDidUpdateProgress:)]) {
[self.delegate downloadDidUpdateProgress:progress];
}
}
- (void)logShapeType:(id)shape {
if ([shape isKindOfClass:[UIView class]]) {
NSLog(@"Usable as a UIView (could be UIView or any subclass)");
}
if ([shape isMemberOfClass:[UIView class]]) {
NSLog(@"Is EXACTLY a plain UIView instance, not a subclass");
} else {
NSLog(@"Is some UIView subclass, or not a UIView at all");
}
}Where This Shows Up in Real Codebases
Nearly every Objective-C framework you've used leans on this exact toolkit without advertising it. Key-Value Observing installs a dynamically-created isa-swizzled subclass on observed objects, which is precisely the scenario where object_getClass and [obj class] can disagree. Mocking and testing frameworks use class_copyMethodList to auto-generate stub implementations for every method a class exposes. JSON/model mapping libraries (the kind that turn a network response into a Person object automatically) use class_copyPropertyList to discover a class's properties without you having to hand-write a mapping for each one. And delegate-heavy Apple frameworks — UITableView, NSURLSession, and dozens of others — are built entirely around respondsToSelector: checks against @optional protocol methods, which is why so many delegate methods in Apple's own frameworks are optional in the first place: the framework checks before calling, exactly like the download-progress example above.
Method *m = class_copyMethodList([Person class], &count); /* ...use m... */ // no free(m)
Method *m = class_copyMethodList([Person class], &count); /* ...use m... */ free(m);
class_copyMethodList (and class_copyPropertyList, class_copyProtocolList) allocate their returned arrays on the heap. The caller owns that memory and must free() it; skipping this is a real, if usually small, memory leak.
if ([obj isMemberOfClass:[UIView class]]) { /* treat as a view */ } if ([obj isKindOfClass:[UIView class]]) { /* treat as a view */ } isMemberOfClass: rejects every subclass, including perfectly ordinary ones like UIButton or UILabel. Using it for a general 'can I treat this as a view' check silently breaks for any subclass — almost always the wrong choice for polymorphic code; isKindOfClass: is what you want instead.
[self.delegate downloadDidUpdateProgress:progress]; // called unconditionally if ([self.delegate respondsToSelector:@selector(downloadDidUpdateProgress:)]) { [self.delegate downloadDidUpdateProgress:progress]; }
Calling an @optional protocol method without first checking respondsToSelector: crashes with 'unrecognized selector sent to instance' the moment a delegate that doesn't implement it is used — respondsToSelector: is exactly the guard the optional/required protocol design expects you to use.
Class c = [obj class]; // assumed always reliable Class c = object_getClass(obj); // reads isa directly, immune to an overridden -class
[obj class] is an ordinary Objective-C method and can be overridden — code performing isa-swizzling (some mocking frameworks, KVO internals) sometimes does exactly that, to hide the swap. object_getClass reads the isa pointer directly and can't be fooled this way, which is why low-level tooling prefers it.
What does class_getName return? A. A pointer to the class's implementation B. The class's name as a C string (const char *) C. The number of methods the class implements D. A new instance of the class
B. The class's name as a C string (const char *) — class_getName converts a Class value into its plain C-string name, most commonly used for logging and debugging output.
Why might object_getClass(obj) be preferred over [obj class] in low-level tooling? A. object_getClass is faster to type B. [obj class] doesn't exist on all objects C. object_getClass reads the isa pointer directly and can't be fooled by an overridden -class method (e.g. from isa-swizzling) D. object_getClass returns the superclass instead of the class
C. object_getClass reads the isa pointer directly and can't be fooled by an overridden -class method (e.g. from isa-swizzling) — [obj class] is an ordinary, overridable Objective-C method; code that performs isa-swizzling (some mocking frameworks, KVO) can override -class to hide the swap. object_getClass inspects the isa pointer directly, so it always reports the object's true runtime class.
What must you do after calling class_copyMethodList or class_copyPropertyList? A. Nothing — the runtime cleans it up automatically B. Call free() on the returned array, since the caller owns that heap allocation C. Call release on each Method or objc_property_t D. Call class_getSuperclass to finish the enumeration
B. Call free() on the returned array, since the caller owns that heap allocation — Both functions return heap-allocated C arrays that the caller owns and must free() explicitly — skipping this leaks memory.
What is the key behavioral difference between isKindOfClass: and isMemberOfClass:? A. They are exact synonyms with no difference B. isKindOfClass: only checks class methods; isMemberOfClass: only checks instance methods C. isKindOfClass: returns YES for the exact class or any subclass; isMemberOfClass: returns YES only for the exact class, never a subclass D. isMemberOfClass: is deprecated and isKindOfClass: replaced it entirely
C. isKindOfClass: returns YES for the exact class or any subclass; isMemberOfClass: returns YES only for the exact class, never a subclass — isKindOfClass: walks the superclass chain, so it accepts subclasses. isMemberOfClass: performs one exact comparison against the object's immediate class only, rejecting every subclass — which is why it's the wrong tool for most polymorphic type checks.
What is respondsToSelector: primarily used for in everyday Objective-C code? A. Freeing memory allocated by class_copyMethodList B. Safely checking whether an object implements a given method before calling it — most commonly for @optional protocol methods C. Converting a selector into a C string D. Walking a class's superclass chain
B. Safely checking whether an object implements a given method before calling it — most commonly for @optional protocol methods — respondsToSelector: lets you check whether an object implements a method — most importantly an @optional protocol method — before sending that message, avoiding an unrecognized-selector crash.
class_getSuperclass([NSObject class]) returns what? A. id B. NSObject itself C. Nil, because NSObject is a root class with no superclass D. A compiler error
C. Nil, because NSObject is a root class with no superclass — NSObject sits at the root of the class hierarchy in this scenario, so it has no superclass; class_getSuperclass returns Nil, which is exactly the stopping condition used when walking a hierarchy upward.
Write a function -(void)describeObject:(id)obj that: (1) logs the object's class name using class_getName, (2) walks and logs every ancestor in its class hierarchy using class_getSuperclass, (3) reports whether the object responds to a selector called doSomething, and (4) reports whether the object isKindOfClass:[NSString class] versus isMemberOfClass:[NSString class] for both an NSString literal and an NSMutableString instance, explaining why the two calls can disagree for NSMutableString.
• (void)describeObject:(id)obj { NSLog(@"Class: %s", class_getName([obj class])); Class cls = [obj class]; while (cls) { NSLog(@" ancestor: %s", class_getName(cls)); cls = class_getSuperclass(cls); } NSLog(@"respondsToSelector:doSomething = %d", [obj respondsToSelector:@selector(doSomething)]); NSLog(@"isKindOfClass:NSString = %d", [obj isKindOfClass:[NSString class]]); NSLog(@"isMemberOfClass:NSString = %d", [obj isMemberOfClass:[NSString class]]); }
NSMutableString is a subclass of NSString, so isKindOfClass:[NSString class] is YES for both, but isMemberOfClass:[NSString class] is NO for the NSMutableString instance — the same asymmetry as the UIView/UIButton example in this lesson.