What is difference between self.timer = nil vs [self.timer invalidate] in iOS?

生来就可爱ヽ(ⅴ<●) 提交于 2019-11-29 09:49:39

Once you have no need to run timer, invalidate timer object, after that no need to nullify its reference.

This is what Apple documentation says: NSTimer

Once scheduled on a run loop, the timer fires at the specified interval until it is invalidated. A non-repeating timer invalidates itself immediately after it fires. However, for a repeating timer, you must invalidate the timer object yourself by calling its invalidate method. Calling this method requests the removal of the timer from the current run loop; as a result, you should always call the invalidate method from the same thread on which the timer was installed. Invalidating the timer immediately disables it so that it no longer affects the run loop. The run loop then removes the timer (and the strong reference it had to the timer), either just before the invalidate method returns or at some later point. Once invalidated, timer objects cannot be reused.

There is a key difference not mentioned in the other answers.

To test this drop the following code in Playground.

1st Attempt:

import Foundation
import PlaygroundSupport

PlaygroundPage.current.needsIndefiniteExecution = true

class Person{
    var age = 0
    lazy var timer: Timer? = {
        let _timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(fireTimer), userInfo: nil, repeats: true)
        return _timer
    }()

    init(age: Int) {
        self.age = age
    }

    @objc func fireTimer(){
        age += 1
        print("age: \(age)")
    }

    deinit {
        print("person was deallocated")
    }
}

// attempt:
var person : Person? = Person(age: 0)
let _ = person?.timer
person = nil

So let me ask you a question. At the last line of the code I just set person to nil. That means the person object is deallocated and all its properties are set to nil and removed from memory. Right?

An object is deallocated as long as no other object is holding a strong a reference to it. In our case the timer is still holding a strong reference to person, because the run-loop has a strong reference to the timer§ hence the person object will not get deallocated.

The result of the above code is that it still continues to execute! Let's fix it.


2nd Attempt:

Let's set the timer to nil. This should remove the strong reference of timer pointing to person.

var person : Person? = Person(age: 0)
let _ = person?.timer
person?.timer = nil
person = nil

WRONG! We only removed our pointer to the timer. Yet the result of the above code is just like our initial attempt. It still continues to execute...because the run loop is still keeping its pointer to it.


So what do we need to do?

Glad you asked. We must invalidate the timer!

3rd Attempt:

var person : Person? = Person(age: 0)
let _ = person?.timer

person?.timer = nil
person?.timer?.invalidate()
person = nil

This looks better, but it's still wrong. Can you guess why?

I'll give you a hint. See code below 👇.


4th Attempt (correct)

var person : Person? = Person(age: 0)
let _ = person?.timer

person?.timer?.invalidate()
person?.timer = nil
person = nil
// person was deallocated

Our 4th attempt was just like our 3rd attempt, just that the sequence of code was different.

person?.timer?.invalidate() removes the run loop's strong reference and now if a pointer to person is removed...it gets deallocated.!

The attempt below is also correct:

5th Attempt (correct)

var person : Person? = Person(age: 0)
let _ = person?.timer

person?.timer?.invalidate()
person = nil
// person was deallocated

Notice that in our 5th attempt we didn't set the timer to nil. As it's not necessary. Setting it nil is just an indicator that we can use in other lines of code. It helps up so that we can check against it and if it wasn't nil then we'd know the timer is still valid and also to not have a meaningless object around.

After invalidating the timer you should assign nil to the variable otherwise the variable is left pointing to a useless timer. Memory management and ARC have nothing to do with why you should set it to nil. After invalidating the timer, self.timer is now referencing a useless timer. No further attempts should be made to use that value. Setting it to nil ensures that any further attempts to access self.timer will result in nil

from rmaddy's comment above

That being said I think isValid is a more meaningful approach just as isEmpty is more meaningful and efficient than doing array.count == 0...


So why is 3rd attempt not correct?

Because we need a pointer to the timer so we can invalidate it. If we set that pointer to nil then we loose our pointer to it. We lose it while the run-loop has still maintained its pointer to it! So if we ever wanted to turn off the timer we should invalidate it BEFORE we lose our reference to it (ie before we set its pointer to nil) and then it becomes an abandoned memory (not leak).

Conclusion:

  • To get rid of a timer, niling is not necessary. In fact it is harmful if you do it before you invalidate your timer.
  • Calling invalidate will remove the run loop's pointer to it. Only then the object containing the timer will be released.

So how does this apply when I'm actually building an application?

If your viewController has person property and then your popped this viewController off your navigation stack then your viewController will get deallocated. In it's deinit method you must invalidate the person's timer. Otherwise your person instance is kept in memory because of the run loop and its timer action will still want to execute! This can lead to a crash!

Correction:

Thanks to Rob's answer

If you're dealing with repeating [NS]Timers, don't try to invalidate them in dealloc of the owner of the [NS]Timer because the dealloc obviously will not be called until the strong reference cycle is resolved. In the case of a UIViewController, for example, you might do it in viewDidDisappear

That being said viewDidDisappear may not always be the correct place since viewDidDisappear also gets called if you just push a new viewController on top of it. You should basically do it from a point that it's no longer needed. You get the idea...


§: Because the run loop maintains the timer, from the perspective of object lifetimes there’s typically no need to keep a reference to a timer after you’ve scheduled it. (Because the timer is passed as an argument when you specify its method as a selector, you can invalidate a repeating timer when appropriate within that method.) In many situations, however, you also want the option of invalidating the timer—perhaps even before it starts. In this case, you do need to keep a reference to the timer, so that you can stop it whenever appropriate.


With all the credit going to my colleague Brandon:

Pro Tip:

Even if you don't have a repeating timer, the Runloop [as mentioned within the docs] will hold a strong reference to your target if you use the selector function, until it fires, after that it will release it.

However if you use the block based function then as long as you point weakly to self inside your block then you don't need to worry about calling invalidate against the timer. It will just go away, since it was being maintained by the target not the runloop. If you don't use [weak self] then the block based will act just like the selector kind, that it will deallocate self after it has been fired.

Paste the following code in Playground and see the difference. The selector version will be deallocated after it fires. The block base will be deallocated upon being deallocated.

@objc class MyClass: NSObject {
    var timer: Timer?

    func startSelectorTimer() {
        timer = Timer.scheduledTimer(timeInterval: 3, target: self, selector: #selector(MyClass.doThing), userInfo: nil, repeats: false)
    }

    func startBlockTimer() {
        timer = Timer.scheduledTimer(withTimeInterval: 3, repeats: false, block: { [weak self] _ in
            self?.doThing()
        })
    }

    @objc func doThing() {
        print("Ran timer")
    }

    deinit {
        print("My Class deinited")
    }
}

var mySelectorClass: MyClass? = MyClass()
mySelectorClass?.startSelectorTimer()
mySelectorClass = nil // Notice that MyClass.deinit is not called until after Ran Timer happens
print("Should have deinited Selector timer here")

RunLoop.current.run(until: Date().addingTimeInterval(7))

print("---- NEW TEST ----")

var myBlockClass: MyClass? = MyClass()
myBlockClass?.startBlockTimer()
myBlockClass = nil // Notice that MyClass.deinit IS called before the timer finishes. No need for invalidation
print("Should have deinited Block timer here")

RunLoop.current.run(until: Date().addingTimeInterval(7))

First of all, invalidate is a method of NSTimer class which can use to stop currently running timer. Where when you assign nil to any object then, in an ARC environment the variable will release the object.

Its important to stop running timer when you don't longer need, so we write [timer invalidate] and then we write timer = nil; to make sure it'll loose its address from memory and later time you can recreate the timer.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!