guide basics objective-c automatic-ref-counting nstimer retain-cycle

objective-c - basics - swift reference



Referencia débil al objetivo de NSTimer para evitar el ciclo de retención (8)

El código propuesto:

__weak id weakSelf = self; timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

tiene el efecto de que (i) se hace una referencia débil a uno mismo; (ii) esa referencia débil se lee para proporcionar un puntero a NSTimer . No tendrá el efecto de crear un NSTimer con una referencia débil. La única diferencia entre ese código y el uso de una referencia __strong es que si el self se desasigna entre las dos líneas dadas, pasarás nil al temporizador.

Lo mejor que puedes hacer es crear un objeto proxy. Algo como:

[...] @implementation BTWeakTimerTarget { __weak target; SEL selector; } [...] - (void)timerDidFire:(NSTimer *)timer { if(target) { [target performSelector:selector withObject:timer]; } else { [timer invalidate]; } } @end

Entonces harías algo como:

BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)]; timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];

O incluso agregue un método de clase a BTWeakTimerTarget de la forma +scheduledTimerWithTimeInterval:target:selector:... para crear una forma más nítida de ese código. Probablemente quiera exponer el NSTimer real para que pueda invalidate lo contrario, las reglas establecidas serán:

  1. el objetivo real no es retenido por el temporizador;
  2. el temporizador disparará una vez que el objetivo real haya comenzado (y probablemente haya completado) la desasignación, pero esa activación se ignorará y el temporizador se invalidará en ese momento.

Estoy usando un NSTimer así:

timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];

Por supuesto, NSTimer retiene el objetivo que crea un ciclo de retención. Además, self no es un UIViewController así que no tengo nada como viewDidUnload donde puedo invalidar el temporizador para romper el ciclo. Entonces me pregunto si podría usar una referencia débil en su lugar:

__weak id weakSelf = self; timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];

He oído que el temporizador debe estar invalidado (supongo que para liberarlo del ciclo de ejecución). Pero podríamos hacer eso en nuestro dealloc, ¿verdad?

- (void) dealloc { [timer invalidate]; }

¿Es esta una opción viable? He visto muchas maneras en que las personas lidian con este problema, pero no he visto esto.


En Swift he definido una clase de ayuda 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 } }

Y luego puedes usarlo así:

let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in // Your code here... }

El NSTimer devuelto tiene una referencia débil a self , por lo que puede invocar su método de invalidate en deinit .


No importa que el weakelf es débil, el temporizador aún retiene el objeto por lo que todavía hay un ciclo de retención. Como el ciclo de ejecución conserva un temporizador, puede (y sugiero que) mantener un puntero débil al temporizador:

NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];

Acerca de invalidar que tu forma de hacerlo es correcta.


Si está usando Swift aquí hay un temporizador de cancelación automática:

https://gist.github.com/evgenyneu/516f7dcdb5f2f73d7923

El temporizador se cancela automáticamente al deinit .

var timer: AutoCancellingTimer? // Strong reference func startTimer() { timer = AutoCancellingTimer(interval: 1, repeats: true) { print("Timer fired") } }


Si no le preocupa la precisión en milisegundos de los eventos del temporizador, puede usar dispatch_after & __weak en lugar de NSTimer para hacerlo. Aquí está el patrón de código:

- (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]; }); }

No NSTimer @property, no hay material invalidate / runloop y ningún objeto proxy, solo un método de limpieza simple.

La desventaja de este enfoque es que (a diferencia de NSTimer ) el tiempo de ejecución del bloque (que contiene [weakSelf doSomethingRepeatedly]; ) tendrá un impacto en la programación de los eventos.


Swift 4 versión. Invalidar debe invocarse antes del acuerdo.

class TimerProxy { var timer: Timer! var timerHandler: (() -> Void)? init(withInterval interval: TimeInterval, repeats: Bool, timerHandler: (() -> Void)?) { self.timerHandler = timerHandler timer = Timer.scheduledTimer(timeInterval: interval, target: self, selector: #selector(timerDidFire(_:)), userInfo: nil, repeats: repeats) } @objc func timerDidFire(_ timer: Timer) { timerHandler?() } func invalidate() { timer.invalidate() } }

Uso

func startTimer() { timerProxy = TimerProxy(withInterval: 10, repeats: false, timerHandler: { [weak self] in self?.fireTimer() }) } @objc func fireTimer() { timerProxy?.invalidate() timerProxy = nil }


Swift 3

Objetivo de la aplicación <iOS 10 :

Implementación personalizada 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() } } }

Uso:

let timer = WeakTimer.scheduledTimer(timeInterval: 2, target: self, repeats: true) { [weak self] timer in // Place your action code here. }

timer es una instancia del Timer clase estándar, por lo que puede utilizar todos los métodos disponibles (por ejemplo, invalidate , fire , isValid , fireDate y etc.).
timer instancia del timer se desasignará cuando el self sea ​​desasignado o cuando el trabajo del temporizador haya finalizado (p. ej., repeats == false ).

Objetivo de la aplicación> = iOS 10 :
Implementación del temporizador estándar:

open class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Swift.Void) -> Timer

Uso:

let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in // Place your action code here. }


iOS 10 y macOS 10.12 "Sierra" introdujo un nuevo método, +scheduledTimerWithTimeInterval:repeats:block: para que pueda capturar self débilmente simplemente como:

__weak MyClass* weakSelf = self; _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) { MyClass* _Nullable strongSelf = weakSelf; [strongSelf doSomething]; }];

Equivalencia en Swift 3:

_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in self?.doSomething() }

Si todavía necesita apuntar a iOS 9 o inferior (que debe hacerlo en este momento), este método no se puede usar, por lo que aún deberá usar el código en las otras respuestas.