Delaying deserialization with Mantle until it's needed
21 Sep 2015I’m using Github’s Mantle in a small side project where I recently started running into performance bottlenecks with it. I have JSON that looks roughly like this:
[{"id": 1,
"title": "My Thing",
"widgets": [...]},
{"id": 2,
"title": "Another Thing",
"widgets": [...]},
{"id": 3,
"title": "Third Thing",
"widgets": [...]},
...etc.
A bunch of Things, which each has an array of Widgets. I’ve been working with a fairly large data set recently: 70ish things, and a grand total of 1,341 widgets across all of the things at the moment. On older devices (iPhone 4, iPad 2) deserializing all of that at once was unreasonably slow: I could easily see 5-10 second times for it.
Profiling showed that most of the time was spent in Mantle. I really didn’t want to have to drop it: i’s straightforward to write the deserialization code myself, but there are a lot of types that I’d need to do it for, and this particular project I only get to spend a few hours a week on.
Fortunately, I came up with a good workaround: I don’t need that widgets array until it’s time to render a Thing, and there’s only ever one Thing on the screen at a time. I can just delay deserializing the Widgets until rendering time.
How do we do that with Mantle?
First, the class interface looks like this:
@interface Thing : MTLModel<MTLJSONSerializing>
@property (readonly, assign, nonatomic) uint64_t thingId;
@property (readonly, copy , nonatomic) NSString *title;
@property (readonly, strong, nonatomic) NSArray *widgets;
@end
And in the implementation file, I tell Mantle what class that widgets array is supposed to be:
@implementation Thing
+ (NSValueTransformer *)widgetJSONTransformer
{
return [NSValueTransformer mtl_JSONArrayTransformerWithModelClass:Widget.class];
}
@end
What I can do instead is remove the +widgetJSONTransformer
method so Mantle no longer knows
how to deserialize that field: that results in Mantle just setting widgets
to the raw NSArray
of NSDictionaries that it was handed in the first place. Then, add a new readonly field that
deserializes those widgets on demand.
In the header file:
@interface Thing : MTLModel<MTLJSONSerializing>
@property (readonly, assign, nonatomic) uint64_t thingId;
@property (readonly, copy , nonatomic) NSString *title;
@property (readonly, strong, nonatomic) NSArray *widgets;
// Deserializes the widgets array on demand
@property (readonly, strong, nonatomic) NSArray *parsedWidgets;
@end
And in the implementation:
@interface Thing ()
@property (readwrite, strong, nonatomic) NSArray *parsedWidgets;
@end
@implementation Thing
- (NSArray *)parsedWidgets
{
if (_parsedWidgets == nil) {
NSError *error;
_parsedWidgets = [MTLJSONAdapter modelsOfClass:Widget.class fromJSONArray:self.widgets error:&error];
if (error != nil) {
NSLog(@"Thing -parsedWidgets: An error occurred while decoding widgets: %@", error.localizedDescription);
_parsedWidgets = @[];
}
}
return _parsedWidgets;
}
@end
With that, we’re good to go: I just replace references to -widgets
with -parsedWidgets
in the rendering code.
Due to when I’m calling -parsedWidgets
the deserializing ends up happening on
the main thread, so I might need to get a bit fancier in the future. For now things are
sped up dramatically without having to write a bunch of accessor code, so I’m happy.