Delaying deserialization with Mantle until it's needed

21 Sep 2015

I’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.