Effective Objective-C 2.0: Item 40: Avoid Retain Cycles Introduced by Blocks

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 allocinitWithString:
                  @"http://www.example.com/something.dat"];
    _networkFetcher =
        [[EOCNetworkFetcher allocinitWithURL: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.

Image

Figure 6.2 Retain cycle between the network fetcher and the class that owns it

This retain cycle can easily be fixed by breaking either the reference the _networkFetcherinstance 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 allocinitWithString:
                  @"http://www.example.com/something.dat"];
    EOCNetworkFetcher *networkFetcher =
        [[EOCNetworkFetcher allocinitWithURL: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

Image Be aware of the potential problem of retain cycles introduced by blocks that capture objects that directly or indirectly retain the block.

Image Ensure that retain cycles are broken at an opportune moment, but never leave responsibility to the consumer of your API.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值