Effective Objective-C 2.0:Item 52: Remember that NSTimer Retains Its Target

NSTimer 专栏收录该内容
1 篇文章 0 订阅

Item 52: Remember that NSTimer Retains Its Target

Timers are a useful object to have at your disposal. The Foundation framework contains a class called NSTimer that can be scheduled to run either at an absolute date and time or after a given delay. Timers can also repeat and therefore have an associated interval to define how frequently they should fire. You may use one to fire every 5 seconds to handle polling of a resource, for example.

Timers are associated with a run loop, and the run loop handles when it should fire. When a timer is created, it can either be prescheduled in the current run loop, or you can create it and schedule it yourself. Either way, the timer will fire only if it is scheduled in a run loop. For example, the method to create a timer that is prescheduled is as follows:

+ (NSTimer *)scheduledTimerWithTimeInterval:
                            (NSTimeInterval)seconds
                                     target:(id)target
                                   selector:(SEL)selector
                                   userInfo:(id)userInfo
                                    repeats:(BOOL)repeats

This method can be used to create a timer that fires after a certain time interval. Optionally, it can repeat until it is manually stopped at a later time. The target and the selector specify which selector should be called on which object when the timer fires. The timer retains its target and will release it when the timer is invalidated. A timer is invalidated either through a call to invalidate or when it fires. If a timer is set to repeat, you invalidate the timer when you want to stop it.

Because the timer retains its target, repeating timers can often cause problems in applications. This means that you can often get into a retain-cycle situation with repeating timers. To see why, consider this example:

#import <Foundation/Foundation.h>

@interface EOCClass : NSObject
- (void)startPolling;
- (void)stopPolling;
@end

@implementation EOCClass {
    NSTimer *_pollTimer;
}

- (id)init {
    return [super init];
}

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

- (void)stopPolling {
    [_pollTimer invalidate];
    _pollTimer = nil;
}

- (void)startPolling {
    _pollTimer =
    [NSTimer scheduledTimerWithTimeInterval:5.0
                                     target:self
                                   selector:@selector(p_doPoll)
                                   userInfo:nil
                                    repeats:YES];
}

- (void)p_doPoll {
    // Poll the resource
}

@end

Can you spot the problem here? Consider what happens if an instance of this class is created and polling is started. The timer is created, which retains the instance because the target is self. However, the timer is also retained by the instance because it is set as an instance variable. (Recall that with ARC, Item 30, this means that it is retained.) This sets up a retain cycle, which would be fine if the retain cycle were broken at some point. The only way it can be broken is if the instance variable is changed or the timer is invalidated. So the only way it is broken is if stopPolling is called or the instance is deallocated. You cannot assume that stopPolling will be called unless you control all the code that uses this class. Even then, it is not good practice to require that a method be called to avoid a leak. Also, there is a chicken-and-egg situation with the other way the timer is invalidated through deallocation. The instance will not be deallocated, because its retain count will never drop to zero while the timer is valid. And the timer will stay valid until it is invalidated. Figure 7.1 illustrates this.

Image

Figure 7.1 Retain cycle because timer retains its target, which in turn retains the timer

Once the final reference to an instance of EOCClass is removed, it will continue to stay alive, thanks to the timer retaining it. The timer will never be released, because the instance holds a strong reference to it. Worse still, this instance will be lost forever because there are no more references to it other than through the timer. But you don’t have any references to the timer other than through the instance. This is a leak. It’s a particularly bad leak because the polling will continue to occur forever. If polling is downloading data from a network, data will continue to be downloaded forever, further adding to the potential leak.

Little can be done to alleviate this problem by using timers on their own. You could mandate that stopPolling be called before all other objects release an instance. However, there is no way to check for this, and if the class forms part of a public API that you expose to other developers, you cannot guarantee that they will call it.

One way to solve this problem is to use blocks. Although timers do not currently support blocks directly, the functionality can be added like this:

#import <Foundation/Foundation.h>

@interface NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:
                           (NSTimeInterval)interval
                                     block:(void(^)())block
                                   repeats:(BOOL)repeats;

@end

@implementation NSTimer (EOCBlocksSupport)

+ (NSTimer*)eoc_scheduledTimerWithTimeInterval:
                           (NSTimeInterval)interval
                                     block:(void(^)())block
                                   repeats:(BOOL)repeats
{
    return [self scheduledTimerWithTimeInterval:interval
                                         target:self
                           selector:@selector(eoc_blockInvoke:)
                                       userInfo:[block copy]
                                        repeats:repeats];
}

+ (void)eoc_blockInvoke:(NSTimer*)timer {
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}

@end

The reason for doing this to solve the retain-cycle problem will become clear shortly. The block that is to be run when the timer fires is set as theuserInfo parameter of the timer. This is an opaque value that the timer retains while it is valid. A copy of the block needs to be taken to ensure that it is a heap block (see Item 37); otherwise, it may be invalid when we come to execute it later. The target of the timer is now the NSTimer class object, a singleton, and it therefore does not matter if it is retained by the timer. A retain cycle remains here, but since the class object never needs to be deallocated, it doesn’t matter.

On its own, this solution does not solve the problem but merely provides the tools with which to solve the problem. Consider changing the problematic code to use this new category:

- (void)startPolling {
    _pollTimer =
    [NSTimer eoc_scheduledTimerWithTimeInterval:5.0
                                          block:^{
                                              [self p_doPoll];
                                                 }
                                        repeats:YES];
}

If you think about this one carefully, you’ll note that there is still a retain cycle. The block retains the instance because it captures self. In turn, the timer retains the block through the userInfo parameter. Finally, the timer is retained by the instance. However, the retain cycle can be broken through the use of weak references (see Item 33):

- (void)startPolling {
    __weak EOCClass *weakSelf = self;
    _pollTimer =
    [NSTimer eoc_scheduledTimerWithTimeInterval:5.0
                                          block:^{
                               EOCClass *strongSelf = weakSelf;
                               [strongSelf p_doPoll];
                                                 }
                                        repeats:YES];
}

This code uses a useful pattern of defining a weak self variable, which is captured by the block instead of the normal self variable. This means thatself won’t be retained. However, when the block is executed, a strongreference is immediately generated, which will ensure that the instance is guaranteed to be alive for the duration of the block.

With this pattern, if the instance of EOCClass has its last reference to it from outside released, it will be deallocated. The invalidation of the timer during deallocation (check back to the original example) ensures that the timer will no longer run again. Using a weak reference ensures more safety; if the timer does run again for any reason, perhaps because you have forgotten to invalidate it during deallocation, weakSelf will be nil once in the block.

Things to Remember

Image An NSTimer object retains its target until the timer is invalidated either because it fires or through an explicit call to invalidate.

Image Retain cycles are easy to introduce through the use of repeating timers and do so if the target of a timer retains the timer. This may happen directly or indirectly through other objects in the object graph.

Image An extension to NSTimer to use blocks can be used to break the retain cycle. Until this is made part of the public NSTimer interface, the functionality must be added through a category.

  • 0
    点赞
  • 0
    评论
  • 0
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值