I'm using an NSTimer
like this:
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:self selector:@selector(tick) userInfo:nil repeats:YES];
Of course, NSTimer
retains the target which creates a retain cycle. Furthermore, self
isn't a UIViewController so I don't have anything like viewDidUnload
where I can invalidate the timer to break the cycle. So I'm wondering if I could use a weak reference instead:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
I've heard that the timer must be invalidated (i guess to release it from the run loop). But we could do that in our dealloc, right?
- (void) dealloc {
[timer invalidate];
}
Is this a viable option? I've seen a lot of ways that people deal with this issue, but I haven't seen this.
The proposed code:
__weak id weakSelf = self;
timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target:weakSelf selector:@selector(tick) userInfo:nil repeats:YES];
has the effect that (i) a weak reference is made to self; (ii) that weak reference is read in order to provide a pointer to NSTimer
. It won't have the effect of creating an NSTimer
with a weak reference. The only difference between that code and using a __strong
reference is that if self is deallocated in between the two lines given then you'll pass nil
to the timer.
The best thing you can do is create a proxy object. Something like:
[...]
@implementation BTWeakTimerTarget
{
__weak target;
SEL selector;
}
[...]
- (void)timerDidFire:(NSTimer *)timer
{
if(target)
{
[target performSelector:selector withObject:timer];
}
else
{
[timer invalidate];
}
}
@end
Then you'd do something like:
BTWeakTimerTarget *target = [[BTWeakTimerTarget alloc] initWithTarget:self selector:@selector(tick)];
timer = [NSTimer scheduledTimerWithTimeInterval:30.0 target:target selector:@selector(timerDidFire:) ...];
Or even add a class method to BTWeakTimerTarget of the form +scheduledTimerWithTimeInterval:target:selector:...
to create a neater form of that code. You'll probably want to expose the real NSTimer
so that you can invalidate
it, otherwise the rules established will be:
- the real target isn't retained by the timer;
- the timer will fire once after the real target has begun (and probably completed) deallocation, but that firing will be ignored and the timer invalidated then.
If you are not that concerned about the millisecond accuracy of the timer events, you could use dispatch_after & __weak instead of NSTimer to do this. Here's the code pattern:
- (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 invalidate/runloop stuff and no proxy object, just a simple clean method.
The downside of this approach is that (unlike NSTimer
) the execution time of the block (containing [weakSelf doSomethingRepeatedly];
) will impact scheduling of the events.
iOS 10 and macOS 10.12 "Sierra" introduced a new method, +scheduledTimerWithTimeInterval:repeats:block:
, so you could capture self
weakly simply as:
__weak MyClass* weakSelf = self;
_timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer* t) {
MyClass* _Nullable strongSelf = weakSelf;
[strongSelf doSomething];
}];
Equivalence in Swift 3:
_timer = Timer(timeInterval: 1.0, repeats: true) { [weak self] _ in
self?.doSomething()
}
If you still need to target iOS 9 or below (which you should at this moment), this method cannot be used, so you would still need to use code in the other answers.
Swift 3
App target < iOS 10:
Custom WeakTimer (GitHubGist) implementation:
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
is instance of standard class Timer
, so you can use all available methods (e.g. invalidate
, fire
, isValid
, fireDate
and etc).timer
instance will be deallocated when self
is deallocated or when timer's job is done (e.g. repeats == false
).
App target >= iOS 10:
Standard Timer implementation:
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.
}
In Swift I've defined a WeakTimer
helper class:
/// 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
}
}
And then you can use it like such:
let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
// Your code here...
}
The returned NSTimer
has a weak reference to self
, so you can call its invalidate
method in deinit
.
It doesn't matter that weakSelf is weak, the timer still retains the object so there's still a retain cycle. Since a timer is retained by the run loop, you can (and I suggest to ) hold a weak pointer to the timer:
NSTimer* __weak timer = [NSTimer scheduledTimerWithTimeInterval:30.0f target: self selector:@selector(tick) userInfo:nil repeats:YES];
About invalidate you're way of doing is correct.
If you are using Swift here is an auto-cancelling timer:
https://gist.github.com/evgenyneu/516f7dcdb5f2f73d7923
The timer cancels itself automatically on deinit
.
var timer: AutoCancellingTimer? // Strong reference
func startTimer() {
timer = AutoCancellingTimer(interval: 1, repeats: true) {
print("Timer fired")
}
}
Swift 4 version. Invalidate must be called before the 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
}
With theory and practice.Tommy's solution is not work.
Theoretically,__weak instance is as the parameter,In the implementation of
[NSTimer scheduledTimerWithTimeInterval:target:selector: userInfo: repeats:],
target will be retained still.
You can implement a proxy ,which hold the weak reference and forward selector calling to self , and then pass the proxy as the target. Such as YYWeakProxy.
来源:https://stackoverflow.com/questions/16821736/weak-reference-to-nstimer-target-to-prevent-retain-cycle