33

Does any one know when is the best time to stop an NSTimer that is held reference inside of a UIViewController to avoid retain cycle between the timer and the controller?

Here is the question in more details: I have an NSTimer inside of a UIViewController.

During ViewDidLoad of the view controller, I start the timer:

statusTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: self selector: @selector(updateStatus) userInfo: nil repeats: YES];

The above causes the timer to hold a reference to the view controller.

Now I want to release my controller (parent controller releases it for example)

the question is: where can I put the call to [statusTimer invalidate] to force the timer to release the reference to the controller?

I tried putting it in ViewDidUnload, but that does not get fired until the view receives a memory warning, so not a good place. I tried dealloc, but dealloc will never get called as long as the timer is alive (chicken & egg problem).

Any good suggestions?

MyCSharpCorner
  • 1,313
  • 11
  • 15
  • There is no need to retain the view controller in the timer. The VC should own the timer, like any other object it would own, and destroy it when appropriate. – logancautrell Oct 21 '10 at 20:48
  • @logancautrell, you have a good point, but `NSTimer` retains the target you pass to it and that cannot be changed. (Some of the answers suggest ways to try and work around that though.) – Jon-Eric Oct 22 '10 at 14:56
  • Yikes, I see the problem you are running into. An alternative is to add a helper object that both your VC and the timer own. – logancautrell Oct 22 '10 at 15:15

10 Answers10

21
  1. You could avoid the retain cycle to begin with by, e.g., aiming the timer at a StatusUpdate object that holds a non-retained (weak) reference to your controller, or by having a StatusUpdater that is initialized with a pointer your controller, holds a weak reference to that, and sets up the timer for you.

    • You could have the view stop the timer in -willMoveToWindow: when the target window is nil (which should handle the counterexample to -viewDidDisappear: that you provided) as well as in -viewDidDisappear:. This does mean your view is reaching back into your controller; you could avoid reaching in to grab the timer by just send the controller a -view:willMoveToWindow: message or by posting a notification, if you care.

    • Presumably, you're the one causing the view to be removed from the window, so you could add a line to stop the timer alongside the line that evicts the view.

    • You could use a non-repeating timer. It will invalidate as soon as it fires. You can then test in the callback whether a new non-repeating timer should be created, and, if so, create it. The unwanted retain cycle will then only keep the timer and controller pair around till the next fire date. With a 1 second fire date, you wouldn't have much to worry about.

Every suggestion but the first is a way to live with the retain cycle and break it at the appropriate time. The first suggestion actually avoids the retain cycle.

Jeremy W. Sherman
  • 35,901
  • 5
  • 77
  • 111
  • I prefer using a non-repeating timer and recreating in the callback. – David James Sep 16 '14 at 16:05
  • I think using `viewWillMovetoWindow:` is the best solution when using a custom view class that has the timer, and when the view controller (VC) using it doesn't hold a reference to that view class. Consider the case where dynamic views get initialized in a VC; the VC has no explicit way to tell the view class to invalidate its timer when `viewWillDisappear:` gets called. It could iterate through its views and create a `cleanup` method on its view class or use an `NSNotification`, but I think `willMoveToWindow:` is the cleanest. Don't think the non-repeating timer suggestion would work here. – Evan R Feb 10 '16 at 22:19
  • @IulianOnofrei As OP points out, there's a chicken and egg problem: While a timer is retaining its target, its target will not dealloc. If its target expects to invalidate the timer retaining it in dealloc, then it will never invalidate it, because the timer is keeping it from deallocing! If it's still unclear, I recommend asking a new, full question. – Jeremy W. Sherman Aug 10 '16 at 13:17
  • @JeremyW.Sherman, But it doesn't look like this is the case, since I use `dealloc` in `UIViewController`s to deallocate timers and it worked ok. Isn't in fact your overwritten `dealloc` called prior to any `NSObject` deallocation? What is `dealloc` even used for, then? If not manually deallocing the respective object's properties? – Iulian Onofrei Aug 10 '16 at 13:38
  • @IulianOnofrei Please [ask a question](https://stackoverflow.com/questions/ask) with some example source code to make the discussion concrete. The short answer is "because retain (aka strong reference) cycles in the common case where the timer's target is also its owner". – Jeremy W. Sherman Aug 10 '16 at 13:43
  • @JeremyW.Sherman, Did it [here](https://stackoverflow.com/questions/40358646/invalidate-nstimer-in-dealloc). – Iulian Onofrei Nov 01 '16 at 10:54
6

One way around it is to make the NStimer hold a weak reference to your UIViewController. I created a class that holds a weak reference to your object and forwards the calls to that:

#import <Foundation/Foundation.h>

@interface WeakRefClass : NSObject

+ (id) getWeakReferenceOf: (id) source;

- (void)forwardInvocation:(NSInvocation *)anInvocation;

@property(nonatomic,assign) id source;

@end

@implementation WeakRefClass

@synthesize source;

- (id)init{
    self = [super init];
//    if (self) {
//    }
    return self;
}

+ (id) getWeakReferenceOf: (id) _source{

    WeakRefClass* ref = [[WeakRefClass alloc]init];
    ref.source = _source; //hold weak reference to original class

    return [ref autorelease];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [[self.source class ] instanceMethodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [anInvocation    invokeWithTarget:self.source ];

}

@end

and you use it like this:

statusTimer = [NSTimer scheduledTimerWithTimeInterval: 1 target: [WeakRefClass getWeakReferenceOf:self] selector: @selector(updateStatus) userInfo: nil repeats: YES];

Your dealloc method gets called (unlike before) and inside it you just call:

[statusTimer invalidate];
Petrakeas
  • 1,521
  • 15
  • 28
3

You can try with - (void)viewDidDisappear:(BOOL)animated and then you should validate it again in - (void)viewDidAppear:(BOOL)animated

More here

vodkhang
  • 18,639
  • 11
  • 76
  • 110
  • when inside a UINavigationController, viewDidDisappear does not gets called, so I'm looking for a more general place to put it, also when I hide the view using [self.view removeFromSuperview], viewDidDisappear does not get called either. – MyCSharpCorner Aug 13 '10 at 15:54
  • what do you mean by inside a UINavigationController. when you pop it out? Can you post some more code about that – vodkhang Aug 13 '10 at 16:16
2

For @available(iOS 10.0, *) you could also use:

Timer.scheduledTimer(
    withTimeInterval: 1,
    repeats: true,
    block: { [weak self] _ in
        self?.updateStatus()
    }
)
Durdu
  • 4,649
  • 2
  • 27
  • 47
1

invalidate timer inside - (void)viewWillDisappear:(BOOL)animated did work for me

Michal Gumny
  • 1,770
  • 1
  • 16
  • 24
1

The -viewDidDisappear method may be what you're looking for. It's called whenever the view is hidden or dismissed.

Josh Lindsey
  • 8,455
  • 3
  • 24
  • 25
  • But viewDidDisappear never gets called when I hide my parent view using: [self.view removeFromSuperview]. Any other suggestion? – MyCSharpCorner Aug 13 '10 at 15:45
  • In addition, when inside a UINavigationController, viewDidDisappear does not gets called, so I'm looking for a more general place to put it. – MyCSharpCorner Aug 13 '10 at 15:47
  • viewDidDisappear would be perfect if one could rely on it always being called, unfortunately this is not the case – Bradley Thomas Oct 15 '14 at 14:33
0

If the timer.REPEAT is set to YES, the owner of the timer (e.g. view controller or view) will not be deallocated until the timer is invalidated.

The solution to this question is to find some trigger point to stop your timer.

For example, I start a timer to play animated GIF images in a view, and the trigger point would be:

  1. when the view is added to the superview, start the timer
  2. when the view is removed from the superview, stop the timer

so I choose the UIView's willMoveToWindow: method as such:

- (void)willMoveToWindow:(UIWindow *)newWindow {
    if (self.animatedImages && newWindow) {
        _animationTimer = [NSTimer scheduledTimerWithTimeInterval:_animationInterval
            target:self selector:@selector(drawAnimationImages)
            userInfo:nil repeats:YES];
    } else {
        [_animationTimer invalidate];
        _animationTimer = nil;
    }
}

If your timer is owned by a ViewController, maybe viewWillAppear: and viewWillDisappear: are a good place for you to start and stop the timer.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
flypig
  • 1,260
  • 1
  • 18
  • 23
0

I wrote a "weak reference" class for exactly this reason. It subclasses NSObject, but forwards all methods that NSObject doesn't support to a target object. The timer retains the weakref, but the weakref doesn't retain its target, so there's no retain cycle.

The target calls [weakref clear] and [timer invalidate] or so in dealloc. Icky, isn't it?

(The next obvious thing is to write your own timer class that handles all of this for you.)

tc.
  • 33,468
  • 5
  • 78
  • 96
-2

I had exactly the same issue and in the end I decided to override the release method of the View Controller to look for the special case of the retainCount being 2 and my timer running. If the timer wasn't running then this would have caused the release count to drop to zero and then call dealloc.

- (oneway void) release {
    // Check for special case where the only retain is from the timer
    if (bTimerRunning && [self retainCount] == 2) {
        bTimerRunning = NO;
        [gameLoopTimer invalidate];
    }
    [super release];
}

I prefer this approach because it keeps it simple and encapsulated within the one object, i.e., the View Controller and therefore easier to debug. I don't like, however, mucking about with the retain/release chain but I cannot find a way around this.

Hope this helps and if you do find a better approach would love to hear it too.

Dave

EDIT: Should have been -(oneway void)

Magic Bullet Dave
  • 9,006
  • 10
  • 51
  • 81
-3

You can write this code in dealloc function of view controller

for eg.

-(void)dealloc
{
   if([statusTimer isValid])
  {
       [statusTimer inValidate];
       [statustimer release];
      statusTimer = nil;
  }
}

this way the reference counter of statustimer will automatically decrement by 1 & also the data on the allocated memory will also erase

also you can write this code in - (void)viewDidDisappear:(BOOL)animated function

Jon-Eric
  • 16,977
  • 9
  • 65
  • 97
Tarun Mittal
  • 103
  • 3
  • 3
    -1 The problem is that `dealloc` will never be called because the `NSTimer` is keeping the `UIViewController` alive. – Jon-Eric Oct 21 '10 at 17:20