12

谁能解释我self.timer=nilvs [self.timer invalidate]

的内存位置究竟发生了什么self.timer

在我的代码中

self.timer=nil

不会停止计时器,但

[self.timer invalidate]

停止计时器。

如果您需要我的代码,我也会更新。

4

3 回答 3

11

一旦您不需要运行计时器,则使计时器对象无效,之后无需取消其引用。

这就是 Apple 文档所说的:NSTimer

一旦安排在运行循环上,计时器就会以指定的时间间隔触发,直到它失效。非重复计时器在触发后立即使自己失效。但是,对于重复计时器,您必须自己通过调用其 invalidate 方法来使计时器对象无效。调用此方法请求从当前运行循环中移除计时器;因此,您应该始终从安装了计时器的同一线程调用 invalidate 方法。使计时器无效会立即将其禁用,以便它不再影响运行循环。然后运行循环删除计时器(以及它对计时器的强引用),或者在 invalidate 方法返回之前或稍后的某个时间点。一旦失效,定时器对象就不能被重用。

于 2014-12-11T06:50:20.850 回答
8

其他答案中没有提到一个关键区别。

要对此进行测试,请将以下代码放入 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

所以让我问你一个问题。在代码的最后一行,我只是设置personnil. 这意味着person对象被释放,它的所有属性都被设置到nil内存并从内存中删除。正确的?

只要没有其他对象持有对它的强引用,一个对象就会被释放。在我们的例子中,timer仍然持有对 person 的引用,因为run- loop对 timer 有一个强引用,因此person对象不会被释放。

上面代码的结果是它仍然继续执行!让我们修复它。


第二次尝试:

让我们将计时器设置为nil。这应该删除timer指向的强引用person

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

我们的第 4 次尝试和第 3 次尝试一样,只是代码的顺序不同。

person?.timer?.invalidate()删除运行循环 对其目标的强引用,即self,现在如果一个指向person被删除...我们的人对象被释放!

下面的尝试也是正确的:

第五次尝试(正确)

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

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

请注意,在我们的第 5 次尝试中,我们没有将计时器设置为nil. 但 Apple 建议我们这样做:

一旦失效,定时器对象就不能被重用。

请参阅任务管理 - 计时器

将其设置nil为也是其他代码部分的指标。它有助于我们检查它,如果不是,nil那么我们就会知道计时器仍然有效,并且周围没有无意义的对象。

使计时器无效后,您应该将其分配nil给变量,否则该变量将指向无用的计时器。内存管理和 ARC 与为什么要将其设置为 nil. 在使计时器无效后,self.timer现在正在引用一个 无用的计时器。不应再尝试使用该值。将其设置为nil可确保进一步尝试访问 self.timer 将导致nil

来自rmaddy上面的评论

话虽如此,我认为这isValid是一种更有意义的方法,就像isEmpty比做array.count == 0...更有意义和有效一样


那么为什么第三次尝试不正确呢?

因为我们需要一个指向计时器的指针,所以我们可以使其无效。如果我们将该指针设置为 ,nil那么我们将失去指向它的指针。运行循环仍然保持其指向它的指针时,我们丢失了它!因此,如果我们想要关闭计时器,我们应该在失去invalidate对它的引用之前(即在我们将其指针设置为 之前nil)关闭它,否则它会成为废弃的内存(而不是泄漏)。

结论:

  • 要正确停止计时器,您必须使用invalidate. 不要niltimer你面前invalidate呢。
  • 使 a 无效后timer,将其设置为nil不会被重用。
  • 调用invalidate将删除运行循环的指针self。只有这样,包含计时器的对象才会被释放。

那么这在我实际构建应用程序时如何应用?

如果您的 viewController 具有person属性,然后您将该 viewController 从导航堆栈中弹出,那么您的 viewController 将被释放。在其deinit方法中,您必须使该人的计时器无效。否则,由于运行循环,您的 person 实例将保留在内存中,并且它的计时器操作仍将要执行!这可能导致崩溃!

更正:

感谢Rob 的回答

如果您正在处理重复的 [NS]Timer,请不要尝试在 [NS]Timer 的所有者的 dealloc 中使它们无效,因为在解决强引用循环之前显然不会调用 dealloc。例如,在 UIViewController 的情况下,您可以在 viewDidDisappear 中执行此操作

话虽这么说viewDidDisappear可能并不总是正确的地方,因为viewDidDisappear如果你只是在它上面推一个新的 viewController 也会被调用。您基本上应该从不再需要它的角度进行操作。你明白了……


§: 因为运行循环维护计时器,所以从对象生命周期的角度来看,通常不需要在您安排好计时器后保留对计时器的引用。(因为当您将计时器的方法指定为选择器时,计时器作为参数传递,您可以在该方法中适当时使重复计时器无效。)然而,在许多情况下,您还需要使计时器无效的选项——甚至可能在此之前开始。在这种情况下,您确实需要保留对计时器的引用,以便您可以在适当的时候停止它。


归功于我的同事布兰登:

专家提示:

即使您没有重复计时器,如果您使用选择器函数,Runloop [如文档中所述] 将保持对您的目标的强引用,直到它触发,之后它将释放它。

但是,如果您使用基于块的函数,那么只要您在块内弱指向 self ,那么 runloop 将不会保留self。但是由于缺少调用,它将继续执行invalidate

如果你不使用[weak self],那么基于块的行为就像选择器类型一样,它会self在它被触发后释放。

将以下代码粘贴到 Playground 中并查看差异。选择器版本将在触发后被释放。块基将在解除分配被解除分配。基本上,一个的生命周期由运行循环控制,而另一个则由对象本身控制

@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))
于 2018-09-27T20:24:08.743 回答
1

首先,invalidate是一个NSTimer类的方法,可以用来停止当前正在运行的定时器。那么,当您分配nil给任何对象时,在 ARC 环境中,变量将释放该对象。

当您不再需要时停止运行计时器很重要,因此我们编写[timer invalidate]然后我们编写timer = nil;以确保它会从内存中丢失其地址,稍后您可以重新创建计时器。

于 2014-12-11T06:51:33.557 回答