J'utilise une NSTimer
comme ceci:
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];
Bien entendu, NSTimer
conserve la cible, ce qui crée un cycle de conservation. De plus, self
n'est pas un UIViewController, je n'ai donc rien de tel que viewDidUnload
où je peux invalider le minuteur pour interrompre le cycle. Je me demande donc si je pourrais utiliser une référence faible à la place:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
J'ai entendu dire que le chronomètre doit être invalidé (je suppose qu'il faut le relâcher de la boucle d'exécution). Mais nous pourrions le faire dans notre dealloc, non?
- (void) dealloc {
[timer invalidate];
}
Est-ce une option viable? J'ai vu de nombreuses façons dont les gens traitent ce problème, mais je ne l'ai pas vu.
Le code proposé:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
a pour effet que (i) une faible référence est faite à soi-même; (ii) cette référence faible est lue afin de fournir un pointeur sur NSTimer
. Cela n'aura pas pour effet de créer un NSTimer
avec une référence faible. La seule différence entre ce code et l'utilisation d'une référence __strong
est que, si self est désalloué entre les deux lignes données, vous passerez alors nil
au temporisateur.
La meilleure chose à faire est de créer un objet proxy. Quelque chose comme:
[...]
@implementation BTWeakTimerTarget
{
__weak target;
SEL selector;
}
[...]
- (void)timerDidFire:(NSTimer *)timer
{
if(target)
{
[target performSelector:selector withObject:timer];
}
else
{
[timer invalidate];
}
}
@end
Ensuite, vous feriez quelque chose comme:
BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];
Ou même ajouter une méthode de classe à BTWeakTimerTarget de la forme +scheduledTimerWithTimeInterval:target:selector:...
pour créer une forme plus nette de ce code. Vous voudrez probablement exposer le réel NSTimer
pour pouvoir le invalidate
, sinon les règles établies seront les suivantes:
Si vous n'êtes pas préoccupé par la précision à la milliseconde des événements du minuteur, vous pouvez utiliser dispatch_after & __weak au lieu de NSTimer pour le faire. Voici le modèle de code:
- (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];
});
}
Pas de propriété NSTimer @property, pas de commande invalidate/runloop et pas d'objet proxy, juste une méthode simple à nettoyer.
L'inconvénient de cette approche est que (contrairement à NSTimer
), le temps d'exécution du bloc (contenant [weakSelf doSomethingRepeatedly];
) aura une incidence sur la planification des événements.
iOS 10 et macOS 10.12 "Sierra" a introduit une nouvelle méthode, +scheduledTimerWithTimeInterval:repeats:block:
, afin que vous puissiez capturer self
faiblement comme suit:
__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
MyClass* _Nullable strongSelf = weakSelf;
[strongSelf doSomething];
}];
Équivalences dans Swift 3:
_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.doSomething()
}
Si vous devez toujours cibler iOS 9 ou une version antérieure (ce que vous devriez faire pour le moment), cette méthode ne peut pas être utilisée. Vous devrez donc toujours utiliser du code dans les autres réponses.
Swift 3
App cible <iOS 10:
Implémentation personnalisée de 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()
}
}
}
_ {Usage:
let timer = WeakTimer.scheduledTimer(timeInterval: 2,
target: self,
repeats: true) { [weak self] timer in
// Place your action code here.
}
timer
est une instance de la classe standard Timer
. Vous pouvez donc utiliser toutes les méthodes disponibles (par exemple, invalidate
, fire
, isValid
, fireDate
et etc).
__ instancetimer
sera désalloué lorsque self
sera désalloué ou lorsque le travail du minuteur sera terminé (par exemple, repeats == false
).
Cible de l'application> = iOS 10:
implémentation du minuteur standard:
open class func scheduledTimer(withTimeInterval interval: TimeInterval,
repeats: Bool,
block: @escaping (Timer) -> Swift.Void) -> Timer
_ {Usage:
let timer = Timer.scheduledTimer(withTimeInterval: 2, repeats: true) { [weak self] timer in
// Place your action code here.
}
Dans Swift, j'ai défini une classe d'assistance 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
}
}
Et puis vous pouvez l'utiliser comme ceci:
let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
// Your code here...
}
La variable NSTimer
renvoyée a une faible référence à self
. Vous pouvez donc appeler sa méthode invalidate
dans deinit
.
Peu importe que faibleSelf soit faible, le temporisateur conserve toujours l'objet, il y a donc encore un cycle de conservation. Puisqu'une minuterie est conservée par la boucle d'exécution, vous pouvez (et je suggère que) garder un pointeur faible sur la minuterie:
NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];
À propos de invalidate votre façon de faire est correcte.
Avec la théorie et la pratique. La solution de Tommy n’est pas un travail.
Théoriquement, __ instance faible est le paramètre, dans la mise en œuvre de
[NSTimer scheduledTimerWithTimeInterval:target:selector: userInfo: repeats:],
la cible sera conservée encore.
Vous pouvez implémenter un proxy, qui conserve la référence faible et le sélecteur de transfert en appelant à soi-même, puis passe le proxy en tant que cible. Tels que YYWeakProxy.
Swift 4 version. Invalidate doit être appelé avant le dealloc.
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()
}
}
Usage
func startTimer() {
timerProxy = TimerProxy(withInterval: 10,
repeats: false,
timerHandler: { [weak self] in
self?.fireTimer()
})
}
@objc func fireTimer() {
timerProxy?.invalidate()
timerProxy = nil
}
Si vous utilisez Swift, voici un minuteur à annulation automatique:
https://Gist.github.com/evgenyneu/516f7dcdb5f2f73d7923
La minuterie s’annule automatiquement le deinit
.
var timer: AutoCancellingTimer? // Strong reference
func startTimer() {
timer = AutoCancellingTimer(interval: 1, repeats: true) {
print("Timer fired")
}
}