其他答案中没有提到一个关键区别。
要对此进行测试,请将以下代码放入 Playground。
第一次尝试:
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
所以让我问你一个问题。在代码的最后一行,我只是将person 设置为nil。这意味着person 对象被释放,其所有属性都设置为nil 并从内存中删除。对吧?
只要没有其他对象持有对它的强引用,一个对象就会被释放。在我们的例子中,timer 仍然持有对 person 的 strong 引用,因为run-loop has a strong reference to the timer§ 因此person 对象不会被释放。
上面代码的结果是它仍然继续执行!
让我们解决它。
第二次尝试:
让我们将计时器设置为nil。这应该删除指向person 的timer 的强引用。
var person : Person? = Person(age: 0)
let _ = person?.timer
person?.timer = nil
person = nil
错了!我们只删除了 我们的 指向timer 的指针。然而上面代码的结果就像我们最初的尝试一样。它仍在继续执行...因为 运行循环 仍在定位/引用 self。
那么我们需要做什么?
很高兴你问。我们必须invalidate计时器!
第三次尝试:
var person : Person? = Person(age: 0)
let _ = person?.timer
person?.timer = nil
person?.timer?.invalidate()
person = nil
这看起来更好,但它仍然是错误的。你能猜到为什么吗?
我会给你一个提示。请参阅下面的代码?。
第四次尝试(正确)
var person : Person? = Person(age: 0)
let _ = person?.timer
person?.timer?.invalidate()
person?.timer = nil
person = nil
// person was deallocated
我们的第四次尝试和第三次尝试一样,只是代码的序列不同。
person?.timer?.invalidate() 移除 run loop 的强引用
到它的目标,即self,现在如果指向person 的指针被删除...我们的人对象被释放!
下面的尝试也是正确的:
第 5 次尝试(正确)
var person : Person? = Person(age: 0)
let _ = person?.timer
person?.timer?.invalidate()
person = nil
// person was deallocated
请注意,在我们的第 5 次尝试中,我们没有将计时器设置为 nil。但 Apple 建议我们这样做:
一旦失效,定时器对象就不能再使用了。
见Task Management - Timer
将其设置为nil 也是一个指标,用于其他代码部分。它有助于我们检查它,如果它不是nil,那么我们就会知道计时器仍然有效并且周围没有无意义的对象。
在使计时器无效后,您应该将nil 分配给变量
否则该变量将指向一个无用的计时器。记忆
management 和 ARC 与您为什么应该将其设置为无关
nil。使计时器无效后,self.timer 现在正在引用一个
无用计时器。不应再尝试使用该值。将其设置为 nil 可确保任何进一步的访问尝试
self.timer 将导致nil
来自rmaddy的评论above
话虽如此,我认为isValid 是一种更有意义的方法,就像isEmpty 比array.count == 0 更有意义和高效...
那么为什么第三次尝试不正确?
因为我们需要一个指向计时器的指针,所以我们可以使其无效。如果我们将该指针设置为nil,那么我们将失去指向它的指针。我们丢失了它而 run-loop 仍然保持其指向它的指针!因此,如果我们想要关闭计时器,我们应该在 BEFORE 之前将其 invalidate 关闭我们失去对它的引用(即在我们将其指针设置为 nil 之前)否则它会成为一个废弃的内存(@ 987654324@).
结论:
- 要正确停止计时器,您必须使用
invalidate。不要niltimer在你invalidate它之前。
- 使
timer 失效后,将其设置为nil,这样它就不会被重复使用。
- 调用
invalidate 将删除运行循环指向self 的指针。只有这样,包含计时器的对象才会被释放。
那么当我实际构建应用程序时,这如何应用?
如果您的 viewController 具有 person 属性,然后您将该 viewController 从导航堆栈中弹出,那么您的 viewController 将被释放。在其deinit 方法中,您必须使该人的计时器无效。否则,您的 person 实例会因为 run loop 而保存在内存中,并且它的计时器操作仍将要执行!这可能会导致崩溃!
更正:
感谢Rob's answer
如果您正在处理重复的 [NS]Timer,请不要尝试在 [NS]Timer 的所有者的 dealloc 中使它们无效,因为在强引用循环之前不会调用 dealloc解决。例如,在 UIViewController 的情况下,您可以在 viewDidDisappear 中执行此操作
话虽如此,viewDidDisappear 可能并不总是正确的位置,因为如果您只是将一个新的 viewController 推到它上面,viewDidDisappear 也会被调用。您基本上应该从不再需要它的角度进行操作。你明白了……
§:因为run loop维护了定时器,从
对象生命周期通常不需要保留对
计时器在你安排之后。 (因为计时器作为
当您将其方法指定为选择器时,您可以使参数无效
在该方法中适当时重复计时器。)在许多
但是,您还希望选择使
计时器——甚至可能在它开始之前。在这种情况下,您确实需要
保留对计时器的引用,以便您可以随时停止它
合适。
感谢我的同事布兰登:
专业提示:
即使您没有重复计时器,如果您使用 selector function,Runloop [如文档中所述] 将保持对您的目标的强引用,直到它触发,之后它会释放它。
但是,如果您使用block based function,那么只要您在块内弱指向self,那么runloop 将不会保留self。但是它会继续执行,因为没有调用invalidate
如果你不使用[weak self],那么基于块的行为就像选择器类型一样,它会在被触发后释放self。
将以下代码粘贴到 Playground 中,看看有什么不同。选择器版本将在 触发后被释放。 在解除分配时,块基将被解除分配。基本上,一个的生命周期由 runloop 控制,而另一个则由对象本身控制
@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))