останавливает таймер.
Если вам нужен мой код, я тоже обновлю его.
Ответ 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))