Salman Paracha
Salman Paracha

Reputation: 1703

How do I properly dealloc and release objects being referred to in a while loop

The following code creates a memory leak. An asynchronous background process downloads images in tmp_pack_folder and another background thread is checking if the image count matches the total count expected, and then makes the images available to users once the download is complete.

The issue is that if the background process that is downloading images to the tmp_pack_folder fails for some reason, the following code becomes an infinite loop. This is a rare case, but when it does there is a memory leak. getAllFileNamesinFolder method is actually calling contentsOfDirectoryAtPath:bundleRoot of NSFileManager and it is called repeatedly. How to do I properly deallocate memory in this case (apart from preventing the infinite loop to begin with)

NSString *tmp_pack_folder = [packid stringByAppendingString:@"_tmp"];
if([fileMgr folderExists: tmp_pack_folder]){
    NSArray *packImages = [fileMgr getAllFileNamesInFolder:tmp_pack_folder];

    while(packImages.count != arrImages.count ){
        packImages = [fileMgr getAllFileNamesInFolder:tmp_pack_folder]; //get the contents of the folder again.
        if(cancel==YES){
            break;
        }
    }
}

Upvotes: 0

Views: 170

Answers (2)

danh
danh

Reputation: 62676

It's too bad Objective-C doesn't give us javascript-like promises. The way I solve this problem is by giving my asynch task a caller's interface like this:

- (void)doAsynchThingWithParams:(id)params completion:(void (^)(id))completion;

The params parameterize whatever the task is, and the completion handler takes result of the task.

This let's me treat several concurrent tasks like a todo list, with a completion handler that gets called with all the results once they've arrived.

// array is an array of params for each task e.g. urls for making url requests
// completion is called when all are complete with an array of results

- (void)doManyThingsWithParams:(NSArray *)array completion:(void (^)(NSArray *))completion {

    NSMutableArray *todoList = [array mutableCopy];
    NSMutableArray *results = [NSMutableArray array];

    // results will always have N elements, one for each task
    // nulls can be replaced by either good results or NSErrors
    for (int i=0; i<array.count; ++i) results[i] = [NSNull null];

    for (id params in array) {
        [self doAsynchThingWithParams:params completion:^(id result) {
            if (result) {
                NSInteger index = [array indexOfObject:params];
                [results replaceObjectAtIndex:index withObject:result];
            }
            [todoList removeObject:params];
            if (!todoList.count) completion(results);
        }];
    }
}

Upvotes: 0

Rob
Rob

Reputation: 437882

You say that you will rework this to "prevent the infinite loop." You should take that a step further and eliminate the loop altogether. If you ever find yourself with code that loops, polling some status, there's invariably an alternate, more efficient design. Bottom line, your memory situation is not the real problem: It's merely a symptom of a broader design issue.

I'd advise you move to an event-driven approach. So, rather than having a method that repeatedly performs the "am I done yet" logic, you should only check this status when triggered by the appropriate event (i.e. only when a download finishes/fails, and not before). This loop is probably causing to your memory problem, so don't fix the memory problem, but rather eliminate the loop altogether.

In answer to your question, one possible source of the memory problem arises from autorelease objects. These are objects that are allocated in such a manner that they are not released immediately when you're done with them, but rather only when the autorelease pool is drained (which generally happens for you automatically when you yield back to the app's run loop). But if you have some large loop that you repeatedly call, you end up adding lots of objects to an autorelease pool that isn't drained in a timely manner.

In some special cases, if you truly needed some loop (and to be clear, that's not the case here; you neither need nor want a loop in this case), you could employ your own custom @autoreleasepool, through which you'd effectively control the frequency of the draining of the pool.

But, at the risk of belaboring the point, this is simply not one of those situations. Don't use your own autorelease pool. Get rid of the loop. Only trigger the "am I done yet" logic when a download finishes/fails, and your problem should go away.

Upvotes: 2

Related Questions