4
votes

Here is a tricky question, to which the answer might prove useful for many networking apprentice out there, including me.

Some background information on the context:

  • Let's say you want to download data from an Online Service
  • you want to do this asynchronously
  • you want to do one download at a time, then maybe do another one upon completion of the previous one.

One neat way of doing this is using recursion. The problem with common implementations you can come up with is Retain cycles, between networking completion blocks and self. This can be solved using weakSelf reference pointers.

But, what about retain cycles for recursive calls ?

We have implemented a recursive stack, self pointing to a download management class, like this:

-(void)startNetworkDownloadForObjectAtIndex:(int) anIndex
{
    __typeof__(self) __weak weakSelf = self;
    NSURL *urlForObjectAtIndex = [SomeClass URLforIndex:anIndex];
    [self.downloadManager getResourceAtURL:urlForObjectAtIndex success:^(AFHTTPRequestOperation *operation, id responseObject) {
                               if (indexOfObjectToDownload < weakself.totalNumberOfObjectsToDownload) [weakSelf startNetworkDownloadForObjectAtIndex:indexOfObjectToDownload+1];
                               else [weakSelf startDOwnloadTimer]; 
                            }
                            failure:^(AFHTTPRequestOperation *operation, NSError *error) {
                                // response code is in operation.response.statusCode
                                [weakSelf handleNetworkError:error];
                            }];
}


-(void)handleNetworkError:error
{
     // Do some error handling
     [self startNetworkDownloadForObjectAtIndex:self.lastUnsentObjectIndex];
 }

-(void)startDownloadTimer
{
     if (self.syncEngineTimer) [self.syncEngineTimer invalidate];
     self.syncEngineTimer = [NSTimer scheduledTimerWithTimeInterval:kSyncTimeInterval
                                                        target:self
                                                    selector:@selector(restartNetworkDownload)
                                                      userInfo:nil
                                                       repeats:NO];
}

-(void)restartNetworkDownload
{
      // do some fancy calculations / etc to manage your download
     int anIndex = theResultOfYourCalculation;
     [self startNetworkDownloadForObjectAtIndex:anIndex];
}

OK, this is an example of a possible recursive call of several network downloads (get 100 flicker pictures for instance) and try to get the new ones after 1 hour, for instance. Excuse any coding typos.

We're running this under ARC for iOS devices above iOS 5.0

We obviously broke the first level of retain cycle by using a weakSelf reference pointer when using self.downloadManager keeping a reference to the success and failure completion block. This is all fine and goes well in instruments.

Now, when looking at allocation in Instruments, we launch a download operation for several downloads. Instruments shows no leaks. BUT when regurlarly saving the heap, you can see it slowly growing.

Checking allocations and having a look at the call stack, it definitely looks like the block is keeping a reference to self through the use of startDownloadTimer

Any explanation on a possible cause and solution will be greatly appreciated :)

1
This is overly complicated. You can schedule downloads on an operation queue and set a number of concurrent downloads, set the timeout in the network operation, and in the completion handler remove the URL from the pool of URLs to download. No recursion needed. NSTimer is prone to cycles, see for example gist.github.com/j4n0/5575264 for a safe usage. - Jano
@ChevenementDavid: There is no recursion here. The completion blocks are called from the event loop, not from the method that contains them in the source code. But otherwise, the question is relevant. - Codo
@codo, recursion is indeed there: you keep adding recursive calls to startDownloadingObjectAtIndex and only in the end of the recursion, you start the timer for instance. - Chevenement David
@Jano, sure you can manage paralleled downloads but my question strictly refers to the ability to serialize them one after the other. Recursion is a quite simple algorithm in this case - Chevenement David

1 Answers

1
votes

Your timer retains its target (the self).

Try the solution from this question: Weak Reference to NSTimer Target To Prevent Retain Cycle

Or use dispatch_after instead of timer.