3

This bug has been in my code for days - I've been trying to solve it but to no avail. I've done extensive debugging, on simulator and on a device, logging, Crashlytics throwing up the crash log, and it has to do with my NSTimer being sent a message after deallocation. I'll explain further.

This NSTimer controls my UIToolbar label on some view controllers, and it keeps updating the label to show the relative time from when the user refreshed the UITableView. Upon load, and with every refresh, it shows Updated just now and then if the user doesn't refresh/go to another VC, it keeps updating every 60 seconds. So, after a minute, it would show Updated 1 minute ago. The NSTimer triggers a method that does the UILabel updating. On user refresh (by pulling on the refresh control), this method is invoked manually inside my getAllItems (that's why I pass nil).

My NSTimer is defined in my superclass as:

@property (nonatomic, weak) NSTimer *updateDateTimer;

This is the code that get the items into my tableview:

- (void)getAllItems
{
    KILL_TIMER(self.updateDateTimer);
    self.updateLabel = [Formatters boldLabelWithLabel:self.updateLabel
                                             withText:@"Checking for items..."
                                             withFont:[UIFont fontWithName:kOpenSansBold size:15.0f]];

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [self fetchObjectsWithRequest:self.request];
        sleep(1);
        dispatch_async(dispatch_get_main_queue(), ^{
                [self.refreshControl performSelector:@selector(endRefreshing) withObject:nil afterDelay:0];

            self.updatedDate = [NSDate date];
            [self updateTimer:nil];
            self.updateDateTimer = [NSTimer scheduledTimerWithTimeInterval:kToolbarUpdateLabelInterval
                                                                    target:self
                                                                  selector:@selector(updateTimer:)
                                                                  userInfo:nil repeats:YES];
        });
    });
}

and KILL_TIMER(q) is defined, in my superclass, as:

#define KILL_TIMER(q) if (q) {DDLogVerbose(@"KILLING TIMER: %@", q); [q invalidate]; q=nil;}

This define is invoked in 2 methods:

a) in my getAllItems method: so when I (re)load items (by invoking that method in viewDidLoad or by user refreshing the table view), I always try to kill the current timer that's active, show the 'checking for items', fetch the items, show 'updated just now' and then schedule the timer to execute every 60 seconds.

b) on each viewWillDisappear`, as such:

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    KILL_TIMER(self.updateDateTimer);
}

However, if I hit the home button, and then reopen the app, it generally (not always, only if I had switched view controllers) crashes with the following message (with NSZombies enabled):

2013-07-11 17:21:37.576 App[3923:907] *** -[ItemCDTVC setUpdateDateTimer:]: message sent to deallocated instance 0x20bc49f0

I do have an NSNotification that invokes my getAllItems:

// Notification received when user returns to app (refreshes the table view)
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(getAllItems) name:UIApplicationWillEnterForegroundNotification object:nil];

This only happens after I switch view controllers using the menu, leave the app, and reopen (the app does not terminate). It does not crash if the user stays in the app, regardless of how many view controllers they switch between.

It's a pretty annoying bug, because although it's very easily reproduce, I don't know how to fix it. Any help is appreciated.

stevekohls
  • 2,214
  • 23
  • 29
swiftcode
  • 3,039
  • 9
  • 39
  • 64

2 Answers2

4

You should be invalidating your timers when your app becomes inactive. -applicationWillResignActive: is a good place to do that. You can create new timers when the app becomes active again.

An educated guess is that your app is crashing because the timers get killed automatically when the app goes inactive, and then you crash when your app returns to the foreground and tries to access the timers, which are no longer valid.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • That was it, I added a `NSNotification` for `UIApplicationWillResignActiveNotification` and handled it by killing the timer before it resigned active. When it comes back, I recreate the timer in `viewWillAppear`. Thanks! – swiftcode Jul 11 '13 at 20:59
2

Once you call invalidate on a timer, you can't use it again - How to "validate" a NSTimer after invalidating it?

If I want to have a timer that I can "switch on / off" at will, I use a BOOL useTimer or something of the sort that decides if the timer should fire or not, for a repeating timer.

For a non-repeating timer, I create a new one.

Community
  • 1
  • 1
Sid
  • 9,508
  • 5
  • 39
  • 60