iOS 中 self.timer = nil 与 [self.timer invalidate] 有啥区别?

Posted

技术标签:

【中文标题】iOS 中 self.timer = nil 与 [self.timer invalidate] 有啥区别?【英文标题】:What is difference between self.timer = nil vs [self.timer invalidate] in iOS?iOS 中 self.timer = nil 与 [self.timer invalidate] 有什么区别? 【发布时间】:2015-02-09 14:02:57 【问题描述】:

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

self.timer 的内存位置到底发生了什么?

在我的代码中

self.timer=nil

不会停止计时器,但

[self.timer invalidate]

停止计时器。

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

【问题讨论】:

NSTimer - "特别注意运行循环维护对其计时器的强引用,因此在将计时器添加到运行循环后,您不必维护自己对计时器的强引用。”因此,您的并不是唯一对该计时器的引用,这就是为什么将其归零并不能阻止它触发的原因。 是的,我得到了答案,谢谢 【参考方案1】:

一旦你不需要运行定时器,就使定时器对象失效,之后就不需要取消它的引用了。

这是 Apple 文档所说的:NSTimer

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

【讨论】:

在定时器失效后,你应该将 nil 赋给变量,否则变量会指向一个无用的定时器。 内存管理和 ARC 与为什么要将其设置为 nil 无关。在使计时器无效后,self.timer 现在引用了一个无用的计时器。不应再尝试使用该值。将其设置为nil 可确保进一步尝试访问self.timer 将导致nil 我建议在 if 条件下检查 self.timer.isValid 属性,然后再使用它 @Honey 在失效后立即设置为nil。如果不是重复定时器,可以在块尾设置为nil @rmaddy 我正在阅读docs。不确定您的 cmets 是否现在有效。 1. 一个非重复定时器在它触发后立即失效。 所以我们不需要在它的块结束时使一个非重复定时器失效 2. 使定时器失效立即禁用它所以它不再影响运行循环。然后运行循环删除计时器(以及它对计时器的强引用)这是否意味着它现在是nil? 3 一旦它的父对象被释放,定时器不会像任何其他属性一样被释放吗?【参考方案2】:

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

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

【讨论】:

【参考方案3】:

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

要对此进行测试,请将以下代码放入 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。这应该删除指向persontimer 的强引用。

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 是一种更有意义的方法,就像isEmptyarray.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))

【讨论】:

感谢您的深入分析。我来到这里是因为我想知道是否有必要在将其无效后将计时器设置为nil。苹果文档似乎暗示这不是必需的。但是在我自己的测试中,我发现仅仅使定时器无效并不会从内存中删除该定时器的实例。很高兴看到你得出了同样的结论。所以我想在你失效后总是将定时器设置为nil 是一个好习惯。

以上是关于iOS 中 self.timer = nil 与 [self.timer invalidate] 有啥区别?的主要内容,如果未能解决你的问题,请参考以下文章

iOS 中nil,Nil,NULL,NSNull的区别

🔥🔥iOS中解决NSTimer循环引用问题

IOS Swift Amazon S3 传输实用程序 - nil 与预期的参数类型 nsurl 不兼容

iOS Facebook 登录“object nil”崩溃

iOS中xib与storyboard各种加载

为啥 SKSpriteNode(fileNamed:) 使用 Swift 在 iOS 中生成 nil?