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 NSTimerthat
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
invalidateor
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
stopPollingis called or the instance is deallocated. You cannot assume that
stopPollingwill
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
EOCClassis 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
stopPollingbe
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
userInfoparameter
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
NSTimerclass 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
userInfoparameter. Finally, the timer is retained by the instance. However, the retain cycle can be broken through the
use of
weakreferences (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
selfvariable,
which is captured by the block instead of the normal
selfvariable. This
means that
selfwon’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
EOCClasshas 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
weakreference
ensures more safety; if the timer does run again for any reason, perhaps because you have forgotten to invalidate it during deallocation,
weakSelfwill be
nilonce
in the block.
Things to Remember
An
NSTimerobject
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
NSTimerto
use blocks can be used to break the retain cycle. Until this is made part of the public
NSTimerinterface, the functionality must be added through a category.
相关文章推荐
- The current .NET SDK does not support targeting .NET Core 2.1. Either target .NET Core 2.0 or lower, or use a version of the .NET SDK that supports .NET Core 2.1.
- Yet another round of biased MS marketing, only that this time its target is Eclipse.
- Android: Only the original thread that created a view hierarchy can touch its views 异常
- 『phphot』【SD2.0大会】30日10:00 毛新生:Google不是真正的IT公司
- How to build C++/CLI target to .net 2.0 without vs2008
- android开发中出现Only the original thread that created a view hierarchy can touch its views.错误的解决方法
- That's Just the Way It Is - How NT Describes I/O Requests
- ccah-500 第34题 you want to change a configuration parameter so that it affects all six DataNodes
- What Is Target Blank Anchor Tag Phishing Attack? How To Prevent It?
- There is no ViewData item of type 'IEnumerable<SelectListItem>' that has the key ''.
- 浅析Android中的消息机制-解决:Only the original thread that created a view hierarchy can touch its views.
- dialog子线程操作UI异常:Only the original thread that created a view hierarchy can touch its views
- Unity5.4.1与NGUI出现的问题Ignoring menu item NGUI because it is in no submenu!
- Activiti in Action(实战Activiti)-第一章 BPMN 2.0: what’s in it for developers?(1)
- 不能将多个项传入“Microsoft.Build.Framework.ITaskItem”类型的参数
- It is confused that the difference of JSP with Servlet
- It is also possible that a host key has just been changed. The fingerprint for the RSA key sent by t
- Unrecognized attribute 'targetFramework'. Note that attribute names are case-sensitive.(转)
- error: Error retrieving parent for item: No resource found that matches the given name 'Widget.AppCo
- Effective C++ 3nd 读书摘要(八、定制new和delete)Item49 - 52