重复使用NSTimer,弱引用,拥有引用或iVar?

10

我想将这个问题单独放在这里,与我的先前的问题“保留重复的NSTimer以供后续访问”分开,因为讨论已经进一步发展,用一个新的问题更加清晰,而不是再次编辑:

场景是,一个对象在viewDidLoad中创建了一个重复的NSTimer,一旦创建,NSTimer需要保持存在,以便其他方法可以访问它。

NSTimer *ti = [NSTimer scheduledTimerWithTimeInterval:1 
                                               target:self 
                                             selector:@selector(updateDisplay:) 
                                             userInfo:nil 
                                              repeats:YES];

我明白当创建runloop时,runloop会拥有NSTimer,并在调用[ti invalidate];后最终停止、移除并释放NSTimer。

由于我们需要在多个方法中访问NSTimer,因此我们需要一种保留对它的引用以供未来使用的方法,修改后的问题是:

// (1) Should the NSTimer be held using an owning reference (i.e.)
@property(nonatomic, retain) NSTimer *walkTimer;
[self setWalkTimer: ti];
...
...
// Cancel method
[[self walkTimer] invalidate;
[self setWalkTimer:nil];
...
...
// dealloc method
[walkTimer release];
[super dealloc];

.

// (2) Should the NSTimer be held using a weak reference (i.e.)
@property(nonatomic, assign) NSTimer *walkTimer;
[self setWalkTimer: ti];
...
...
// Cancel method
[[self walkTimer] invalidate];
[self setWalkTimer:nil];
...
...
// dealloc method
[super dealloc];

.

// (3) Use an iVar and rely on the runLoop holding (i.e. retaining) the timer
NSTimer *walkTimer;
NSTimer *walkTimer = [NSTimer scheduledTimerWithTimeInterval:1 
                                                      target:self 
                                                    selector:@selector(updateDisplay:) 
                                                    userInfo:nil 
                                                     repeats:YES];
...
...
// Cancel method
[walkTimer invalidate];
walkTimer = nil;

.

// (4) Something not listed above ...

我很高兴只用(1) (2) (3)或(4),因为在其他帖子中已经有很多关于哪种方式最好的讨论。看起来有很多不同的答案,所以我希望这个更具体的问题能够帮助集中讨论在这种情况下可能是最佳实践的内容。


编辑:

作为一个副注,在Apple NSTimer 类引用中,五个示例代码项目中有四个使用了被分配给保留属性的NSTimer。以下是类参考示例显示的示例:

@property (nonatomic, retain) NSTimer *updateTimer;
updateTimer = [NSTimer scheduledTimerWithTimeInterval:.01 target:self selector:@selector(updateCurrentTime) userInfo:p repeats:YES];
...
...
// Cancel
[updateTimer invalidate];
updateTimer = nil;
...
...
// Dealloc method
[super dealloc];
[updateTimer release];

需要注意的是,在这些例子中,Apple直接分配了iVar而不使用属性设置器。


那个来自苹果的最后一个例子看起来明显是错误的,但我知道你是从哪里得到的。这个属性被声明为retain,但实际上计时器并没有被保留 - 在dealloc中的最后一个release应该会导致崩溃。我有什么遗漏吗? - Daniel Dickison
@Daniel Dickison 这段代码不会崩溃的原因是,当你在模拟器中构建和调试它时,该代码示例中的dealloc实际上从未被调用——这有些说得通,因为对象似乎应该与应用程序一样长寿... 话虽如此,你是正确的:这完全是错的。 - danyowdee
2个回答

18

在更加深入地思考后,我发现了我的推理中的一个重要缺陷,得出了不同的结论:

无论你持有计时器的拥有或非拥有引用,都不会有太大影响。这完全是一种口味问题。

关键在于计时器的目标是什么:

如果创建计时器的对象是其目标,则管理该对象的生命周期变得更加脆弱:它不能简单地进行保留/释放管理,而是需要确保持有对此对象的最后引用的客户端在处理对象之前使其失效。

让我用几个近似对象图来说明这种情况:

  1. 你从一个状态开始,设置计时器并将自己设为目标。计时器的设置: yourObject 是由 someClientObject 拥有的。同时存在一个包含预定计时器数组的当前运行循环。在 yourObject 上调用 setupTimer 方法:

  2. 结果是以下初始状态。除了前面的状态之外,yourObject 现在还拥有对 workTimer 的引用(拥有或不拥有),而 workTimer 又拥有 yourObject。此外,workTimer 是运行循环预定计时器数组的所有者:

  3. 因此,现在你将使用该对象,但当你完成它并简单释放时,你最终会遇到简单的释放泄漏:在通过简单释放处理 yourObject 后,someClientObject 会使其失效,yourObject 将与对象图分离,但仍然被 workTimer 保持活动状态。因此 workTimeryourObject 会泄漏!

如果运行循环保持计时器的活性,进而保持对对象的拥有引用,那么您泄漏了对象(和计时器)。

如果只有一个单独的实例正确处理取消操作,那么可以避免这种情况,使yourObject始终只拥有一个所有者:在通过释放处理yourObject之前,someClientObject调用yourObject上的cancelTimer方法。在该方法中,yourObject使workTimer无效,并(如果它拥有workTimer)通过释放处置workTimer:

但是,现在如何解决以下情况?
多个所有者:设置与初始状态相同,但现在有多个独立的clientObjects持有对yourObject的引用。

我知道没有简单的答案!(虽然后者没什么可说的,不过...)

所以我的建议是...

  1. 不要将计时器作为属性/不要提供访问器!相反,将其保持私有(使用现代运行时,我认为你可以在类扩展中定义ivar),并且只从一个对象处理它。如果你感觉更舒服,可以保留它,但这完全没有必要。

    • 注意:如果你绝对需要从另一个对象访问计时器,请使属性retain计时器(因为这是避免由直接使计时器失效的客户端引起的崩溃的唯一方法)并且提供自己的setter。在我看来,重新安排计时器不是打破封装的好理由:如果需要这样做,请提供一个mutator。
  2. 使用与self不同的目标设置计时器。(有很多方法可以这样做。也许通过编写通用的TimerTarget类或-如果可以使用-通过MAZeroingWeakReference?)

我为在第一次讨论中表现愚蠢而道歉,并感谢丹尼尔Dickison和Rob Napier的耐心。

所以这就是我从现在开始处理计时器的方式:

// NSTimer+D12WeakTimerTarget.h:
#import <Foundation/NSTimer.h>
@interface NSTimer (D12WeakTimerTarget)
+(NSTimer *)D12scheduledTimerWithTimeInterval:(NSTimeInterval)ti weakTarget:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat logsDeallocation:(BOOL)shouldLogDealloc;
@end

// NSTimer+D12WeakTimerTarget.m:
#import "NSTimer+D12WeakTimerTarget.h"
@interface D12WeakTimerTarget : NSObject {
    __weak id weakTarget;
    SEL selector;
    // for logging purposes:
    BOOL logging;
    NSString *targetDescription;
}
-(id)initWithTarget:(id)target selector:(SEL)aSelector shouldLog:(BOOL)shouldLogDealloc;
-(void)passthroughFiredTimer:(NSTimer *)aTimer;
-(void)dumbCallbackTimer:(NSTimer *)aTimer;
@end

@implementation D12WeakTimerTarget
-(id)initWithTarget:(id)target selector:(SEL)aSelector shouldLog:(BOOL)shouldLogDealloc
{
    self = [super init];
    if ( !self )
        return nil;

    logging = shouldLogDealloc;

    if (logging)
        targetDescription = [[target description] copy];

    weakTarget = target;
    selector = aSelector;

    return self;
}

-(void)dealloc
{
    if (logging)
        NSLog(@"-[%@ dealloc]! (Target was %@)", self, targetDescription);

    [targetDescription release];
    [super dealloc];
}

-(void)passthroughFiredTimer:(NSTimer *)aTimer;
{
    [weakTarget performSelector:selector withObject:aTimer];
}

-(void)dumbCallbackTimer:(NSTimer *)aTimer;
{
    [weakTarget performSelector:selector];
}
@end

@implementation NSTimer (D12WeakTimerTarget)
+(NSTimer *)D12scheduledTimerWithTimeInterval:(NSTimeInterval)ti weakTarget:(id)target selector:(SEL)selector userInfo:(id)userInfo repeats:(BOOL)shouldRepeat logsDeallocation:(BOOL)shouldLogDealloc
{
    SEL actualSelector = @selector(dumbCallbackTimer:);
    if ( 2 != [[target methodSignatureForSelector:aSelector] numberOfArguments] )
        actualSelector = @selector(passthroughFiredTimer:);

    D12WeakTimerTarget *indirector = [[D12WeakTimerTarget alloc] initWithTarget:target selector:selector shouldLog:shouldLogDealloc];

    NSTimer *theTimer = [NSTimer scheduledTimerWithTimeInterval:ti target:indirector selector:actualSelector userInfo:userInfo repeats:shouldRepeat];
    [indirector release];

    return theTimer;
}
@end

原文(全部公开):

你从你的其他帖子中已经知道了我的观点:

没有什么理由拥有一个定时器的所有权引用(bbum 似乎也同意)。

话虽如此,你的选项23本质上是相同的。(在[self setWalkTimer:nil]walkTimer = nil之间存在额外的消息传递,但我不确定编译器是否会优化它并直接访问 ivar,但是......)


是的,抱歉,2中的发布不应该存在(已编辑以删除) - fuzzygoat
谢谢,所以弱引用(2)或iVar(3)是最佳选择。 - fuzzygoat
好的,就像我说的:选项2和3本质上是相同的。如果你要使用选项2,那么请编写自己的setter来确保旧计时器将被作废 - 这样,你可以完全摆脱cancelTimer或将其DRY为self.walkTimer=nil - danyowdee

2

我通常在访问器内部处理无效操作,以便您不会在认为已经摆脱计时器后被其访问而感到惊讶:

@property(nonatomic, retain) NSTimer *walkTimer;
[self setWalkTimer: ti];

- (void)setWalkTimer:(NSTimer *)aTimer
{
    if (aTimer != walkTimer_)
    {
        [aTimer retain];
        [walkTimer invalidate];
        [walkTimer release];
        walkTimer = aTimer;
    }
}
...
...
// Cancel method
[self setWalkTimer:nil];
...
...
// Make a new timer, automatically invalidating the old one
[self setWalkTimer:[... a new timer ...]]
...
...
// dealloc method
[walkTimer_ invalidate];
[walkTimer_ release];
[super dealloc];

非常感谢,罗布。那么我可以从你之前和现在的评论中得出结论,你打算保留计时器? - fuzzygoat
2
是的。我总是保留ivars,除非有充分的理由不这样做(特别是委托)。这可以最大程度地减少调用者的猜测。 - Rob Napier

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