可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
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.
回答1:
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.
回答2:
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.
回答3:
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.
回答4:
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.
}
回答5:
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
.
回答6:
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.
回答7:
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")
}
}
回答8:
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
}
回答9:
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.