0

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?

Hope the question it's clear.

Lorenzo B
  • 33,216
  • 24
  • 116
  • 190
  • But you don't need to have a retain cycle here at all. You know the block is only used by this network fetcher object and by no one else; thus the block can only be executed while the network fetcher object is alive (nobody else has access to it); thus the block should only have a weak reference to the network fetcher object, since it knows the network fetcher will always be alive when the block is run anyway. – newacct Apr 25 '14 at 09:23
  • @newacct thanks for your comment. Without a retain cycle the fetcher won't execute. Am I wrong? if the block has a `weak` reference to the fetcher, at the end of `downloadData` method the fetcher will be released. `weak` means `nil`ling the reference to the fetcher... – Lorenzo B Apr 25 '14 at 12:18
  • 1
    Not necessarily. You need to know who is responsible for keeping the `NetworkFetcher` alive; without the code it's impossible to tell. `startWithCompletionHandler:` starts an asynchronous operation, which needs to have a reference to `NetworkFetcher` to complete the operation. It is very possible that this is a strong reference (for example, `NSURLConnection`'s delegate is a strong reference, or the internal operation may have a completion block that strongly references the `NetworkFetcher`), and thus your code is not responsible for keeping `NetworkFetcher` alive. (continued) – newacct Apr 25 '14 at 18:44
  • 1
    This is similar to how for `UIAlertView`, you just show it, and don't need to hold a strong reference to it, but it still works. If, on the other hand, the asynchronous operation holds a weak reference to `NetworkFetcher` (IMO that's a weird design), then you need to somehow keep `NetworkFetcher` alive during the operation. But you still don't need to rely on `NetworkFetcher` to `nil` out the completion handler; instead you can have the block capture a `__block` reference to the `NetworkFetcher`, and set that to `nil` in the block when it's done. – newacct Apr 25 '14 at 18:44
  • @newacct thanks for comments. Could you share any lib or snippet that uses a similar pattern? Thanks – Lorenzo B Apr 25 '14 at 23:14
  • Anyway I understand what you mean...to summarize you need to know if you are responsible or not to keep the fetcher alive.. – Lorenzo B Apr 25 '14 at 23:21

2 Answers2

3
// 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.

jrturton
  • 118,105
  • 32
  • 252
  • 268
  • Perfectly agree with sync execution and the rest of your answer. So the fetcher releases the block which in turns releases the fetcher... – Lorenzo B Apr 20 '14 at 02:57
  • A final consideration. Without caputuring the network fetcher in the `NSLog` it will be released at the end of `dowloadData` method because of ARC. Am I right? So my question, is this a trick used in network libs? – Lorenzo B Apr 21 '14 at 19:31
  • Yes. A lot of them self-retain so that the caller doesn't have to explicitly hold a reference between kicking off the network call and waiting for the results. – jrturton Apr 21 '14 at 19:52
  • Logging it isn't the best approach though. Having a specific property which you assign yourself to shows the intent much more clearly. – jrturton Apr 21 '14 at 19:55
  • Nice. Very useful!! So in this case a strong property to reference the fetcher inside the block would be more useful than log...thanks for the support! – Lorenzo B Apr 21 '14 at 20:05
1

The steps are more like this:

  1. The network fetcher finishes the request and then calls the Block.
  2. The block executes

Nothing else happens. Since there is a retain cycle the network fetcher won't deallocate.

IFF the network fetcher explicitly sets its completion block ivar to nil after it called it, you get this:

Initially, the reference count of the completion block is +1.

  1. The network fetcher finishes the request and then calls the Block.
  2. The block executes asynchronously. (Release count of the block equals +1).
  3. The network fetcher sets the block to nil (release -1)
  4. The block finishes (release -1) and the block deallocates, and captured retainable variables get released (including the networkFetcher pointer).

There are a few other approaches to prevent a retain cycle:

- (void)downloadData {
    NSURL *url = // alloc-init
    NetworkFetcher *networkFetcher =
        [[NetworkFetcher alloc] initWithURL:url];
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", url);
        _fetchedData = data;
    }];
}


- (void)downloadData {
    NSURL *url = // alloc-init
    NetworkFetcher *networkFetcher =
        [[NetworkFetcher alloc] initWithURL:url];
    __block NetworkFetcher* blockNetworkFeatcher = networkFetcher;
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", blockNetworkFeatcher.url);
        _fetchedData = data;
        blockNetworkFeatcher = nil;
    }];
}

- (void)downloadData {
    NSURL *url = // alloc-init
    NetworkFetcher *networkFetcher =
        [[NetworkFetcher alloc] initWithURL:url];
    __weak NetworkFetcher* weakNetworkFeatcher = networkFetcher;
    [networkFetcher startWithCompletionHandler:^(NSData *data){
        NSLog(@"Request URL %@ finished", weakNetworkFeatcher.url);
        _fetchedData = data;
    }];
}
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • Thanks for your details. But I'm missing something on the async execution of the block. How can I be sure that 3 is executed always before 4? – Lorenzo B Apr 19 '14 at 15:28
  • "asynchronous" execution was just an example. If a block will be enqueued, it will be retained by the queue, and released again when the block finished. If the block will be executed synchronously, it will not be retained. In both cases (and iff the block ivar will be set to `nil` eventually), the block's retain count eventually reaches zero, and then the block will be deallocated. The order of 3. and 4. doesn't matter. It only matters that 3. and 4. release the block. – CouchDeveloper Apr 19 '14 at 16:23
  • I'm thinking about your samples. Which element is retaining the fetcher in order to avoid its dealloc? At the end of *downloadData* it will be dismissed due to ARC... – Lorenzo B Apr 22 '14 at 04:18