47

What do people mean when they say this is expensive? I create instances of a lot of transient objects just for intermediate storage (NSString and NSDate are typical ones). How do I know if my program use of NSDateFormatter is overdoing it?

Until now, I have tended to create what amounts to a singleton, but my preference would be to encapsulate it into some of the other objects it is associated with so I can use self references.

Short of running performance tests, I am looking for a better "rule-of-thumb" understanding of why I should or shouldn't do this.

Draco
  • 1,003
  • 1
  • 10
  • 14
  • You can profile your app to determine which way suits your needs better. I reckon the developers wanted to point out that something as innocuous-sounding as `NSDateFormatter` may have some pretty complicated initializer code so here's a hint in the documentation to reuse the initialized object as much as possible. – Tim Reddy Feb 06 '12 at 15:19

2 Answers2

40

When something like this is referred to as expensive, it doesn't necessarily mean that you should never do it, it just means avoid doing it in situations when you need to get out of a method as quickly as possible. For example, back when the iPhone 3G was the latest device, I was writing an application with a UITableView that formatted numbers for display in each cell (I might add, this was back when I was a beginner at iOS development). My first attempt was the following:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *reuseIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];

    MyManagedObject *managedObject = [self.managedObjects objectAtIndex:indexPath.row];
    NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
    [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

    [cell.textLabel setText:[managedObject title]];
    [cell.detailTextLabel setText:[numberFormatter stringFromNumber:[managedObject amount]]];

    return cell;
}

The scrolling performance of this code was terrible. The frame rate dropped to about 15 FPS because I was allocating a new NSNumberFormatter every time tableView:cellForRowAtIndexPath: was hit.

I fixed it by changing the code to this:

- (NSNumberFormatter *)numberFormatter {

    if (_numberFormatter != nil) {
        return _numberFormatter;
    }

    _numberFormatter = [[NSNumberFormatter alloc] init];
    [_numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];

    return _numberFormatter;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    NSString *reuseIdentifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuseIdentifier forIndexPath:indexPath];

    MyManagedObject *managedObject = [self.managedObjects objectAtIndex:indexPath.row];
    NSNumberFormatter *numberFormatter = [self numberFormatter];

    [cell.textLabel setText:[managedObject title]];
    [cell.detailTextLabel setText:[numberFormatter stringFromNumber:[managedObject amount]]];

    return cell;
}

The difference here is that I've lazily loaded the NSNumberFormatter into an ivar, so that each run of tableView:cellForRowAtIndexPath: no longer allocates a new instance. This simple change pushed the scrolling performance back up to about 60 FPS.

This specific example isn't quite as relevant anymore, as the newer chips are capable of handling the allocation without affecting scrolling performance, but it's always better to be as efficient as possible.

Ell Neal
  • 6,014
  • 2
  • 29
  • 54
  • 1
    The danger with this is that you have to make sure your numberFormatter isn't called from more than one thread. It doesn't support concurrency. – Greg Maletic Sep 13 '13 at 22:13
  • 1
    @GregMaletic this is a very simple example, ````tableView:cellForRowAtIndexPath:```` will never be called from anything but the main thread. – Ell Neal Sep 14 '13 at 22:12
  • 1
    @Eli True, but if your numberFormatter method is called in another method in your code, and that method was invoked on another thread, you could screw yourself. You just have to remember to only call your numberFormatter method from the main thread. – Greg Maletic Sep 15 '13 at 02:04
  • Awesome, didn't know this but it was brought up in an interview when I did basically the similar thing you did in cellForRow. Now I know what to tell them when asked a better approach! – romero-ios Dec 14 '17 at 13:35
8

I had this same question sometime ago. I've ran Instruments on some app I was working and I figure out the previous developers were creating a new NSDateFormatter for each custom log they did. Since for each screen they used to log about 3 lines. The app used to spend about one sec only creating NSDateFormatters.

The simple solution would be retain the date formatter instance in your class as an attribute or something and reuse it for each log line.

After some trivial thinking, I've came with a "factory" to handle the reuse of NSDateFormatters based on format and locale wanted. I request a date formatter of some format and locale and my class give the already loaded formatter to me. Good performance tuning, you should try.

PS: Maybe someone would like to test it, so I made it public: https://github.com/DougFischer/DFDateFormatterFactory/blob/master/README.md

Douglas Fischer
  • 535
  • 4
  • 7
  • 1
    Did you get any feedback? – Dejell Sep 17 '13 at 16:29
  • 1
    I agree. Easy and convenient. My current app has got a lot of benefit from using it. Thanks a lot – rmvz3 Jul 29 '14 at 02:47
  • 1
    Initially, I worried about thread safety, but it turns out that information is old: https://developer.apple.com/reference/foundation/dateformatter On iOS 7 and later NSDateFormatter is thread safe. – Pat Nov 15 '16 at 21:24