Reputation: 13085
I'm trying to download images asynchronously and store them in Core Data. Step one is to download a json file, parse it, and save an entity in Core Data for each object in the feed. That part is working fine.
Say I end up with 10 Bird objects in Core Data. Each Bird has a name, description, etc. and a to-many relationship with BirdImage, which is it's own entity. BirdImage has an "image_url" attribute (string) and an "image" attribute (Transformable).
Now, when I get to the screen in the app that will display the pictures of the birds, I first check the "image" attribute of the BirdImage. If it's not null, I just set whateverBirdEntity.image as the image of the UIImageView. If it is null, I need to download the image. In code that goes like this:
@property (nonatomic, strong) AssetRequest *assetRequest; //this is just a wrapper for an asset url, cache policy, and time out
@property (nonatomic, strong) NSURLRequest *assetURLRequest;
@property (nonatomic, strong) NSURLConnection *assetConnection;
@property (nonatomic, strong) NSMutableData *assetConnectionData;
@property (nonatomic, strong) BirdImage *imageEntity;
@property (nonatomic, strong) NSManagedObjectContext *objectContext;
...
- (void)load {
dispatch_async(dispatchQueue, ^{
//Check for the image in Core Data
self.objectContext = [[NSManagedObjectContext alloc]
initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.objectContext.parentContext = [[CoreDataController sharedController] managedObjectContext];
NSFetchRequest *fetch = [NSFetchRequest fetchRequestWithEntityName:@"BirdImage"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"image_url = %@", [self.assetRequest.assetURL absoluteString]];
[fetch setPredicate:predicate];
NSArray *objects = [self.objectContext executeFetchRequest:fetch error:nil];
if ([objects count] > 0)
{
BirdImage *birdImage = [objects objectAtIndex:0];
if (birdImage.image) {
dispatch_async(dispatch_get_main_queue(), ^{
BirdAsset *asset = [[BirdAsset alloc] init];
asset.url = [NSURL URLWithString:birdImage.image_url];
asset.image = birdImage.image;
if (self.successBlock)
self.successBlock(asset); //the caller will use asset.image for the UIImageView
});
return;
}else{
//no image found, need to download it
self.imageEntity = birdImage; //this is the entity I want to re-save in Core Data once the image finishes downloading
dispatch_async(dispatch_get_main_queue(), ^{
self.assetURLRequest = [NSURLRequest requestWithURL:self.assetRequest.assetURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:self.assetRequest.timeOut];
self.assetConnection = [[NSURLConnection alloc] initWithRequest:self.assetURLRequest delegate:self startImmediately:NO];
[self.assetConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.assetConnection start];
});
}
}
}];
});
}
Then, when the download completes:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
dispatch_async(dispatchQueue, ^{
UIImage *resultImage = [UIImage decompressImageFromData:self.assetConnectionData];
NSData *resultData = UIImagePNGRepresentation(resultImage);
DLog(@"saving to core data: %@", self.imageEntity.image_url); //THIS HAPPENS 10 TIMES (every time)
self.imageEntity.image = resultImage;
@try {
NSError *saveError = nil;
if (![self.objectContext save:&saveError])
NSLog(@"saveError %@", saveError);
}
@catch (NSException *exception) {
NSLog(@"Exception: %@", exception);
}
[[CoreDataController sharedController] saveContext];
BirdAsset *finalAsset = [[BirdAsset alloc] init];
finalAsset.data = resultData;
finalAsset.image = resultImage;
finalAsset.url = [NSURL URLWithString:self.imageEntity.image_url];
DLog(@"SUCCESS"); //THIS HAPPENS anywhere from 4-7 times. I never get all 10 images.
dispatch_async(dispatch_get_main_queue(), ^{
if (self.successBlock)
self.successBlock(finalAsset);
});
});
}
The images are downloading fine, and when I inspect my database I can see BLOB data for each BirdImage "image". The problem is that out of the 10 images, a random number of them actually get displayed (anywhere from 4-7 of them on first run). Then if I come back to this screen again, the app will lock up, with no error messages or crashes. I'm suspecting it's some sort of Core Data locking.
I know I must "access a context from the same thread that created it." But if I'm accessing the context in different methods (such as in the load and connectionDidFinishLoading methods above), how can I use the same thread? In other words, how can I modify my code so that I'm performing thread-safe CoreData context saving of the images when they finish downloading?
Upvotes: 1
Views: 545
Reputation: 10175
First of all I'm not sure that your images are downloaded on separate thread
dispatch_async(dispatch_get_main_queue(), ^{
self.assetURLRequest = [NSURLRequest requestWithURL:self.assetRequest.assetURL cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:self.assetRequest.timeOut];
self.assetConnection = [[NSURLConnection alloc] initWithRequest:self.assetURLRequest delegate:self startImmediately:NO];
[self.assetConnection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[self.assetConnection start];
});
dispatch_get_main_queue()
returns the main queue that is associated with the main thread so your [NSRunLoop currentRunLoop]
will return the main thread run loop which is not preaty good.
Second, it's not a good idea to save images in database as blob
because the database size will increase dramatically and your queries and other operation will take longer time to execute, so you should save them local (documents dir) or cache them for a period of time and save in the database only the path to the image.
Third, self.imageEntity = birdImage
this is not safe, it's possible that this line will be called multiple times and only one image is downloaded so you are loosing the reference to your entity and I think this is the main reason your images are not fully downloaded.
Fourth, you should rely use AFNetworking
and AFImageRequestOperation
this will take care of the async download and you can save the images by comparing the image URL with your entity URL.
Upvotes: 1
Reputation: 16946
NSManagedObjectContext
has a method called performBlock:
(or performBlockAndWait:
), which, unsurprisingly, takes a block. That block is then guaranteed to be executed on the context's thread. You could use it to your advantage by putting the code from connectionDidFinishLoading:
in a block that you pass to performBlock:
:
void (^contextBlock)() = ... // your code here
[self.objectContext performBlock:contextBlock];
If you need to do UI updates or perform code on a different thread, you can use dispatch_async
within that block as well.
Upvotes: 0