В чем разница между self.timer = nil и [self.timer invalidate] в iOS?

Может ли кто-нибудь объяснить мне self.timer=nil vs [self.timer invalidate]?

Что именно происходит в ячейке памяти self.timer?

В моем коде

self.timer=nil

не останавливает таймер, но

[self.timer invalidate]

останавливает таймер.

Если вам нужен мой код, я тоже обновлю его.

Ответ 1

Как только вам не нужно запускать таймер, недействительный объект таймера, после чего нет необходимости аннулировать его ссылку.

Вот что говорит документация Apple: NSTimer

После запланированного цикла запуска таймер срабатывает при заданном до тех пор, пока он не будет признан недействительным. Не повторяющийся таймер недействителен сам сразу после его возникновения. Однако для повторяющегося таймера вы должен сам аннулировать объект таймера, вызвав его недействительным метод. Вызов этого метода требует удаления таймера из текущий цикл цикла; в результате вы всегда должны вызывать invalidate метод из того же потока, на котором был установлен таймер. Недействительный таймер немедленно отключает его, чтобы он больше не влияет на цикл выполнения. Затем цикл запуска удаляет таймер (и сильная ссылка на таймер), либо непосредственно перед invalidate возвращает или в какой-то более поздний момент. Как только это признано недействительным, объекты таймера не могут быть повторно использованы.

Ответ 2

Существует ключевое отличие, не упомянутое в других ответах.

Чтобы проверить это, добавьте следующий код в Playground.

1-я попытка:

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 не будет освобожден.

Результатом приведенного выше кода является то, что он все еще продолжает выполняться! Давай исправим.


2-я попытка:

Позвольте установить таймер на nil. Это должно удалить сильную ссылку timer, указывающую на person.

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

НЕПРАВИЛЬНО! Мы только удалили наш указатель на timer. И все же результат приведенного выше кода аналогичен нашей первоначальной попытке. он все еще продолжает выполняться... потому что цикл выполнения по-прежнему сохраняет на него указатель.


Так что нам нужно сделать?

Рад, что ты спросил. Мы должны invalidate таймер!

3-я попытка:

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

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

Это выглядит лучше, но все равно неправильно. Вы можете догадаться, почему?

Я дам вам подсказку. См. код ниже 👇.


4-я попытка (правильная)

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() удаляет сильную ссылку цикла выполнения, и теперь, если указатель на person удален... он освобождается.!

Попытка ниже также правильна:

5-я попытка (правильная)

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

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

Обратите внимание, что в нашей 5-й попытке мы не установили таймер на nil. Как это не нужно. Установка его nil - это просто индикатор, который мы можем использовать в других строках кода. Это помогает нам проверить это, и если бы это не было nil, мы бы знали, что таймер все еще действует, а также не имеет ничего не значащего объекта.

После аннулирования таймера вы должны присвоить nil переменной в противном случае переменная остается указывающей на бесполезный таймер. Память Управление и ARC не имеют ничего общего с тем, почему вы должны установить его nil. После аннулирования таймера self.timer теперь ссылается на бесполезный таймер. Не следует предпринимать дальнейших попыток использовать это значение. Установка его в nil гарантирует, что любые дальнейшие попытки доступа Результатом self.timer будет nil

from rmaddy comment above

При этом я думаю, что isValid является более значимым подходом, так же как isEmpty более значим и эффективен, чем array.count == 0...


Так почему 3-я попытка не верна?

Потому что нам нужен указатель на таймер, чтобы мы могли сделать его недействительным. Если мы установим этот указатель на nil, мы потеряем указатель на него. Мы теряем его, пока цикл выполнения все еще поддерживает указатель на него! Поэтому, если мы когда-либо хотели отключить таймер, мы должны invalidate сделать это ДО, мы потеряем нашу ссылку на него (то есть, прежде чем мы установим его указатель на nil), а затем он станет заброшенной памятью (не течь).

Вывод:

  • Чтобы избавиться от таймера, nil не требуется. На самом деле, это вредно, если вы делаете это до того, как invalidate ваш таймер.
  • Вызов invalidate удалит указатель цикла выполнения на него. Только тогда объект, содержащий таймер, будет освобожден.

Итак, как это применимо, когда я на самом деле строю приложение?

Если ваш viewController имеет свойство person, и вы вытолкнули этот viewController из стека навигации, тогда ваш viewController будет освобожден. В этом методе deinit вы должны сделать недействительным таймер человека. В противном случае ваш экземпляр пользователя будет сохранен в памяти из-за цикла выполнения, и его действие по-прежнему будет выполняться! Это может привести к сбою!

Исправление:

Благодаря Робу ответ

Если вы имеете дело с повторяющимися таймерами [NS], не пытайтесь сделать их недействительными в dealloc владельца таймера [NS], поскольку очевидно, что dealloc не будет вызываться до тех пор, пока не будет решен цикл сильных ссылок. Например, в случае UIViewController вы можете сделать это в viewDidDisappear

При этом viewDidDisappear не всегда может быть правильным местом, так как viewDidDisappear также вызывается, если вы просто помещаете новый viewController поверх него. Вы должны в основном делать это с момента, когда он больше не нужен. Вы поняли...


§: поскольку цикл выполнения поддерживает таймер с точки зрения Время жизни объекта, как правило, нет необходимости хранить ссылку на таймер после того, как вы запланировали это. (Потому что таймер передается как аргумент, когда вы указываете его метод в качестве селектора, вы можете сделать недействительным повторяющийся таймер, когда это уместно в этом методе.) Во многих ситуации, однако, вы также хотите опцию аннулирования таймер - возможно, даже до того, как он начнется В этом случае вам нужно сохраните ссылку на таймер, чтобы вы могли остановить его всякий раз, когда уместно.


С благодарностью моему коллеге Брэндону:

Профессиональный совет:

Даже если у вас нет повторяющегося таймера, Runloop [как упомянуто в документации] будет содержать сильную ссылку на вашу цель, если вы будете использовать функцию выбора, до тех пор, пока она не сработает, после этого она выпустит Это.

Однако если вы используете блочную функцию, тогда, пока вы слабо указываете на себя внутри своего блока, вам не нужно беспокоиться о вызове invalidate против таймера. Он просто уйдет, так как его поддерживал целевой объект, а не runloop. Если вы не используете [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))

Ответ 3

Прежде всего, invalidate - это метод класса NSTimer, который может использоваться для остановки текущего таймера. Где, когда вы назначаете nil любому объекту, тогда в среде ARC переменная освободит объект.

Важно остановить запуск таймера, когда вам это больше не нужно, поэтому мы пишем [timer invalidate], а затем пишем timer = nil;, чтобы он потерял свой адрес из памяти, а позже вы сможете восстановить таймер.