Multi-threading using SDWebImage causes NSManagedObjects to appear twice - ios

I have the following problem. I'm currently building a recipe-based app and use a standard tableview to display the recipes. On the app launch, a file is downloaded from a server which includes recipe data. The data is then entered into CoreData making sure, that it's unique. At the same time, I use SDWebImage in cellForRowAtIndexPath in order to load async. After downloading the images I save the images and save their paths in an Recipe object (it's an NSManagedObject). I also use UIRefreshControl in order to refresh the tableview in case there had been changes on the server (it uses the same mechanism as on app launch).
The issue with the code below is the saveImage function. The line recipe.thumbImage = [NSString...] causes issues with CoreData in that when I refresh the app using the pull-down gesture (thus activating UIRefreshControl), the recipes appear twice in the tableView. If I delete the line, the problems go away.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
...
NSString *imageURL = [#"http://www.example.com" stringByAppendingString:recipe.externalThumbImageURL];
[cell.thumbImageView setImageWithURL:[NSURL URLWithString:imageURL]
placeholderImage:[UIImage imageNamed:#"blankTableviewImage"]
completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType) {
if(error) NSLog(#"Error when downloading thumbImage, error: %#", error);
else {
[self saveImage:image forRecipeID:recipe.objectID];
}
}];
}
- (void)saveImage:(UIImage *)image forRecipeID:(NSManagedObjectID *)recipeID
{
if(image) {
ImageHandler *imageHandler = [[ImageHandler alloc] init];
NSError *error;
Recipe *recipe = (Recipe *)[self.recipeDatabase.managedObjectContext existingObjectWithID:recipeID error:&error];
NSString *fileName = [imageHandler getImageName:recipe.externalThumbImageURL];
NSString *localImageDirectory = [imageHandler imageDirectoryPathFromRecipe:recipe];
if (![[NSFileManager defaultManager] fileExistsAtPath:localImageDirectory]) {
[[NSFileManager defaultManager] createDirectoryAtPath:localImageDirectory withIntermediateDirectories:NO attributes:nil error:&error];
}
[imageHandler saveImage:image withFileName:fileName ofType:#"png" inDirectory:localImageDirectory];
recipe.thumbImage = [localImageDirectory stringByAppendingString:[NSString stringWithFormat:#"%#.png", fileName]];
if(error) NSLog(#"Error in saveImage – error: %#", error);
}
}
The function [cell.thumbImageView setImageWithURL:...] is part of SDWebImage and works async as far as I know. And I think that's part of the problem. And I have tried wrapping recipe.thumbImage in a [recipe.managedObjectContext performBlock:^{}] block, but it doesn't help either.
Does anyone have a hint as to what is the cause of the problem? I know that threading with CoreData is tricky and I've tried several things in order to make it work, but nothing has worked so far. Any hints are very much appreciated!

After some additional trial and error and reading I found out that it makes sense to create a child context when you do that kind of async updating. So in the saveImage method I now simply create a child context and wrap the recipe.thumbImage in a [context performBlock:^{}]. As I use UIManagedDocument with CoreData, you have to make sure to write the changes to the file.
Here's the code
- (void)saveImage:(UIImage *)image forRecipeID:(NSManagedObjectID *)recipeID
{
if(image) {
NSError *error;
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
context.parentContext = self.document.managedObjectContext;
Recipe *recipe = (Recipe *)[context existingObjectWithID:recipeID error:&error];
...
[context performBlock:^{
recipe.thumbImage = [localImageDirectory stringByAppendingString:[NSString stringWithFormat:#"%#.png", fileName]];
[context save:nil];
[self.document saveToURL:document.fileURL forSaveOperation:UIDocumentSaveForOverwriting completionHandler:nil];
}];
}
}
Here's also a good thread with more comments on multi-threading and core data and here an in-depth post

Related

iOS Core Data saveContext method is unsuccessful due to NSSQLiteErrorDomain = 1032

Some background for this issue, I'm trying to include what I think may be relevant to help understand the context.
I am currently adding an linked library which used Core Data to save some user information and a feature which adds an Entity to the pre-existing Core Data model already in the app. Each managedObjectContext has its own instance when created (verified) as well as its own PSC and MOM and neither interact with the other's entities(thus seem to be independent).
The entirety of the following code, errors, and (I believe issue) is in the Main Target of the app. (Hopefully) not the newly added linked library.
The saveContext method is:
- (void)saveContext {
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;
// Register
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(myManagedObjectContextDidSaveNotificationHandler:) name:NSManagedObjectContextDidSaveNotification object:self.managedObjectContext];
if (self.managedObjectContext != nil) {
if ([self.managedObjectContext hasChanges]) {
BOOL success = [self.managedObjectContext save:&error];
if (!success) {
[Error showErrorByAppendingString:NSLocalizedString(#"UnableToSaveChanges", nil) withError:error];
} else {
//
}
}
}
// Unregister
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.managedObjectContext];
});
}
When called, error = nil, success = NO and by forcing the compiler past the exception I get the following:
CoreData: error: exception during obtainPermenantIDsForObjects: Updating max pk failed: attempt to write a readonly database with userInfo of { NSSQLiteErrorDomain = 1032;
}
I have googled, "NSSQLiteErrorDomain = 1032", "obtainPermenantIDsForObjects", and "CoreData readonly database". It does appear that the key primary key for each object is the same, but I am setting that value, I believe sqlite is. I have not found any solutions to help with this. I do have the argument passed on launch, "Concurrency Debug 1" set to on.
I have not implemented obtainPermenantIDsForObjects and I've searched the whole project and cant find its implementation so I think CoreData is using this.
The saveContext method is called on the main queue because thats how my predecessors rolled out the code and I don't have time at the moment to deal with it.
The method calling saveContext (from a background thread):
- (NSMutableArray *)convertRawStepDataTo:(NSMutableArray*)steps
withDates:(NSMutableArray*)dates
inManagedObjectContext:(NSManagedObjectContext*)theMOC {
NSMutableArray *theStepsArray = [[NSMutableArray alloc] init];
// prepare values for chart
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
StepSelector *theSelector = [[StepSelector alloc] init];
NSString* apiSelectionForStep = [theSelector getCurrentSelectionString];
for (int iter = 0; iter < steps.count; iter++) {
NSNumber *currStepValue = [steps objectAtIndex:iter];
// NSNumber *stepCountforIter = [NSNumber numberWithLong:[[steps objectAtIndex:iter] longValue]];
NSNumber* dateForIter = [NSNumber numberWithLong:[[dates objectAtIndex:iter] longLongValue]];
Step *step = [delegate addStepObjectToPersistentStorewithAPI:apiSelectionForStep
andStep:stepCountforIter
andDate:dateForIter
forMOC:theMOC];
[theStepsArray addObject:step];
if (VERBOSE) {
NSLog(#"This is step number %d, with object ID: %#", count, [theMOC objectWithID:step.objectID]);
count++;
}
}
[delegate saveContext];
return theStepsArray;
}
Thats all I can think that might help. The source for the MOC in the main target is the appDelegate which is where all the core data code was written initially.
EDIT Here is the requested PSC code. The store is located in the documents directory. I discovered that these objects are being saved to the Persistent Store.. but the error is still occurs. Se below for PSC code:
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator {
if (persistentStoreCoordinator != nil) {
return persistentStoreCoordinator;
}
NSURL *storeUrl = [self getStoreURL];
// Rollback journalling mode...
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES],NSInferMappingModelAutomaticallyOption,
NSFileProtectionComplete, NSFileProtectionKey,
#{#"journal_mode": #"TRUNCATE"}, NSSQLitePragmasOption, nil];
persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
NSError *error = nil;
self.persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeUrl options:options error:&error];
if (!self.persistentStore) {
NSLog(#"Error: %#",error);
[Error showErrorByAppendingString:NSLocalizedString(#"UnableToFindDatabaseFile", nil) withError:error];
}
return persistentStoreCoordinator;
}
-(NSURL *)getStoreURL {
NSString *storePath = [[self applicationDocumentsDirectory] stringByAppendingPathComponent: kSQLFILENAME];
/*
Set up the store.
For the sake of illustration, provide a pre-populated default store.
*/
NSFileManager *fileManager = [NSFileManager defaultManager];
// If the expected store doesn't exist, copy the default store.
if (![fileManager fileExistsAtPath:storePath]) {
NSString *defaultStorePath = [[NSBundle mainBundle] pathForResource:SQLFILEPATHRESOURCE ofType:#"sqlite"];
if (defaultStorePath) {
[fileManager copyItemAtPath:defaultStorePath toPath:storePath error:NULL];
}
}
NSURL *storeUrl = [NSURL fileURLWithPath:storePath];
return storeUrl;
}
The NSSQLiteErrorDomain key means that this error came from SQLite, and that Core Data is passing it back to you. SQLite defines error 1032 as follows:
The SQLITE_READONLY_DBMOVED error code is an extended error code for SQLITE_READONLY. The SQLITE_READONLY_DBMOVED error code indicates that a database cannot be modified because the database file has been moved since it was opened, and so any attempt to modify the database might result in database corruption if the processes crashes because the rollback journal would not be correctly named.
...which appears to mean that SQLite is making the persistent store file read only because something has happened to it since it was opened, and SQLite is trying to prevent data corruption.
I don't see anything in the code you've posted that is obviously at fault, at least as far as the error code description goes. So I wonder, are you doing anything anywhere else that would directly affect the persistent store file (i.e. touching the file in any way at all instead of going through Core Data fetch/save calls)?
The mention of the rollback journal in the error code description makes me wonder if setting journal_mode to TRUNCATE is related. If it were me, I'd remove that (I don't know what it's intended to accomplish here) or set it to DELETE. At least for testing purposes, anyway, in the hope of understanding the problem better.

Click To Add Image To Core Data

I'm working on an app that using core data, and everything is working so far. But I haven't used an image in it yet. I would like to do so but I don't know where to start. I would like to have the user click a button that gives them the option of locations of where they can get images. Also, is it possible to have a user just enter the URL of an image and have it save to core data? Thats really it so if someone could point me in the right direction I would appreciate it. Thanks!
My code so far: https://github.com/jackintosh7/Core-Data
EDIT - I updated some of the references in the saveImgData:fromURL to match the parameters passed in the method. I used a variation of this method within an NSURL protocol to intercept requests and cache specific ones. Some of the entity parameters may not apply to your question. Just disregard those (such as encoding/lastModified/mimeType/response).
To save an image to CoreData give this a try.
First, download your image. A user can enter the url in a text entry field. (The example listed by andrewbuilder will work just fine)
NSURL *imgURL = [NSURL URLWithString:myImgURL];
NSData *imgData = [NSData dataWithContentsOfURL:imgURL];
//a better way would be to do this asynchronously. Google "Lazy Image Loading" and
//you should find a suitable example app from Apple
Setup your CoreData entity to store the image data. Here is a sample that stores more than just the image binary data.
Then save your image data directly to CoreData.
I have a CoreData manager class but you may have your CoreData stack in AppDelegate which is how Apple sets them up.
-(void)saveImgData:(NSData*)myImgData fromURL:(NSString*)urlStr{
//If you have a CoreData manager class do something like this
CoreDataManager *cdm = [CoreDataManager defaultManager];
//Use private queue concurrency type for background saving
NSManagedObjectContext *ctx = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
//Set up the parent context, in this case is the mainMOC
//If you are using basic CoreData it would be the managedObjectContext on your AppDelegate
ctx.parentContext = cdm.mainMOC;
[ctx performBlock:^{
//performBlock executes on a background thread
CachedURLResponse *cacheResponse = [NSEntityDescription insertNewObjectForEntityForName:#"CachedURLResponse" inManagedObjectContext:ctx];
cacheResponse.relativeURLHash = [NSNumber numberWithInteger:[urlStr hash]];
cacheResponse.data = [myImgData copy];
cacheResponse.response = [NSKeyedArchiver archivedDataWithRootObject:self.response];
cacheResponse.url = [urlStr copy];
cacheResponse.timestamp = [NSDate date];
cacheResponse.mimeType = [self.response.MIMEType copy];
cacheResponse.encoding = [self.response.textEncodingName copy];
if ([self.headers objectForKey:#"Last-Modified"])
cacheResponse.lastModified = [self.headers objectForKey:#"Last-Modified"];
NSError *__block error;
if (![ctx save:&error])
NSLog(#"Error, Cache not saved: %#", error.userInfo);
[cdm.mainMOC performBlockAndWait: ^{
[cdm saveContext];
}];
}];
}
My CoreDataManager.h looks like this:
and my CoreDataManager.m file looks like this:
#import "CoreDataManager.h"
#import <CoreData/CoreData.h>
#implementation CoreDataManager
#synthesize mainMOC = _mainMOC,
managedObjectModel = _mom,
persistentStoreCoordinator = _psc
;
+(CoreDataManager*)defaultManager
{
static CoreDataManager *_defaultMgr = nil;
static dispatch_once_t oncePred;
dispatch_once(&oncePred, ^{
_defaultMgr = [[CoreDataManager alloc] init];
});
return _defaultMgr;
}
-(id)init
{
if (self = [super init])
{
NSError *error = nil;
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"MyDatabase.sqlite"];
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"MyDataModelName" withExtension:#"momd"];
_mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
_psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_psc addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
_mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainMOC setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return self;
}
- (void)saveContext
{
NSError *error = nil;
if (self.mainMOC != nil && ([self.mainMOC hasChanges] && ![self.mainMOC save:&error])) {
NSLog(#"CoreData save error: %#, %#", error, [error userInfo]);
}
}
#pragma mark - Core Data Stack
- (NSManagedObjectContext *)mainMOC
{
return _mainMOC;
}
- (NSManagedObjectModel *)managedObjectModel
{
return _mom;
}
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
return _psc;
}
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
#end
Hope that gives you a good starting point. Google "CoreData Class Reference" for some really good reading on the topic.
I learnt a lot from Paul Hegarty's excellent iTunes U / Stanford Uni lectures... his latest is called "Developing iOS Apps for iPhone and iPad" A couple of his lectures specifically address the use of images in Core Data. For what you are wanting to do I recommend you watch lectures 10-13, but really it is worth watching the entire series.
Beyond that, spend some time to read through the Apple Documentation on Core Data, starting here. Samir writes a good response to this stack overflow question.
You have a few options in how you implement the code. While Core Data is capable of managing the saving of images into the structure of your persistent data store (e.g. sqlite), it is important to note that large image files will slow down the fetch process.
Core Data can manage how to store the image for you... by clicking on the Allows External Storage option in the Data Model Inspector... see attached image below.
If however, you are looking to point a user to a URL, it may be worth considering adding (another separate) attribute to your entity Item, for example an attribute named pictureURL.
Use a property in a UIViewController to manage the entity attribute...
#property (nonatomic, retain) NSString * pictureURL;
Then you can add a UITextField to a UIViewController to accept user input for this property pictureURL.
When you have a URL for an image, you can return the image with "getter" code similar to this (where the UIImage is a property of the UIViewController)...
- (UIImage *)image {
NSURL *imageURL = [NSURL URLWithString:self.pictureURL];
NSData *imageData = [NSData dataWithContentsOfURL:imageURL];
return [UIImage imageWithData:imageData];
}
Hope that provides you with some direction.

Core Data is not persisting data between app launches

I have a app that is using core data. The Core Data stack (the context, object graph, persistent store coordinator, and the persistent store) is being created, and I am able to use it without issue. The problem is that the saved data is not persisting, can someone help me with what I am doing wrong? Here is where I create the Core Data stack.
- (void)initializeCoreDataStack
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Favorites"
withExtension:#"momd"];
if (!modelURL)
NSLog(#"MODEL URL NOT INITIALIZED");
NSManagedObjectModel *mom = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
if (!mom)
NSLog(#"OBJECT MODEL NOT CREATED");
NSPersistentStoreCoordinator * psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
if (!psc)
NSLog(#"PERSISTENT STORE COORDINATOR NOT CREATED");
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[moc setPersistentStoreCoordinator:psc];
self.managedObjectContext = moc;
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
NSArray *directory = [[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory
inDomains:NSUserDomainMask];
NSURL *storeUrl = [directory lastObject];
storeUrl = [storeUrl URLByAppendingPathComponent:#"Favorites.sqlite"];
NSError *error = nil;
NSPersistentStore *store = [psc addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:storeUrl
options:nil
error:&error];
if (!store)
{
NSLog(#"ERROR CREATING STORE: %# %#", error.localizedDescription, error.domain);
// present error to user
}
else
{
dispatch_sync(dispatch_get_main_queue(), ^{
// do something once the stack is finished being created
NSLog(#"persistent store created");
});
}
});
}
You have to save core data explicitly, otherwise it won't persist. Not hard to solve, though.
In your controller implementation file (e.g. coreDataViewController.m), call this function when you want to save changes to core data
// add this call, whenever you want to save data
// e.g. responding to a UIButton event
[self saveCoreDataContext];
- (void)saveCoreDataContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
My personal experience is that, even if you called [moc save:error], you may not find the content saved while you are running the app from Xcode -> Build and Run on device. However, if you stop the Xcode from running the app, and launch the app from the device by clicking the App Icon, the content is actually persisted.
Just personal experience that I found through trial and error, hope that you see the same thing.
You have to save the MOC.
[mom save:nil];
There's a few things here that stand out to me as odd. First, why are you threading the creation of the NSPersistentStore? Generally, I create the NSPersistentStoreCoordinator, add NSPersistentStores, and then create the contexts. I would suggest doing it in that order unless you have a good reason to do otherwise. While it's not specifically prohibited, you may experience problems adding a NSPersistentStore after the NSManagedObjectContext has been created.
I'm not sure if it is required or not, but I've always explicitly held a strong reference to the NSPersistentStoreCoordinator. It's useful to create multiple contexts, as well. I would suggest doing so in your code.
As everyone else has said, you also need to explicitly save.

Using AFNetworking NSOperations to download a number of files serially.....runs out of memory

Note: I'm using ARC.
I have some code that makes 1 request to an http server for a list of files (via JSON). It then parses that list into model objects which it uses to add a download operation (for downloading that file) to a different nsoperationqueue and then once it's done adding all of those operations (queue starts out suspended) it kicks off the queue and waits for all the operations to finish before continuing. (Note: this is all done on background threads so as not to block the main thread).
Here's the basic code:
NSURLRequest* request = [NSURLRequest requestWithURL:parseServiceUrl];
AFHTTPRequestOperation *op = [[AFHTTPRequestOperation alloc] initWithRequest:request];
op.responseSerializer = [AFJSONResponseSerializer serializer];
[op setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
//NSLog(#"JSON: %#", responseObject);
// Parse JSON into model objects
NSNumber* results = [responseObject objectForKey:#"results"];
if ([results intValue] > 0)
{
dispatch_async(_processQueue, ^{
_totalFiles = [results intValue];
_timestamp = [responseObject objectForKey:#"timestamp"];
NSArray* files = [responseObject objectForKey:#"files"];
for (NSDictionary* fileDict in files)
{
DownloadableFile* file = [[DownloadableFile alloc] init];
file.file_id = [fileDict objectForKey:#"file_id"];
file.file_location = [fileDict objectForKey:#"file_location"];
file.timestamp = [fileDict objectForKey:#"timestamp"];
file.orderInQueue = [files indexOfObject:fileDict];
NSNumber* action = [fileDict objectForKey:#"action"];
if ([action intValue] >= 1)
{
if ([file.file_location.lastPathComponent.pathExtension isEqualToString:#""])
{
continue;
}
[self downloadSingleFile:file];
}
else // action == 0 so DELETE file if it exists
{
if ([[NSFileManager defaultManager] fileExistsAtPath:file.localPath])
{
NSError* error;
[[NSFileManager defaultManager] removeItemAtPath:file.localPath error:&error];
if (error)
{
NSLog(#"Error deleting file after given an Action of 0: %#: %#", file.file_location, error);
}
}
}
[self updateProgress:[files indexOfObject:fileDict] withTotal:[files count]];
}
dispatch_sync(dispatch_get_main_queue(), ^{
[_label setText:#"Syncing Files..."];
});
[_dlQueue setSuspended:NO];
[_dlQueue waitUntilAllOperationsAreFinished];
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
callback(error);
}];
[_parseQueue addOperation:op];
and then the downloadSingleFile method:
- (void)downloadSingleFile:(DownloadableFile*)dfile
{
NSURLRequest* req = [NSURLRequest requestWithURL:dfile.downloadUrl];
AFHTTPRequestOperation* reqOper = [[AFHTTPRequestOperation alloc] initWithRequest:req];
reqOper.responseSerializer = [AFHTTPResponseSerializer serializer];
[reqOper setCompletionBlockWithSuccess:^(AFHTTPRequestOperation* op, id response)
{
__weak NSData* fileData = response;
NSError* error;
__weak DownloadableFile* file = dfile;
NSString* fullPath = [file.localPath substringToIndex:[file.localPath rangeOfString:file.localPath.lastPathComponent options:NSBackwardsSearch].location];
[[NSFileManager defaultManager] createDirectoryAtPath:fullPath withIntermediateDirectories:YES attributes:Nil error:&error];
if (error)
{
NSLog(#"Error creating directory path: %#: %#", fullPath, error);
}
else
{
error = nil;
[fileData writeToFile:file.localPath options:NSDataWritingFileProtectionComplete error:&error];
if (error)
{
NSLog(#"Error writing fileData for file: %#: %#", file.file_location, error);
}
}
[self updateProgress:file.orderInQueue withTotal:_totalFiles];
}
failure:^(AFHTTPRequestOperation* op, NSError* error)
{
[self updateProgress:dfile.orderInQueue withTotal:_totalFiles];
NSLog(#"Error downloading %#: %#", dfile.downloadUrl, error.localizedDescription);
}];
[_dlQueue addOperation:reqOper];
}
What I'm seeing is a constant spike in memory as more files get downloaded. It's like the responseObject or maybe even the whole completionBlock is not being let go of.
I've tried making the responseObject __weak as well as fileData. I've tried adding an autoreleasepool and I've tried making the actual file domain object __weak too but still memory climbs and climbs.
I've run Instruments and not seen any leaks persay but it never gets to a point where all the files have been downloaded before it runs out of memory with a big fat "can't allocate region" error. Looking at allocations, I see a bunch of connection:didFinishLoading and connection:didReceiveData methods that never seem to be let go of, however. I can't seem to debug it further than that though.
My question: Why is it running out of memory? What is not getting deallocated and how can I get it to do such?
There is a few things going on here. The biggest is that you are downloading the entire file, storing it in memory, and then writing it out to disk when the download is complete. Even with just one file of 500 MB, you will run out of memory.
The correct way to do this is using an NSOutputStream with asynchronous downloads. The key is to write out the data as soon as it arrives. It should look like this:
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
[self.outputStream write:[data bytes] maxLength:[data length]];
}
Also of note, you are creating your weak references inside the block, not outside. Because of that, you are still creating a retain cycle and leaking memory. When you create weak references, it should look like this.
NSOperation *op = [[NSOperation alloc] init];
__weak NSOperation *weakOp = op;
op.completion = ^{
// Use only weakOp within this block
};
Lastly, your code is using #autoreleasepool. NSAutoreleasePool, and the ARC equivalent #autoreleasepool are only useful in very limited situations. As a general rule, if you aren't absolutely sure you need one, you don't.
With the help of a friend, I was able to figure out the problem.
The problem was actually in the first block of code:
[_dlQueue waitUntilAllOperationsAreFinished];
Apparently , waiting for all operations to finish meant none of those operations would be released either.
Instead of that, I ended up adding a final operation to the queue that would do the final processing and callback and memory is much more stable now.
[_dlQueue addOperationWithBlock:^{
[SettingsManager sharedInstance].timestamp = _timestamp;
dispatch_async(dispatch_get_main_queue(), ^{
callback(nil);
});
}];
What kind of file you are downloading? If you are working with Images or videos you nee to clear URLCache as when you doneload images it create CFDATA and some information in cache and it does not cleared out. You need to clear it explicitly when your single file download completed. It will never caught as a leak also.
NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];
If you are using ARC replace
[sharedCache release];
with
sharedCache = nil;
Hope It may help you.

Unresponsive TableView while WebRequest (AFNetworking) on device, but ok in simulator + MagicalRecord issue

I have an application that retrieves json (employees workschedules) from a web service using AFNetworking and displays them in a table view.
I have my webservice class that takes care of doing the request and once it is done, it stores these data into coredata (I have an another issue here, being that I use magicalRecord and the data does not persist, and I don't understand why) and then calls back its delegate (my tableViewController) telling it it's done, so this can load the workschedules into the cells.
WebServiceClient.m
NSURL *url = [NSURL URLWithString:stringUrl];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
AFJSONRequestOperation *operation = [AFJSONRequestOperation JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
NSArray *workSchedules = [[[NSSet alloc] initWithArray:JSON] allObjects];
NSManagedObjectContext *context = [NSManagedObjectContext MR_contextForCurrentThread];
Workschedule *workscheduleEntity = nil;
NSError *error = nil;
for (NSDictionary *web_workschedule in workSchedules)
{//Inside this method I create other entities that will hydrate my workschedule entity, and it is done using the MR_CreateInContext
workscheduleEntity = [Workschedule workScheduleFromJSONDictionary:web_workschedule withError:&error];
[context MR_save];
}
if([self.delegate respondsToSelector:#selector(workSchedules)]){
[self.delegate workSchedules];
}
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON) {
LOG_ERROR(2,#"Received an HTTTP %d:", response.statusCode);
LOG_ERROR(2,#"The error was: %#", error);
if([self.delegate respondsToSelector:#selector(workSchedules:)]){
[self.delegate workSchedules:nil];//return error
}}];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
[operationQueue addOperation:operation];
}
PendingWorkscheduleViewController.m
- (void)viewDidLoad
{
[super viewDidLoad];
[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];
[self workSchedules];
}
-(void)workSchedules
{
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"pending == YES"];
NSArray *pendingWorkSchedules = [Workschedule MR_findAllWithPredicate:predicate];
self.pendingWorkSchedules = pendingWorkSchedules;
[self.tableView reloadData];
}
My problem is that when i run this while the request is processed the UI is unresponsive (it's a very brief time, but if the request were to increase...) so that if i load the table view and right away try to scroll or click the back button, it just ignores it as it is "frozen". This behavior is on my iphone 4s. On the simulator this works fine and I can't wrap my head around why is that. I tried to call the "[self.webServiceClient getMockedWorkSchedulesForEmployee:[NSNumber numberWithInt:1]];" in a queue using GCD, I tried using performSelectorInBackground: WithObject: etc but still the same (even though with this last method it seemed a little more efficient, but it's an impression and only on the simulator, no changes on the device).
As far as magicalRecord goes I will make separate question.
I would appreciate your help.
Fixed it. The problem is that the success block run on the main thread! (which I did not understand). I just used GCD in the success block with a background queue for processing the data and the main queue to store this data in core data.
As far as magical record issue, i needed to save "nestedContext".
Cheers everyone.

Resources