iosfreak
iosfreak

Reputation: 5238

Correct way to multithread in objective-c?

I have a UITableView which displays images. Every cell has an image and every time a cell loads, I call a selector (from the cellForRowAtIndexPath) in the background like this:

[self performSelectorInBackground:@selector(lazyLoad:) withObject:aArrayOfData];

The only problem is that sometimes I get a crash (because I am changing data in the background while it's trying to be read elsewhere). Here's the error:

*** Terminating app due to uncaught exception 'NSGenericException', reason: '*** Collection <CALayerArray: 0xce1a920> was mutated while being enumerated.'

When updating the data in the background, should I move it to the main selector and change it? Or should I call the @selector() differently?

Thanks!

Upvotes: 4

Views: 3532

Answers (3)

davehayden
davehayden

Reputation: 3484

You can use @synchronized blocks to keep the threads from walking over each other. If you do

@synchronized(array)
{
  id item = [array objectAtIndex:row];
}

in the main thread and

@synchronized(array)
{
  [array addObject:item];
}

in the background, you're guaranteed they won't happen at the same time. (Hopefully you can extrapolate from that to your code—I'm not sure what all you're doing with the array there..)

It seems, though, like you'd have to notify the main thread anyway that you've loaded the data for a cell (via performSelectorOnMainThread:withObject:waitUntilDone:, say), so why not pass the data along, too?

Upvotes: 1

endy
endy

Reputation: 3872

Given the term 'lazy load' I am assuming that means you are pulling your images down from a server. (If the images are local then there is really no need for multithreading).

If you are downloading images off a server I would suggest using something along these lines (using ASIHTTPRequest)

   static NSCache *cellCache; //Create a Static cache

    if (!cellCache)//If the cache is not initialized initialize it
    {
        cellCache = [[NSCache alloc] init];
    }
    NSString *key = imageURL;
    //Look in the cache for image matching this url
    NSData *imageData = [cellCache objectForKey:key];

    if (!imageData)
    {
        //Set a default image while it's loading
        cell.icon.image = [UIImage imageNamed:@"defaultImage.png"];'

        //Create an async request to the server to get the image
        __unsafe_unretained ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:imageURL]];


        //This code will run when the request finishes
        [request setCompletionBlock:^{
            //Put downloaded image into the cache
            [cellCache setObject:[request responseData] forKey:key];
            //Display image
            cell.icon.image = [UIImage imageWithData:[request responseData]];
        }];
        [request startAsynchronous];
    }
    else 
    {
        //Image was found in the cache no need to redownload
        cell.icon.image = [UIImage imageWithData:imageData];
    }

Upvotes: 0

Dan Rosenstark
Dan Rosenstark

Reputation: 69757

If you can leave the operation on the main thread and have no lagginess nor problems you are done.

However: Let's assume you've already done that and encounter problems. The answer is: don't modify the array in the lazy load. Switch to the main thread to modify the array. See Brad's answer here:

https://stackoverflow.com/a/8186206/8047

for a way to do it with blocks, so you can send your objects over to the main queue (you should probably also use GCD for the call to the lazy load in the first place, but it's not necessary).

Upvotes: 3

Related Questions