您的位置:首页 > 移动开发 > Objective-C

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

2013-12-15 20:28 501 查看


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:

Click here to view code image

+ (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:

Click here to view code image

#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.



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:

Click here to view code image

#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 the
userInfo
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:

Click here to view code image

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

Click here to view code image

- (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 that
self
won’t be retained. However, when the block is executed, a
strong
reference
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


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


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.


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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐