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:
- el objetivo real no es retenido por el temporizador;
- 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.