Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best time to invalidate NSTimer inside UIViewController to avoid retain cycle

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?

like image 471
MyCSharpCorner Avatar asked Aug 13 '10 15:08

MyCSharpCorner


5 Answers

  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.

like image 166
Jeremy W. Sherman Avatar answered Nov 08 '22 15:11

Jeremy W. Sherman


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];
like image 23
Petrakeas Avatar answered Nov 08 '22 13:11

Petrakeas


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

More here

like image 3
vodkhang Avatar answered Nov 08 '22 14:11

vodkhang


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

Timer.scheduledTimer(
    withTimeInterval: 1,
    repeats: true,
    block: { [weak self] _ in
        self?.updateStatus()
    }
)
like image 2
Durdu Avatar answered Nov 08 '22 13:11

Durdu


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

like image 1
Josh Lindsey Avatar answered Nov 08 '22 13:11

Josh Lindsey