objective-cautomatic-ref-countingobjective-c-blocksretain-cycle

Understanding a subtle retain cycle with blocks under ARC


I'm studying a code snippet I grabbed from Effective Objective-C book by Matt Galloway. The snippet is the following (I've modified a little bit).

- (void)downloadData {
    NSURL *url = // alloc-init
    NetworkFetcher *networkFetcher =
        [[NetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", networkFetcher.url);
        _fetchedData = data;
    }];
    // ARC will put a release call for the networkFetcher here
}

As stated by the author, such pattern is used by different networking libraries and there is a retain cycle. The retain cycle is quite obvious for me since, if you think in terms of object graph, the networkFetcher instance retains the block through a completionHandler property (copyied), while the block retains the networkFetcher since it uses it in NSLog.

Now, to break the block, the NetworkFetcher must set the completion handler to nil when it finishes to download the data has been requested.

// in NetworkFetcher.m class
- (void)requestCompleted {
    
    if(self.completionHandler) {
        // invoke the block
        self.completionHandler();
    }

    self.completionHandler = nil;
}

Ok. In this way there is no retain cycle anymore. The block, when run, it frees its reference to the networkFetcher and the networkFetcher makes nil the reference to the block.

Now, my question regards the execution flow of the snippet. Is the following sequence of actions correct?

  1. the networkFetcher runs the completion handler
  2. the block is executed
  3. the block frees the reference to the networkFetcher
  4. the networkFetcher release the reference to the block

My doubt relies on actions 3) and 4) . If 3) is executed before 4) no one has a reference to networkFetcher and so it can be released at any execution time (ARC will put a release call at the end of downloadData). Am I wrong or am I missing something?


Solution

  • // in NetworkFetcher.m class
    - (void)requestCompleted {
    
        if(self.completionHandler) {
            // invoke the block
            self.completionHandler();
        }
    
        self.completionHandler = nil;
    }
    

    The block is executed before it is set to nil. The execution of the block is synchronous in this method - nothing will happen until it has finished executing. Remember, the existence of a block does not mean that the code inside is going to be executed asynchronously.

    The block doesn't free it's references once it is executed, because the block still exists as a property of the network fetcher instance. You could execute it again, if you were a bit strange.

    The block only releases the objects it has captured when it is deallocated - which happens when the completionHandler property is set to nil, which is after the block has been executed.