Подтвердить что ты не робот

Слабая ссылка на цель 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];
}

Является ли это жизнеспособным вариантом? Я видел много способов, которыми люди справляются с этой проблемой, но я этого не видел.

4b9b3361

Ответ 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")
  }
}