Item 40: Avoid Retain Cycles Introduced by Blocks Referencing the Object Owning Them
Blocks can very easily introduce retain cycles if they are not considered carefully. For example, the following class provides an interface for downloading a certain URL. A callback block, called a completion handler, can be set when the fetcher is started and is run when the download has finished. The completion handler needs to be stored in an instance variable in order to be available when the request-completion method is called.
// EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (id)initWithURL:(NSURL*)url;
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;
@end
// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy)
EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadedData;
@end
@implementation EOCNetworkFetcher
- (id)initWithURL:(NSURL*)url {
if ((self = [super init])) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion
{
self.completionHandler = completion;
// Start the request
// Request sets downloadedData property
// When request is finished, p_requestCompleted is called
}
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadedData);
}
}
@end
Another class may create one of these network fetcher objects and use it to download data at a URL, like this:
@implementation EOCClass {
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com/something.dat"];
_networkFetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", _networkFetcher.url);
_fetchedData = data;
}];
}
@end
This code looks fairly normal. But you may have failed to realize that a retain cycle is present. It stems from the fact that the completion-handler block references the self
variable because it sets the _fetchedData
instance variable (see Item 37 for more about captured variables). Thismeans that the EOCClass
instance that creates the network fetcher is retained by the block. This block is retained by the network fetcher, which is in turn retained by the same instance ofEOCClass
because it is held within a strong
instance variable. Figure 6.2 illustrates this retain cycle.
This retain cycle can easily be fixed by breaking either the reference the _networkFetcher
instance variable holds or the one the completionHandler
property holds. This break would need to be done when the completion handler has finished in the case of this network fetcher, so the network fetcher is alive until it has finished. For example, the completion-handler block could be changed to this:
[_networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request for URL %@ finished", _networkFetcher.url);
_fetchedData = data;
_networkFetcher = nil;
}
This retain-cycle problem is common in APIs that make use of completion callback blocks and is therefore important to understand. Often, the problem can be solved by clearing one of the references at an opportune moment; however, it cannot always be guaranteed that the moment will occur. In the example, the retain cycle is broken only if the completion handler is run. If the completion handler was never run, the retain cycle would never be broken, and leaks would occur.
Another potential retain cycle can be introduced with the completion-handler block approach. This retain cycle occurs when the completion-handler block references the object that ends up owning it. For example, to extend the previous example, instead of the consumer having to keep a reference to the network fetcher while it is running, it has a mechanism for staying alive itself. The network fetcher may do this by adding itself to a global collection, such as a set, when it is started and removing itself when it finishes. The consumer could then change its code to the following:
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com/something.dat"];
EOCNetworkFetcher *networkFetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data){
NSLog(@"Request URL %@ finished", networkFetcher.url);
_fetchedData = data;
}];
}
Most networking libraries use this kind of approach, as it is annoying to have to keep the fetcher object alive yourself. An example is the TWRequest
object from the Twitter framework. However, as the code for EOCNetworkFetcher
stands, a retain cycle remains. It is more subtle than before, though, and stems from the fact that the completion-handler block references the request itself. The block therefore retains the fetcher, which in turn retains the block through thecompletionHandler
property. Fortunately, the fix is simple. Recall that the completion handler was being kept in a property only so that it could be used later. The problem is that once the completion handler has been run, it no longer needs to hold onto the block. So the simple fix is to change the following method:
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadedData);
}
self.completionHandler = nil;
}
The retain cycle is then broken once the request has completed, and the fetcher object will be deallocated as necessary. Note that this is a good reason to pass the completion handler in thestart
method. If instead the completion handler were exposed as a public property, you couldn’t just go and clear it when the request completed, as that would break the encapsulation semantics you have given the consumer by saying that the completion handler is public. In this case, the only way to break the retain cycle sensibly is by enforcing that the consumer clear thecompletionHandler
property in the handler itself. But that is not very sensible, because you cannot assume that a consumer will do this and will instead blame you for the leaks.
Both of these scenarios are not uncommon. They are bugs that are easy to creep in when using blocks; similarly, they are just as easy to mitigate if you are careful. The key is to think about what objects a block may capture and therefore retain. If any of these can be an object that retains the block, either directly or indirectly, you will need to think about how to break the retain cycle at the correct moment.
Things to Remember
Be aware of the potential problem of retain cycles introduced by blocks that capture objects that directly or indirectly retain the block.
Ensure that retain cycles are broken at an opportune moment, but never leave responsibility to the consumer of your API.