Thoughts on creating a self-retained network lib (under ARC)
In my daily work I’m now involved in JavaScript mobile development but I also want to stay updated with Objective-C. During my train travels I read a lot but sometimes, due to the uproar or to my tiredness, I miss some important concepts. Currently I’m reading Effective Objective-C 2.0 by Matt Galloway. A book I really suggest to everyone. It is challenging and contains a lot of concepts.
A section of the above mentioned book talks about avoiding retain cycles introduced by block referencing the object owning them. In particular, the author introduces a subtle retain cycle that it is not immediate to grasp. The code looks like the following.
- (void)downloadData { NSURL *url = [NSURL URLWithString:@"someurlhere"]; NetworkFetcher *networkFetcher = [[NetworkFetcher alloc] initWithURL:url]; [networkFetcher startWithCompletionHandler:^(NSData *data){ NSLog(@"Request URL %@ finished", networkFetcher.url); _fetchedData = data; }]; // Here with ARC a release call will be inserted for the networkFetcher }
If you think in terms of object graph, the retain cycle is clear. The networkFetcher
instance retains
the block through a completionHandler
copyied property (I’m skipping internal details here),
while the block retains the networkFetcher
since it uses it in NSLog
.
Breaking the retain cycle it means setting the completionHandler
property to nil when the request has been fulfilled.
The block will release the object it has captured, i.e. the networkFetcher
,
when it is deallocated - which happens when the completionHandler
property is
set to nil. So, within the NetworkFetcher
class you need to just set as follows.
// in NetworkFetcher - (void)requestCompleted { if(self.completionHandler) { // invoke the block with data self.completionHandler(self.downloadedData); } // break the retain cycle self.completionHandler = nil; }
So, what about creating a self-retained network lib? As stated by Matt Galloway the code snippet
contained in downloadData
method is an approach that network libraries use to keep themselves alive.
But here my question. What if I remove the line responsible for the retain cycle (i.e. the NSLog
one)?
Under ARC, a objc_release
call will be inserted at the end of downloadData
method. Hence, the request will not be performed. Since the author does not provide further details I’ve tried to think on it.
How does this approach work? I ended up with the following code.
// NetworkRequest.h typedef void(^NetworkRequestCompletionHandler)(NSData *data); @interface NetworkRequest : NSObject - (id)initWithURL:(NSURL*)url; - (void)performWithCompletionHandler:(NetworkRequestCompletionHandler)completionHandler; @end // NetworkRequest.m #import "NetworkRequest.h" @interface NetworkRequest () @property (nonatomic, copy) NetworkRequestCompletionHandler completionHandler; @property (nonatomic, strong) NetworkRequest *selfRef; @property (nonatomic, strong) NSData *downloadedData; @end @implementation NetworkRequest - (void)dealloc { NSLog(@"Hey!! NetworkRequest deallocated"); } - (id)initWithURL:(NSURL*)url { if(self = [super init]) { _url = url; } return self; } - (void)performWithCompletionHandler:(NetworkRequestCompletionHandler)completionHandler { self.completionHandler = completionHandler; // self.selfRef = self; __weak NetworkRequest* weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [weakSelf internal_requestCompleted]; }); } - (void)internal_requestCompleted { NSLog(@"Hey!! NetworkRequest completed"); if(self.completionHandler) { self.completionHandler(self.downloadedData); } self.completionHandler = nil; // self.selfRef = nil; }
Within performWithCompletionHandler
method I set the completionHandler
for reusing
it later and I create a sort of fake async request. Here, the using of weak
is very important since
the dispatch_after cannot retain self. Otherwise the experiment will fail (try your own).
If now you run a NetworkRequest
using the following code
// how to use NetworkRequest class NSURL *urlRequest = [NSURL URLWithString:@"someurlhere"]; NetworkRequest *request = [[NetworkRequest alloc] initWithURL:urlRequest]; [request performWithCompletionHandler:^(NSData *data) { NSLog(@"Hello world!!!"); }];
you will notice that Hey!! NetworkRequest deallocated is logged as soon the app is run. This due to ARC. On the contrary,
if you remove the comments to self.selfRet
lines the code will work as expected.
A final note is that if you don’t use ARC pay attention to retain cycles…
Yep!! Goal achieved!!
Useful references: Effective Objective-C 2.0