Слабая ссылка на цель NSTimer для предотвращения сохранения цикла
Я использую NSTimer
следующим образом:
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];
Конечно, NSTimer
сохраняет цель, которая создает цикл сохранения. Кроме того, self
не является UIViewController, поэтому у меня нет ничего подобного viewDidUnload
, где я могу сделать недействительным таймер, чтобы разорвать цикл. Поэтому мне интересно, могу ли я использовать слабую ссылку:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
Я слышал, что таймер должен быть недействительным (я думаю, чтобы освободить его из цикла запуска). Но мы могли бы сделать это в нашем dealloc, правильно?
- (void) dealloc {
[timer invalidate];
}
Является ли это жизнеспособным вариантом? Я видел много способов, которыми люди справляются с этой проблемой, но я этого не видел.
Ответы
Ответ 1
Предлагаемый код:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
приводит к тому, что (i) слабая ссылка делается на себя; (ii) эта слабая ссылка считывается для указания указателя на NSTimer
. Это не приведет к созданию NSTimer
со слабой ссылкой. Единственная разница между этим кодом и использованием ссылки __strong
заключается в том, что если self освобождается между двумя указанными строками, вы передаете nil
на таймер.
Лучшее, что вы можете сделать, это создать прокси-объект. Что-то вроде:
[...]
@implementation BTWeakTimerTarget
{
__weak target;
SEL selector;
}
[...]
- (void)timerDidFire:(NSTimer *)timer
{
if(target)
{
[target performSelector:selector withObject:timer];
}
else
{
[timer invalidate];
}
}
@end
Затем вы сделаете что-то вроде:
BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];
Или добавьте метод класса в BTWeakTimerTarget формы +scheduledTimerWithTimeInterval:target:selector:...
, чтобы создать более аккуратную форму этого кода. Вероятно, вы захотите открыть реальный NSTimer
, чтобы вы могли invalidate
его, иначе установленные правила будут:
- реальная цель не сохраняется таймером;
- таймер будет срабатывать один раз после запуска (и, возможно, завершенного) реальной цели, но эта стрельба будет проигнорирована, а затем таймер недействителен.
Ответ 2
Если вы не беспокоитесь о миллисекундной точности событий таймера, вы можете использовать dispatch_after и __weak вместо NSTimer для этого. Здесь шаблон кода:
- (void) doSomethingRepeatedly
{
// Do it once
NSLog(@"doing something …");
// Repeat it in 2.0 seconds
__weak typeof(self) weakSelf = self;
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[weakSelf doSomethingRepeatedly];
});
}
Нет NSTimer @property, нет свойства invalidate/runloop и прокси-объекта, просто простой метод.
Недостатком этого подхода является то, что (в отличие от NSTimer
) время выполнения блока (содержащее [weakSelf doSomethingRepeatedly];
) будет влиять на планирование событий.
Ответ 3
iOS 10 и macOS 10.12 "Сьерра" введен новый метод +scheduledTimerWithTimeInterval:repeats:block:
, поэтому вы могли бы захватить self
слабо просто, как:
__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
MyClass* _Nullable strongSelf = weakSelf;
[strongSelf doSomething];
}];
Эквивалентность в Swift 3:
_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.doSomething()
}
Если вам все еще нужно настроить таргетинг на iOS 9 или ниже (что вам нужно в данный момент), этот метод не может быть использован, поэтому вам все равно придется использовать код в других ответах.
Ответ 4
В Swift я определил вспомогательный класс WeakTimer
:
/// A factory for NSTimer instances that invoke closures, thereby allowing a weak reference to its context.
struct WeakTimerFactory {
class WeakTimer: NSObject {
private var timer: NSTimer!
private let callback: () -> Void
private init(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) {
self.callback = callback
super.init()
self.timer = NSTimer(timeInterval: timeInterval, target: self, selector: "invokeCallback", userInfo: userInfo, repeats: repeats)
}
func invokeCallback() {
callback()
}
}
/// Returns a new timer that has not yet executed, and is not scheduled for execution.
static func timerWithTimeInterval(timeInterval: NSTimeInterval, userInfo: AnyObject?, repeats: Bool, callback: () -> Void) -> NSTimer {
return WeakTimer(timeInterval: timeInterval, userInfo: userInfo, repeats: repeats, callback: callback).timer
}
}
И тогда вы можете использовать его так:
let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
// Your code here...
}
Возвращенный NSTimer
имеет слабую ссылку на self
, поэтому вы можете вызвать его метод invalidate
в deinit
.
Ответ 5
Не имеет значения, что weakSelf слаб, таймер по-прежнему сохраняет объект, поэтому сохраняется цикл сохранения. Поскольку таймер сохраняется в цикле запуска, вы можете (и я предлагаю) удерживать слабый указатель на таймер:
NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];
О недействительности вы делаете правильно.
Ответ 6
Swift 3
Цель приложения < iOS 10:
Пользовательский WeakTimer (GitHubGist):
final class WeakTimer {
fileprivate weak var timer: Timer?
fileprivate weak var target: AnyObject?
fileprivate let action: (Timer) -> Void
fileprivate init(timeInterval: TimeInterval,
target: AnyObject,
repeats: Bool,
action: @escaping (Timer) -> Void) {
self.target = target
self.action = action
self.timer = Timer.scheduledTimer(timeInterval: timeInterval,
target: self,
selector: #selector(fire),
userInfo: nil,
repeats: repeats)
}
class func scheduledTimer(timeInterval: TimeInterval,
target: AnyObject,
repeats: Bool,
action: @escaping (Timer) -> Void) -> Timer {
return WeakTimer(timeInterval: timeInterval,
target: target,
repeats: repeats,
action: action).timer!
}
@objc fileprivate func fire(timer: Timer) {
if target != nil {
action(timer)
} else {
timer.invalidate()
}
}
}
Использование:
let timer = WeakTimer.scheduledTimer(timeInterval: 2,
target: self,
repeats: true) { [weak self] timer in
// Place your action code here.
}
timer
является экземпляром стандартного класса timer
, поэтому вы можете использовать все доступные методы (например, invalidate
, fire
, isValid
, fireDate
и т.д.).
Экземпляр timer
будет освобожден, если self
освободится или будет выполнено задание таймера (например, repeats == false
).
Цель приложения >= iOS 10:
Стандартная реализация таймера:
open class func scheduledTimer(withTimeInterval interval: TimeInterval,
repeats: Bool,
block: @escaping (Timer) -> Swift.Void) -> Timer
Использование:
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
// Place your action code here.
}
Ответ 7
Если вы используете Swift, это таймер автоматической отмены:
https://gist.github.com/evgenyneu/516f7dcdb5f2f73d7923
Таймер автоматически отменяет себя на deinit
.
var timer: AutoCancellingTimer? // Strong reference
func startTimer() {
timer = AutoCancellingTimer(interval: 1, repeats: true) {
print("Timer fired")
}
}