Dictionary-Backed Objects
May, 2021
Today we’ll look at dynamic method resolution, one of the coolest features of the Objective-C runtime, which lets our app create new methods on the fly! 🚀
We’ll use the dynamic method resolution features of the runtime to to build a dictionary-backed class. What does “dictionary backed” mean? Well let’s first have a look at how normal classes work.
Take a Person
class declared as:
@interface Person: NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic) int age;
@property (nonatomic, copy, nullable) NSArray *siblings;
@end
This class needs some backing storage to actually store the string, int and array in memory. In Obj-C, classes are backed by C structs, which in this case would look like:
struct {
Class isa;
NSString *_name;
int _age;
NSArray *_siblings;
};
Objective-C knows to create, or synthesize, an ivar for each property because that’s the default behaviour if we don’t manually implement any getters or setters.
The isa
field is present at the start of every class struct, so that Obj-C knows which kind of class it represents. The other fields which provide storage for properties (“ivars”) are by default prefixed with an underscore.
Historical note: we used to have to write
@synthesize name = _name;
to explicitly declare storage for each property, but since 2012 Xcode has auto-synthesized these for us 🦕
Allocating storage space
When you instantiate an object from a class with new
or alloc
+init
, memory is allocated for the backing struct. This struct is always a fixed size, even though it might contain pointers to variable-size objects, such as the name
string or the array of siblings
.
This space needs to be allocated even for nullable properties which are not set, which is usually not an issue since they’re just pointers that take 64 bits each.
But imagine you had a big class with dozens of properties, many of which are nullable:
@interface Movie: NSObject
@property (nonatomic, copy) NSString *title;
@property (nonatomic) int year;
@property (nonatomic) NSTimeInterval length;
@property (nonatomic, copy) NSString *director;
// nullable metadata:
@property (nonatomic, copy, nullable) NSArray<Movie *> *related;
@property (nonatomic, copy, nullable) NSString *plotSummary;
@property (nonatomic, copy, nullable) NSArray<NSString *> *keywords;
@property (nonatomic, copy, nullable) NSArray<NSString *> *genres;
@property (nonatomic, copy, nullable) NSArray<NSString *> *awards;
@property (nonatomic, copy, nullable) NSArray<NSString *> *altTitles;
@property (nonatomic, copy, nullable) NSArray<NSString *> *reviews;
// etc...
@end
This class struct could get relatively large, perhaps a kilobyte or more for a single instance, which doesn’t even include the space needed for the strings and arrays themselves. Again, this is not a big deal unless you are dealing with thousands of Movie
objects at once, but it’s a lot of wasted memory if most of those fields are usually nil
.
Dictionary storage
Instead of a fixed-size backing struct, what if we used an NSDictionary
for storage instead? Dictionaries are dynamically-sized, so will automatically expand to fit in as many objects as needed.
A movie where most of the properties are nil
could take up much less memory, because those missing properties just won’t be stored at all. The data for a valid Movie
object could be only:
@{
@"title": @"My Great Movie",
@"year": @2020,
@"length", @7200,
@"director": @"Jane Doe",
}
While we reduce the storage space needed, by storing everything in a dictionary we lose the type-safety and convenience of properties. But there is a way to get the best of both.
How could we convert our Movie
class with properties to use dictionary storage instead of a backing struct? By providing custom getters and setters for each property, Xcode will no longer auto-synthesize these properties and therefore no longer reserve space in the struct for them.
An implementation of our class could look something like this:
@interface Movie ()
// internal storage for properties
@property (nonatomic) NSMutableDictionary *storage;
@end
@implementation Movie
- (instancetype)init {
self = [super init];
if (self) {
_storage = [NSMutableDictionary dictionary];
}
return self;
}
- (NSString *)title {
return self.storage[@"title"];
}
- (void)setTitle:(NSString *)title {
self.storage[@"title"] = title;
}
- (int)year {
return [self.storage[@"year"] intValue];
}
- (void)setYear:(int)year {
self.storage[@"year"] = @(year);
}
// ...
@end
For a class with a lot of properties, creating a getter and setter for each one would get tedious very quickly, not to mention error-prone.
If only there were a way to automatically generate all those methods... 🤔
Onto the fun stuff!
Dynamic method resolution
Remember that in Objective-C we’re usually not calling functions, but sending messages. Calling [self doSomething]
sends a message (a selector called doSomething
) to an object (called self
).
The difference with message sending is that the linking between messages and methods isn’t hardcoded into the app, but can be modified while the app is running. If the runtime can’t find a method to respond to a message, it won’t fail immediately but will give us a chance to respond to the message dynamically.
The important runtime calls we’ll need to respond to unknown messages are:
+resolveInstanceMethod
. This is a method onNSObject
called by the runtime when it can’t find a method for a given selector. If we implement this and determine that we can handle the particular selector dynamically, we should add the method and returnYES
.class_addMethod
. This provides an implementation for a selector on a specific class. The method only needs to be added once, and then it can be called just like any normal method which was compiled into the app!imp_implementationWithBlock
. The implementation of a method (IMP
) is usually just a C function which takes at least two parameters,self
and the selector being called (_cmd
). For us it’s more convenient to provide a block for the implementation instead, and this function will turn a block into anIMP
for us.
You can read more about IMP
and method implementations in the earlier article on Implementing Key-Value Coding.
Making properties dynamic
To make use of dynamic method resolution, the first thing we need to do is mark all of our properties as dynamic. Just beneath @implementation Movie
, we put:
@dynamic title, year, length, director; // add all the other props too
The @dynamic
keyword tells Objective-C “don’t create an ivar for this property, and don’t create any getter or setter, I’ll provide them at runtime”. All it will do for you is create the property and the getter and setter selectors but not the actual implementations.
If you tried to access these dynamic properties at this point, you’d get a crash with the familiar error:
-[Movie setTitle:]: unrecognized selector sent to instance
So we have to make sure that those selectors get handled! 😄
Resolving methods
We’ll need to implement the +resolveInstanceMethod
method on our Movie
class. As mentioned, this method takes the selector which Obj-C can’t find a method for.
Since we’re dealing with properties, the selector would only be either a getter or setter. For example, title
and setTitle:
. So the first thing we’ll need to do is figure out which property the selector refers to. For this, we’ll be using the ClassProperty
wrapper from the earlier article on Inspecting Properties.
Let’s add another method to our Movie
class to find the matching property:
+ (nullable ClassProperty *)propertyForSelector:(SEL)sel {
for (ClassProperty *prop in self.classProperties) {
if (sel == prop.getter || sel == prop.setter) {
return prop;
}
}
return nil;
}
Now we can go ahead and implement resolveInstanceMethod
itself:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
ClassProperty *prop = [self propertyForSelector:sel];
if (prop == nil) {
NSLog(@"Couldn't resolve method for selector: %@",
NSStringFromSelector(sel));
return NO;
}
This first part looks for the matching property and exits early if it can’t be found. A return value of NO
indicates to the runtime that the method could not be resolved.
Let’s first handle the case when we’re resolving the getter of the property. We’ll also only add support for NSString
values for now:
if (sel == prop.getter) {
id block = ^(Movie *self) {
NSString *val = self.storage[prop.name];
return val;
};
The implementation block simply reads the value from the backing storage and returns it. We’ll assume it’s the correct type since no-one else can access our storage dictionary.
Before we call class_addMethod
, we’ll need to figure out the type encoding of the method. For a getter, this is the encoding of the return type plus an object type (self
) and selector (_cmd
).
Then we just turn the block into an IMP
and pass it to class_addMethod
:
NSString *typeEncoding
= [prop.type.encoding stringByAppendingString:@"@:"];
IMP imp = imp_implementationWithBlock(block);
return class_addMethod(self, sel, imp, typeEncoding.UTF8String);
}
The case for the setter method looks very similar. The main differences are that the implementation block takes a new value parameter, and the type encoding is now void (the return type), then class, selector and the value type:
if (sel == prop.setter) {
id block = ^(Movie *self, NSString *val) {
self.storage[prop.name] = val;
};
NSString *typeEncoding
= [@"v@:" stringByAppendingString:prop.type.encoding];
IMP imp = imp_implementationWithBlock(block);
return class_addMethod(self, sel, imp, typeEncoding.UTF8String);
}
Supporting properties of any type
From the earlier article on Implementing Key-Value Coding, we saw that properties need to be handled separately for each type. As in that situation, the easiest way to do this is by using a macro to generate the code for most cases.
All of the scalar types will have to be boxed into an NSValue
so they can be placed in the storage dictionary and unboxed when we read them out.
Let’s add some macros that will create getter and setter blocks to box and unbox scalar values for us:
#define GETTER_BLOCK(UNBOX_METHOD) \
(id)^(Movie *self) { \
id val = self.storage[prop.name]; \
return [val UNBOX_METHOD]; \
}
#define SETTER_BLOCK(BOXABLE_TYPE) \
(id)^(Movie *self, BOXABLE_TYPE val) { \
self.storage[prop.name] = @(val); \
}
#define ACCESSOR_BLOCK(GETTER, TYPE, UNBOX) \
GETTER? GETTER_BLOCK(UNBOX) : SETTER_BLOCK(TYPE)
We would call these for int
properties like GETTER_BLOCK(intValue)
and SETTER_BLOCK(int)
, or just use the combined convenience macro: ACCESSOR_BLOCK(isGetter, int, intValue)
.
Since this will be a big switch statement to handle all the cases, we can pull that into a separate method like so:
+ (nullable id)blockForAccessor:(ClassProperty *)prop isGetter:(BOOL)isGetter {
switch (prop.type.type) {
case TypeChar:
return ACCESSOR_BLOCK(isGetter, char, charValue);
case TypeInt:
return ACCESSOR_BLOCK(isGetter, int, intValue);
case TypeShort:
return ACCESSOR_BLOCK(isGetter, short, shortValue);
case TypeLong:
return ACCESSOR_BLOCK(isGetter, long, longValue);
case TypeLongLong:
return ACCESSOR_BLOCK(isGetter, long long, longLongValue);
case TypeUnsignedChar:
return ACCESSOR_BLOCK(isGetter, unsigned char, unsignedCharValue);
case TypeUnsignedInt:
return ACCESSOR_BLOCK(isGetter, unsigned int, unsignedIntValue);
case TypeUnsignedShort:
return ACCESSOR_BLOCK(isGetter, unsigned short, unsignedShortValue);
case TypeUnsignedLong:
return ACCESSOR_BLOCK(isGetter, unsigned long, unsignedLongValue);
case TypeUnsignedLongLong:
return ACCESSOR_BLOCK(isGetter, unsigned long long, unsignedLongLongValue);
case TypeFloat:
return ACCESSOR_BLOCK(isGetter, float, floatValue);
case TypeDouble:
return ACCESSOR_BLOCK(isGetter, double, doubleValue);
case TypeBool:
return ACCESSOR_BLOCK(isGetter, BOOL, boolValue);
case TypeObject:
case TypeClass:
return isGetter? GETTER_BLOCK(self) : (id)^(Movie *self, id val) {
self.storage[prop.name] = val;
};
case TypeCString:
return ACCESSOR_BLOCK(isGetter, const char *, UTF8String);
case TypeSelector:
return isGetter?
(id)^(Movie *self) {
id val = self.storage[prop.name];
return val? NSSelectorFromString(val) : nil;
}
: (id)^(Movie *self, SEL val) {
self.storage[prop.name] = val? NSStringFromSelector(val) : nil;
};
case TypeVoid:
case TypeArray:
case TypeStruct:
case TypeUnion:
case TypeBitField:
case TypePointer:
case TypeUnknown:
default:
return nil;
}
}
Each type is handled in a manner quite similar to that in the KVC article:
- For scalars we just need to pass the correct type and unboxing method.
- Objects and
Class
types are just put straight into the dictionary as-is. We’re useself
as the “unboxing method” for objects here, since that just returns the object itself. - Both C strings and selectors can be boxed into an
NSString
- We’ll skip all the uncommon types that aren’t supported by properties anyway.
Struct properties
The main difference here is the support of structs.
When implementing KVC, we could handle structs in a special way because the method implementations already existed, we just had to figure out how to call them. But here we’re creating the implementations, and it’s not trivial to create a function that can accept a struct parameter of any size.
It would be possible to support specific struct types (like CGSize
or CGRect
) but not in a generic way for all structs. So for now we’ll leave them out, since it would depend on your actual needs to how these would be handled.
Putting it all together
To finish up, let’s rewrite resolveInstanceMethod
to use our new blockForAccessor
method:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
ClassProperty *prop = [self propertyForSelector:sel];
if (prop == nil) {
NSLog(@"Couldn't resolve method for selector %@",
NSStringFromSelector(sel));
return NO;
}
BOOL isGetter = (sel == prop.getter);
id block = [self blockForAccessor:prop isGetter:isGetter];
if (block == nil) {
NSLog(@"Couldn't implement method for selector %@",
NSStringFromSelector(sel));
return NO;
}
NSString *typeEncoding = isGetter
? [prop.type.encoding stringByAppendingString:@"@:"]
: [@"v@:" stringByAppendingString:prop.type.encoding];
IMP imp = imp_implementationWithBlock(block);
return class_addMethod(self, sel, imp, typeEncoding.UTF8String);
}
Wrapping up
In this article we’ve covered some core functionality of the Objective-C runtime: resolving method implementations dynamically. This is a really useful technique for creating many accessors or methods in a generic way without a lot of repetitive code.
To make our dictionary-backed object more useful, we could extract it into a superclass which any object could inherit from. I’ve done this in the sample code for this article, creating a DictionaryBackedObject
class.
The source code from this post can be found on Github. If you spot a bug, please create an issue on there.
Any comments or questions about this post? ✉️ nick @ this domain.
— Nick Randall