使用弱引用指向NSTimer目标以防止循环引用。

58

我正在像这样使用 NSTimer

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

当然了,NSTimer 会保留创建它的对象,从而导致循环引用。此外,self 不是 UIViewController,所以我没有像 viewDidUnload 这样的方法来使定时器失效以打破循环引用。因此,我想知道是否可以使用弱引用来解决这个问题:

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

我听说计时器必须被作废(我猜是为了从运行循环中释放它)。但我们可以在dealloc方法中这样做,对吗?

我听说定时器必须被无效化(我猜是为了从执行循环中释放它)。但是我们可以在 dealloc 方法中做到这一点,对吧?

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

这是一个可行的选择吗?我看到很多人处理这个问题的方式,但我没有看到过这种方法。


27
除了下面的回答外,没有人解释为什么在dealloc中使定时器无效是无用的(来自这里): 定时器保留对其目标的强引用。这意味着只要定时器保持有效,它的目标就不会被释放。作为一个推论,这意味着定时器的目标在其dealloc方法中尝试使定时器无效是没有意义的 - 只要定时器有效,dealloc方法将不会被调用。 - Guy
10个回答

77

提议的代码:

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

该代码的效果是:(i)对self创建一个弱引用;(ii)读取该弱引用以提供指向NSTimer的指针。它不会产生使用弱引用来创建NSTimer的影响。与使用__strong引用的唯一区别是如果在这两行代码之间self被解除分配,则会将nil传递给计时器。

最好的方法是创建一个代理对象。例如:

[...]
@implementation BTWeakTimerTarget
{
    __weak target;
    SEL selector;
}

[...]

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

那么你可以这样做:

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

或者你甚至可以给BTWeakTimerTarget添加一个类方法,其形式为+ scheduledTimerWithTimeInterval:target:selector:..., 以创建一个更简洁的代码形式。你可能需要暴露真正的NSTimer,这样你才能使其失效,否则将会有以下规则:

  1. 计时器不会保留真正的目标对象;
  2. 计时器将在真正的目标已经开始(并可能已经完成)释放之后触发一次,但该触发将被忽略并立即使计时器失效。

3
太棒了,谢谢。我最终制作了一个NSWeakTimer外壳来处理选择器的连线 https://gist.github.com/bendytree/5674709 - bendytree
@bendytree,我已经审查了你的代码。目标属性不应该是assign类型的,而应该是像@Tommy所描述的weak类型的。assign类型的属性在释放后不会变成nil,而weak类型的属性会。因此,你的if (target)检查永远不会为真。 - Pwner
@Pwner 我已经检查过并将assign更改为weak,但它仍然不会使其无效。我该怎么办? - sftsz
@sftsz 你在使用runloop来运行定时器吗? - mabounassif
我刚试了一下@bendytree的解决方案,我一直在苦苦思索,直到发现你在“fire”方法中没有向选择器传递对象。我的应用程序使用重复计时器,并在由“fire”调用的选择器中使其无效,如果满足某些条件。由于没有传递对象,计时器从未失效并一直运行。任何人复制/粘贴您的代码都应确保使用“withObject:timer”而不是“withObject:nil”。无论如何,感谢您提供的片段! - guitarflow
完美的答案,节省了很多时间。 - Vishwas Singh

35

iOS 10macOS 10.12 "Sierra" 引入了一种新的方法,+scheduledTimerWithTimeInterval:repeats:block:,因此您可以简单地使用以下方式弱引用 self

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

Swift 3 中的等价性:

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

如果您仍需要针对iOS 9或更低版本进行目标设置(在此时应该这样做),则无法使用此方法,因此您仍需要在其他答案中使用代码。

1
即使self是弱引用,直到计时器被无效化,这个代码仍然不起作用。 - protspace
1
@protspace 那么你可以像这样做,或者不做?guard let weakSelf = self else { timer.invalidate(); return }。当self不再可用时,计时器将被无效化。 - Baran Emre

26

如果你不是非常关注计时器事件的毫秒级准确性,那么你可以使用 dispatch_after & __weak 代替 NSTimer 来完成这个功能。以下是代码模式:

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

没有NSTimer @property,也没有invalidate/runloop的东西,也没有代理对象,只有一个简单干净的方法。

这种方法的缺点是(与NSTimer不同),块的执行时间(包含[weakSelf doSomethingRepeatedly];)将影响事件的调度。


1
好主意。如果你想让它重复,可以检查 weakSelf != nil,然后在调度块内简单地递归调用 dispatch_after。唯一的缺点是,在设置完成后更改计时器(例如更改间隔)会变得困难。 - devios1
1
使用这种方法实现重复定时器时要小心。当您使用常规的重复NSTimer时,目标选择器的执行持续时间不会影响您的定时。在这种情况下,如果在定时器被调用时不立即进行另一个dispatch_after(即使如此),您将会出现定时偏差。 - Guy

9

Swift 3

iOS 10以下的应用目标:

自定义的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()
        }
    }
}

用法:

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

timer是标准类Timer的实例,因此您可以使用所有可用的方法(例如invalidatefireisValidfireDate等)。

timer实例将在self被释放或定时器完成任务时(例如repeats == false)被释放。

应用程序目标 >= iOS 10:
标准计时器实现:

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

使用方法:

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

但是你现在不是把责任转嫁给了“WeakTimer”吗?因为“WeakTimer#init”现在向“Timer”传递了一个对“target:self”的强引用。 - mkirk
2
@mkirk,是的,但Timer是唯一一个对WeakTimer保持强引用的对象。这意味着只要Timer存在,WeakTimer实例就会一直存在。Timer存活到调用fileprivate func fire时刻,而此时target已经死亡。target有自己的生命周期,在上述存储桶中没有任何人对其进行强引用。 - Vlad Papko

5
在Swift中,我定义了一个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
  }
}

然后您可以像这样使用它:
let timer = WeakTimerFactory.timerWithTimeInterval(interval, userInfo: userInfo, repeats: repeats) { [weak self] in
  // Your code here...
}

返回的NSTimerself有弱引用,因此可以在deinit中调用其invalidate方法。


1
你想把 userInfo 传递到 NSTimer 构造函数中,是吗? - pwightman
@pwightman 哎呀,太好了!我已经更新了我的回复中的代码。(以及我自己存储库中的代码...我一直在传递niluserInfo,所以我完全忽略了这个问题。)谢谢! - shadowmatter
1
我不明白该如何执行计时器,因为您的代码块没有对它进行执行。 - thibaut noah
这会导致泄漏检查问题(在Instruments中进行配置)。请尝试使用此链接 https://gist.github.com/onevcat/2d1ceff1c657591eebde - Aleksey Tsyss
从静态方法返回的weakTimer如果不重复,则不会保留自身。因此,它只能在重复模式下工作。此外,需要手动将其添加到runloop中。 - leavez

3

即使 weakSelf 是弱引用,计时器仍然会保留该对象,因此仍存在强引用循环。由于计时器被运行循环保留,您可以(而且我建议)持有计时器的弱引用:

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

关于使无效,你的做法是正确的。


9
那么,事件循环不会一直强引用计时器,计时器也不会一直强引用“self”,所以不会触发dealloc吗? - Tommy
至少它避免了保留循环。我仍然同意使用弱定时器更好。 - Ramy Al Zuhouri
2
@Tommy是正确的,它并没有避免保留循环。只要计时器没有被使无效,对象就永远不会死亡,如果只有dealloc使计时器失效,那么它也永远不会失效。弱定时器是毫无意义的,几乎从来不是你想要的。 - Mecki
定时器也可以调用一个weakSelf选择器。 - Corbin Miller
@Mecki,苹果的计时器编程指南与您的观点不一致:https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Timers/Articles/usingTimers.html - frangulyan
2
@frangulyan 不,它并没有。它说“因为运行循环维护计时器”,这意味着运行循环使计时器保持活动状态。如果您让它保持活动状态,它不会死亡,只要它被安排了,并且取消计时器的官方方法是使其无效。它还说:“计时器对其目标保持强引用。”,因此只要计时器存在,它也会保持其目标存在。因此,除非在某个时候使其无效,否则将计时器设置为弱引用并没有解决任何问题。 - Mecki

0

Swift 4 版本。在 dealloc 之前必须调用 Invalidate。

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

使用方法

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

@objc func fireTimer() {
    timerProxy?.invalidate()
    timerProxy = nil
}

0

答案非常简单。例如,您可以尝试这样做:

@interface Person : NSObject
@property(nonatomic, strong) DemoViewController_19 *vc;
@end

@implementation Person
@end

@interface DemoViewController_19 ()
@property(nonatomic, strong) Person *person;
@end

@implementation DemoViewController_19

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    self.person = [Person new];
    __weak typeof(self) weaks = self;
    self.person.vc = weaks;
}

@end

运行后,您会发现vc dealloc未被调用。这取决于Person的强属性。


0

结合理论和实践,Tommy的解决方案不可行。

从理论上讲,__weak实例作为参数,在实现中...

[NSTimer scheduledTimerWithTimeInterval:target:selector: userInfo: repeats:],

目标对象将保持不变。

您可以实现一个代理,它持有弱引用并将选择器调用转发给自身,然后将代理作为目标传递。例如YYWeakProxy。


0
如果您正在使用Swift,这里有一个自动取消的计时器:

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

计时器会在deinit时自动取消。
var timer: AutoCancellingTimer? // Strong reference

func startTimer() {
  timer = AutoCancellingTimer(interval: 1, repeats: true) {
    print("Timer fired")
  }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接