58

I'm using an NSTimer like this:

timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];

Of course, NSTimer retains the target which creates a retain cycle. Furthermore, self isn't a UIViewController so I don't have anything like viewDidUnload where I can invalidate the timer to break the cycle. So I'm wondering if I could use a weak reference instead:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

I've heard that the timer must be invalidated (i guess to release it from the run loop). But we could do that in our dealloc, right?

- (void) dealloc {
    [timer invalidate];
}

Is this a viable option? I've seen a lot of ways that people deal with this issue, but I haven't seen this.

bendytree
  • 13,095
  • 11
  • 75
  • 91
  • 26
    Beyond the answers below, no one explained why invalidating the timer in dealloc is useless (from [here](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Timers/Articles/usingTimers.html#//apple_ref/doc/uid/20000807-CJBJCBDE)): A timer maintains a strong reference to its target. This means that as long as a timer remains valid, its target will not be deallocated. As a corollary, this means that it does not make sense for a timer’s target to try to invalidate the timer in its dealloc method—the dealloc method will not be invoked as long as the timer is valid. – Guy Nov 12 '14 at 07:26

10 Answers10

77

The proposed code:

__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

has the effect that (i) a weak reference is made to self; (ii) that weak reference is read in order to provide a pointer to NSTimer. It won't have the effect of creating an NSTimer with a weak reference. The only difference between that code and using a __strong reference is that if self is deallocated in between the two lines given then you'll pass nil to the timer.

The best thing you can do is create a proxy object. Something like:

[...]
@implementation BTWeakTimerTarget
{
    __weak target;
    SEL selector;
}

[...]

- (void)timerDidFire:(NSTimer *)timer
{
    if(target)
    {
        [target performSelector:selector withObject:timer];
    }
    else
    {
        [timer invalidate];
    }
}
@end

Then you'd do something like:

BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

Or even add a class method to BTWeakTimerTarget of the form +scheduledTimerWithTimeInterval:target:selector:... to create a neater form of that code. You'll probably want to expose the real NSTimer so that you can invalidate it, otherwise the rules established will be:

  1. the real target isn't retained by the timer;
  2. the timer will fire once after the real target has begun (and probably completed) deallocation, but that firing will be ignored and the timer invalidated then.
Tommy
  • 99,986
  • 12
  • 185
  • 204
  • 3
    Awesome, thanks. I ended up making an `NSWeakTimer` shell that handled the selector wiring https://gist.github.com/bendytree/5674709 – bendytree May 29 '13 at 23:43
  • @bendytree I reviewed your code. The target should not be an `assign` property. Rather, it should be a `weak` property as @Tommy described. Assign properties do not become `nil` after deallocating, whereas weak properties do. Thus your `if (target)` check will never become true. – Pwner Nov 19 '14 at 20:04
  • @Pwner I checked it and changed assign to weak but still it never invalidates it. what should I do? – sftsz Feb 13 '15 at 22:53
  • @sftsz Are you using a runloop to run the timer? – mabounassif Sep 28 '15 at 19:04
  • I just tried @bendytree's solution and I've been scratching my head forever until I found that you're not passing an object to your selector in the "fire" method. My application uses repeating timers and invalidates them in the selector called by "fire" if a certain condition is met. As no object was passed, the timer was never invalidated and ran forever. Whoever is copy/pasting your code should make sure to use "withObject:timer" instead of "withObject:nil". Anyways, thanks for the snippet!! – guitarflow Oct 19 '16 at 15:07
  • Perfect answer, saved a lot of time. – Vishwas Singh Mar 04 '21 at 14:19
32

iOS 10 and macOS 10.12 "Sierra" introduced a new method, +scheduledTimerWithTimeInterval:repeats:block:, so you could capture self weakly simply as:

__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
    MyClass* _Nullable strongSelf = weakSelf;
    [strongSelf doSomething];
}];

Equivalence in Swift 3:

_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
    self?.doSomething()
}

If you still need to target iOS 9 or below (which you should at this moment), this method cannot be used, so you would still need to use code in the other answers.

kennytm
  • 510,854
  • 105
  • 1,084
  • 1,005
  • 1
    This doesnt’ work until timer is invalidated even if self is weak – protspace Jan 17 '18 at 11:11
  • @protspace then you could do something like this or not? `guard let weakSelf = self else { timer.invalidate(); return }`. The timer is going to be invalidated, when self is not available any more. – Baran Sep 24 '18 at 16:26
26

If you are not that concerned about the millisecond accuracy of the timer events, you could use dispatch_after & __weak instead of NSTimer to do this. Here's the code pattern:

- (void) doSomethingRepeatedly
{
    // Do it once
    NSLog(@"doing something …");

    // Repeat it in 2.0 seconds
    __weak typeof(self) weakSelf = self;
    double delayInSeconds = 2.0;
    dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
    dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
        [weakSelf doSomethingRepeatedly];
    });
}

No NSTimer @property, no invalidate/runloop stuff and no proxy object, just a simple clean method.

The downside of this approach is that (unlike NSTimer) the execution time of the block (containing [weakSelf doSomethingRepeatedly];) will impact scheduling of the events.

Rog
  • 17,070
  • 9
  • 50
  • 73
Jay Zhao
  • 946
  • 10
  • 24
  • 1
    Great idea. And if you want it to repeat, you can check if `weakSelf != nil` and if so, simply recursively call `dispatch_after` inside the dispatch block. The only downside is it makes it difficult to alter the timer (for example to change the interval) after it has been set up. – devios1 Aug 13 '14 at 22:54
  • 1
    Be careful when implementing a repeating timer using this method. When you use a regular repeating NSTimer, the execution duration of the target selector won't affect your timing. In this case, if you don't do another dispatch_after immediately when the timer is called (and even so) you will have timing skews. – Guy Nov 12 '14 at 07:24
9

Swift 3

App target < iOS 10:

Custom WeakTimer (GitHubGist) implementation:

final class WeakTimer {

    fileprivate weak var timer: Timer?
    fileprivate weak var target: AnyObject?
    fileprivate let action: (Timer) -> Void

    fileprivate init(timeInterval: TimeInterval,
         target: AnyObject,
         repeats: Bool,
         action: @escaping (Timer) -> Void) {
        self.target = target
        self.action = action
        self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
                                          target: self,
                                          selector: #selector(fire),
                                          userInfo: nil,
                                          repeats: repeats)
    }

    class func scheduledTimer(timeInterval: TimeInterval,
                              target: AnyObject,
                              repeats: Bool,
                              action: @escaping (Timer) -> Void) -> Timer {
        return WeakTimer(timeInterval: timeInterval,
                         target: target,
                         repeats: repeats,
                         action: action).timer!
    }

    @objc fileprivate func fire(timer: Timer) {
        if target != nil {
            action(timer)
        } else {
            timer.invalidate()
        }
    }
}

Usage:

let timer = WeakTimer.scheduledTimer(timeInterval: 2,
                                     target: self,
                                     repeats: true) { [weak self] timer in
                                         // Place your action code here.
}

timer is instance of standard class Timer, so you can use all available methods (e.g. invalidate, fire, isValid, fireDate and etc).
timer instance will be deallocated when self is deallocated or when timer's job is done (e.g. repeats == false).

App target >= iOS 10:
Standard Timer implementation:

open class func scheduledTimer(withTimeInterval interval: TimeInterval, 
                               repeats: Bool, 
                               block: @escaping (Timer) -> Swift.Void) -> Timer

Usage:

let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
    // Place your action code here.
}
Vlad Papko
  • 13,184
  • 4
  • 41
  • 57
  • But haven't you just "passed the buck" on to `WeakTimer`, since `WeakTimer#init` now passes `Timer` a strong reference to `target: self`? – mkirk Jan 19 '17 at 15:32
  • 2
    @mkirk, Yes, but `Timer` is the only guy who keeps strong reference to `WeakTimer`. That means `WeakTimer` instance lives as long as `Timer` lives. `Timer` lives until moment when `fileprivate func fire` is called and `target` already died. `target` has it's own life, nobody in above bucket keeps strong reference to `target`. – Vlad Papko Jan 19 '17 at 22:33
5

In Swift I've defined a WeakTimer helper class:

/// A factory for NSTimer instances that invoke closures, thereby allowing a weak reference to its context.
struct WeakTimerFactory {
  class WeakTimer: NSObject {
    private var timer: NSTimer!
    private let callback: () -> Void

    private init(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) {
      self.callback = callback
      super.init()
      self.timer = NSTimer(timeInterval: timeInterval, target: self, selector: "invokeCallback", userInfo: userInfo, repeats: repeats)
    }

    func invokeCallback() {
      callback()
    }
  }

  /// Returns a new timer that has not yet executed, and is not scheduled for execution.
  static func timerWithTimeInterval(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) -> NSTimer {
    return WeakTimer(timeInterval: timeInterval, userInfo: userInfo, repeats: repeats, callback: callback).timer
  }
}

And then you can use it like such:

let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
  // Your code here...
}

The returned NSTimer has a weak reference to self, so you can call its invalidate method in deinit.

shadowmatter
  • 1,352
  • 2
  • 18
  • 30
  • 1
    You would want to pass `userInfo` into the `NSTimer` constructor, no? – pwightman Feb 09 '15 at 21:48
  • @pwightman Doh, great catch! I've updated the code in my reply. (As well as the code in my own repo... I had always been passing in `nil` for `userInfo`, so I totally missed this.) Thanks! – shadowmatter Feb 10 '15 at 01:58
  • 1
    I don't get how one is suppose to execute the timer, since your block of code don't execute it – thibaut noah Feb 16 '16 at 14:35
  • this leads to the Leak Checks issues (profile in the Instruments). try this https://gist.github.com/onevcat/2d1ceff1c657591eebde – Aleksey Tsyss May 31 '16 at 14:32
  • the weakTimer returned from static method will not retain itself if it doesn't repeat. So it's only work in repeat mode. And, this should add to runloop manually. – leavez Aug 31 '16 at 15:24
3

It doesn't matter that weakSelf is weak, the timer still retains the object so there's still a retain cycle. Since a timer is retained by the run loop, you can (and I suggest to ) hold a weak pointer to the timer:

NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];

About invalidate you're way of doing is correct.

Ramy Al Zuhouri
  • 21,580
  • 26
  • 105
  • 187
  • 9
    But then wouldn't the runloop keep a strong reference to the timer, the timer keep a strong reference to `self` and `dealloc` never occur? – Tommy May 29 '13 at 19:23
  • At least it avoids the retain cycle. I still agree that using a weak timer is better. – Ramy Al Zuhouri May 30 '13 at 15:02
  • 2
    @Tommy is correct, it doesn't avoid the retain cycle. As long as the timer is not invalidated the object will never die and if only dealloc invalidates the timer, it will never be invalidated either. A weak timer is pointless and pretty much never what you want. – Mecki Sep 02 '15 at 12:37
  • The timer could also invoke a weakSelf selector. – Corbin Miller Apr 13 '17 at 21:19
  • @Mecki Apple's Timer programming guide doesn't agree with you: https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Timers/Articles/usingTimers.html – frangulyan Oct 03 '17 at 20:49
  • 2
    @frangulyan No, it does not. It says "_Because the run loop maintains the timer_", which means the runloop keeps the timer alive. It doesn't matter if you keep it alive, it won't die as long as it is scheduled and the official way to unschedule a timer is to invalidate it. It also says "*A timer maintains a strong reference to its target.*", so as long as the timer is alive, it will keep its target alive, too. So making a timer weak fixes nothing, unless you invalidate it at some point. – Mecki Oct 05 '17 at 09:12
0

If you are using Swift here is an auto-cancelling timer:

https://gist.github.com/evgenyneu/516f7dcdb5f2f73d7923

The timer cancels itself automatically on deinit.

var timer: AutoCancellingTimer? // Strong reference

func startTimer() {
  timer = AutoCancellingTimer(interval: 1, repeats: true) {
    print("Timer fired")
  }
}
Evgenii
  • 36,389
  • 27
  • 134
  • 170
0

Swift 4 version. Invalidate must be called before the dealloc.

class TimerProxy {

    var timer: Timer!
    var timerHandler: (() -> Void)?

    init(withInterval interval: TimeInterval, repeats: Bool, timerHandler: (() -> Void)?) {
        self.timerHandler = timerHandler
        timer = Timer.scheduledTimer(timeInterval: interval,
                                     target: self,
                                     selector: #selector(timerDidFire(_:)),
                                     userInfo: nil,
                                     repeats: repeats)
    }

    @objc func timerDidFire(_ timer: Timer) {
        timerHandler?()
    }

    func invalidate() {
        timer.invalidate()
    }
}

Usage

func  startTimer() {
    timerProxy = TimerProxy(withInterval: 10,
                            repeats: false,
                            timerHandler: { [weak self] in
                                self?.fireTimer()
    })
}

@objc func fireTimer() {
    timerProxy?.invalidate()
    timerProxy = nil
}
rockdaswift
  • 9,613
  • 5
  • 40
  • 46
0

With theory and practice.Tommy's solution is not work.

Theoretically,__weak instance is as the parameter,In the implementation of

[NSTimer scheduledTimerWithTimeInterval:target:selector: userInfo: repeats:],

target will be retained still.

You can implement a proxy ,which hold the weak reference and forward selector calling to self , and then pass the proxy as the target. Such as YYWeakProxy.

0

the answer in very simple. for example you can try this:

@interface Person : NSObject
@property(nonatomic, strong) DemoViewController_19 *vc;
@end

@implementation Person
@end

@interface DemoViewController_19 ()
@property(nonatomic, strong) Person *person;
@end

@implementation DemoViewController_19

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.person = [Person new];
    __weak typeof(self) weaks = self;
    self.person.vc = weaks;
}

@end

After run you can see vc dealloc is not called. It depends on Person's property of strong attribute.