3

I have a problem with an app that takes an XML feed, parses it, and stores the results into Core Data.

The problem only occurs on the very first run of the app when there is nothing in the store and the whole feed is parsed and stored. The problem is simply that memory allocations build up and up until, on 50% of attempts it crashes the app, usually at around 10Mb. The objects allocated seem to be CFData(store) objects and I can't seem to find any way to force a release of them. If you can get it to run once and successfully complete the parsing and save to core data then every subsequent launch is fine, memory usage never exceeds 2.5Mb

Here's the general approach I have before we get into code: Get the feed into an NSData object. Use NSFileManager to store it as a file. Create a URL from the file path and give it to the parseXMLFile: method. Delegate is self. On reaching parser:didStartElement:namespaceURI:qualifiedName:attributes: I create a dictionary to catch data from tags I need. The parser:foundCharacters: method saves the contents of the tag to a string The parser:didEndElement:... method then determines if the tag is something we need. If so it adds it to the dictionary if not it ignores it. Once it reaches the end of an item it calls a method to add it to the core data store.

From looking at sample code and other peoples postings here it seems there's nothing in the general approach thats wrong.

The code is below, it comes from a larger context of a view controller but I omitted anything not directly related.

In terms of things I have tried: The feed is XML, no option to convert to JSON, sorry. It's not the images being found and stored in the shared documents area.

Clues as to what it might be: This is the entry I get from Instruments on the largest most numerous things allocated (256k per item):

Object Address Category Creation Time Live Size Responsible Library Responsible Caller

1 0xd710000 CFData (store) 16024774912 • 262144 CFNetwork URLConnectionClient::clientDidReceiveData(_CFData const*, URLConnectionClient::ClientConnectionEventQueue*)

And here's the code, edited for anonymity of the content ;)

-(void)parserDidStartDocument:(NSXMLParser *)feedParser { }

-(void)parserDidEndDocument:(NSXMLParser *)feedParser { }

-(void)parser:(NSXMLParser *)feedParser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict
{
    eachElement = elementName;
    if ( [eachElement isEqualToString:@"item" ] )
    {
        //create a dictionary to store each item from the XML feed
        self.currentItem = [[[NSMutableDictionary alloc] init] autorelease];
    }
}


-(void)parser:(NSXMLParser *)feedParser foundCharacters:(NSString *)feedString 
{
    //Make sure that tag content doesn't line break unnecessarily
    if (self.currentString == nil) 
    {
        self.currentString = [[[NSMutableString alloc] init]autorelease];
    }
    [self.currentString appendString:feedString];

}

-(void)parser:(NSXMLParser *)feedParser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName 
{
    eachElement = elementName;
    NSString *tempString = [self.currentString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];

if ( [eachElement isEqualToString:@"title"] ) 
    {
        //skip the intro title
        if (![tempString isEqualToString:@"Andy Panda UK"])
        {
            //add item to di citonary output to console
            [self.currentItem setValue:tempString forKey:eachElement];
        }
    }
    if ( [eachElement isEqualToString:@"link"] ) 
    {
        //skip the intro link
        if (![tempString isEqualToString:@"http://andypanda.co.uk/comic"])
        {
            //add item to dicitonary output to console
            [self.currentItem setValue:tempString forKey:eachElement];
        }
    }
    if ( [eachElement isEqualToString:@"pubDate"] ) 
    {
        //add item to dicitonary output to console
        [self.currentItem setValue:tempString forKey:eachElement];
    }
    if ( [eachElement isEqualToString:@"description"] ) 
    {
        if ([tempString length] > 150)
        {
            //trims the string to just give us the link to the image file
            NSRange firstPart = [tempString rangeOfString:@"src"];
            NSString *firstString = [tempString substringFromIndex:firstPart.location+5];
            NSString *secondString;
            NSString *separatorString = @"\"";
            NSScanner *aScanner = [NSScanner scannerWithString:firstString];
            [aScanner scanUpToString:separatorString intoString:&secondString];

            //trims the string further to give us just the credits
            NSRange secondPart = [firstString rangeOfString:@"title="];
            NSString *thirdString = [firstString substringFromIndex:secondPart.location+7];
            thirdString = [thirdString substringToIndex:[thirdString length] - 12];
            NSString *fourthString= [secondString substringFromIndex:35];

            //add the items to the dictionary and output to console
            [self.currentItem setValue:secondString forKey:@"imageURL"];
            [self.currentItem setValue:thirdString forKey:@"credits"];
            [self.currentItem setValue:fourthString forKey:@"imagePreviewURL"];
            //safety sake set unneeded objects to nil before scope rules kick in to release them
            firstString = nil;
            secondString = nil;
            thirdString = nil;
        }
        tempString = nil;
    }


    //close the feed and release all the little objects! Fly be free!
    if ( [eachElement isEqualToString:@"item" ] )
    {
        //get the date sorted
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        formatter.dateFormat = @"EEE, dd MMM yyyy HH:mm:ss ZZZZ";
        NSDate *pubDate = [formatter dateFromString:[currentItem valueForKey:@"pubDate"]];
        [formatter release];
        NSArray *fetchedArray = [[NSArray alloc] initWithArray:[fetchedResultsController fetchedObjects]];
        //build the string to make the image
        NSString *previewURL = [@"http://andypanda.co.uk/comic/comics-rss/" stringByAppendingString:[self.currentItem valueForKey:@"imagePreviewURL"]];

        if ([fetchedArray count] == 0)
        {
            [self sendToCoreDataStoreWithDate:pubDate andPreview:previewURL];
        } 
        else 
        {
            int i, matches = 0;
            for (i = 0; i < [fetchedArray count]; i++)
            {
                if ([[self.currentItem valueForKey:@"title"] isEqualToString:[[fetchedArray objectAtIndex:i] valueForKey:@"stripTitle"]])
                {
                    matches++;
                }
            }

            if (matches == 0)
            {
                [self sendToCoreDataStoreWithDate:pubDate andPreview:previewURL];
            }
        }
        previewURL = nil;
        [previewURL release];
        [fetchedArray release];
    }
    self.currentString = nil;
}

- (void)sendToCoreDataStoreWithDate:(NSDate*)pubDate andPreview:(NSString*)previewURL {
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];
    newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
    [newManagedObject setValue:[[currentItem valueForKey:@"title"] description] forKey:@"stripTitle"];
    [newManagedObject setValue:[[currentItem valueForKey:@"credits"] description] forKey:@"stripCredits"];
    [newManagedObject setValue:[[currentItem valueForKey:@"imageURL"] description] forKey:@"stripImgURL"];
    [newManagedObject setValue:[[currentItem valueForKey:@"link"] description] forKey:@"stripURL"];
    [newManagedObject setValue:pubDate forKey:@"stripPubDate"];
    [newManagedObject setValue:@"NO" forKey:@"stripBookmark"];
    //**THE NEW SYSTEM**
    NSString *destinationPath = [(AndyPadAppDelegate *)[[UIApplication sharedApplication] delegate] applicationDocumentsDirectory];
    NSString *guidPreview = [[NSProcessInfo processInfo] globallyUniqueString];
    NSString *guidImage = [[NSProcessInfo processInfo] globallyUniqueString];
    NSString *dpPreview = [destinationPath stringByAppendingPathComponent:guidPreview];
    NSString *dpImage = [destinationPath stringByAppendingPathComponent:guidImage];
    NSData *previewD = [NSData dataWithContentsOfURL:[NSURL URLWithString:previewURL]];
    NSData *imageD = [NSData dataWithContentsOfURL:[NSURL URLWithString:[currentItem valueForKey:@"imageURL"]]];
    //NSError *error = nil;
    [[NSFileManager defaultManager] createFileAtPath:dpPreview contents:previewD  attributes:nil];
    [[NSFileManager defaultManager] createFileAtPath:dpImage contents:imageD attributes:nil];
    //TODO: BETTER ERROR HANDLING WHEN COMPLETED APP
    [newManagedObject setValue:dpPreview forKey:@"stripLocalPreviewPath"];
    [newManagedObject setValue:dpImage forKey:@"stripLocalImagePath"];  
    [newManagedObject release];
    before++;
    [self.currentItem removeAllObjects];
    self.currentItem = nil;
    [self.currentItem release];
    [xmlParserPool drain];
    self.xmlParserPool = [[NSAutoreleasePool alloc] init];

}    

[EDIT] @Rog I tried playing with NSOperation but no luck so far, the same me,ory build up with the same items, all about 256k each. Here's the code:

- (void)sendToCoreDataStoreWithDate:(NSDate*)pubDate andPreview:(NSString*)previewURL
{
    NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[fetchedResultsController fetchRequest] entity];

    newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];


    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];
    [newManagedObject setValue:[[currentItem valueForKey:@"title"] description] forKey:@"stripTitle"];
    [newManagedObject setValue:[[currentItem valueForKey:@"credits"] description] forKey:@"stripCredits"];
    [newManagedObject setValue:[[currentItem valueForKey:@"imageURL"] description] forKey:@"stripImgURL"];
    [newManagedObject setValue:[[currentItem valueForKey:@"link"] description] forKey:@"stripURL"];
    [newManagedObject setValue:pubDate forKey:@"stripPubDate"];
    [newManagedObject setValue:@"NO" forKey:@"stripBookmark"];

NSString *destinationPath = [(AndyPadAppDelegate *)[[UIApplication sharedApplication] delegate] applicationDocumentsDirectory];
NSString *guidPreview = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *guidImage = [[NSProcessInfo processInfo] globallyUniqueString];
NSString *dpPreview = [destinationPath stringByAppendingPathComponent:guidPreview];
NSString *dpImage = [destinationPath stringByAppendingPathComponent:guidImage];

//Create an array and send the contents off to be dispatched to an operation queue
NSArray *previewArray = [NSArray arrayWithObjects:dpPreview, previewURL, nil];
NSOperation *prevOp = [self taskWithData:previewArray];
[prevOp start];

//Create an array and send the contents off to be dispatched to an operation queue
NSArray *imageArray = [NSArray arrayWithObjects:dpImage, [currentItem valueForKey:@"imageURL"], nil];
NSOperation *imagOp = [self taskWithData:imageArray];
[imagOp start];

[newManagedObject setValue:dpPreview forKey:@"stripLocalPreviewPath"];
[newManagedObject setValue:dpImage forKey:@"stripLocalImagePath"];  
//[newManagedObject release]; **recommended by stackoverflow answer
before++;
[self.currentItem removeAllObjects];
self.currentItem = nil;
[self.currentItem release];
[xmlParserPool drain];
self.xmlParserPool = [[NSAutoreleasePool alloc] init];


}

- (NSOperation*)taskWithData:(id)data
{
    NSInvocationOperation* theOp = [[[NSInvocationOperation alloc] initWithTarget:self
                                                                         selector:@selector(threadedImageDownload:) 
                                                                           object:data] autorelease];
    return theOp;
}

- (void)threadedImageDownload:(id)data 
{
    NSURL *urlFromArray1 = [NSURL URLWithString:[data objectAtIndex:1]];
    NSString *stringFromArray0 = [NSString stringWithString:[data objectAtIndex:0]];
    NSData *imageFile = [NSData dataWithContentsOfURL:urlFromArray1];
    [[NSFileManager defaultManager] createFileAtPath:stringFromArray0 
                                            contents:imageFile 
                                          attributes:nil];
    //-=====0jjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjjmn **OLEG**
}
Rog
  • 18,602
  • 6
  • 76
  • 97
Cocoadelica
  • 3,006
  • 27
  • 30
  • Yeah the code insert function just won't play nice with my code so there's a snippet within a snippet. Sorry. I blame HTML ;) – Cocoadelica Feb 19 '11 at 20:17

3 Answers3

7

You can disable the caching :

NSURLCache *sharedCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];
[NSURLCache setSharedURLCache:sharedCache];
[sharedCache release];

Or clear it :

[[NSURLCache sharedURLCache] removeAllCachedResponses];

This should fix your problem.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Sander Grout
  • 131
  • 4
  • this should be the correct answer, im pretty sure that most people suffering with the same problem are unaware of the Caching that takes place behind the scenes – Matt Apr 06 '11 at 15:04
1

Take a look at AQXMLParser. There is a example app that shows how to integrate it with Core Data on a background thread. It's a streaming parser so memory usage is minimal.

Set up a second MOC and fetched results controller for the UI and refetch when the parsing is complete to get the new data. (one option is to register for change notifications when the background MOC saves to merge between contexts)

For images, there are a number of third party categories on UIImageVew that support asynchronous downloads and placeholder images. So you would parse the image URL, store it in Core Data, then set the URL on the image view when that item is viewed. This way the user doesn't need to wait for all the images to be downloaded.

Scott Ahten
  • 1,141
  • 7
  • 15
0

Your problem is here:

[newManagedObject release];

Get rid of it, and the crashes will be gone (you don't release NSManagedObjects, Coredata will handle that for you).

Cheers,

Rog

Rog
  • 18,602
  • 6
  • 76
  • 97
  • Hi, thanks for your response. I gave that a try and it did lessen the large memory allocation a bit but it still tops 9Mb and I still have many CFData(store) objects hanging until I force-quit it and restart, then it runs at a solid 2.5mb. Any other ideas on what might be hanging around that I'm not handling? – Cocoadelica Feb 20 '11 at 01:51
  • I noticed you are loading some NSData for images during parsing. Is there any chance you could do that on a separate operation or on a needs basis? You could finish the parsing with a imageURL reference and then fire-off a NSOperationQueue to start downloading the images and assign them to your objects. It might help lowering your peak mem consumption during parsing. – Rog Feb 20 '11 at 02:24
  • Or simply fetch the images with a NSURLConnection when the user requests it. Also look at how big these images are, if >1mb (each) you probably want to look at creating a separate Image entity for them, and then add a relationship to your main model. The thumbnails should be fine to stay in your main entity model. – Rog Feb 20 '11 at 02:32
  • Sorry for all the comments but one more thing: I just noticed you're getting `URLConnectionClient` as the source of mem allocation problems on Instruments so it is definitely related to your NSData:dataWithContentsOfURL: calls. Try commenting them out first and see if it makes a difference. If it does, see my 2 comments above for suggestions. – Rog Feb 20 '11 at 02:34
  • Hey, many thanks for the excellent input. I tried using a static image already in the resource bundle and you were right in identifying the URL connections as the seeming cause. What would you recommend as the best fix? The design decision has been to download the full image with the feed. Since the only time you parse that many lements is the first launch it's just something to get over at that point. I'm intrigued by the NSOperationQueue idea, does that mean processing the image downloads on a seperate thread? How do I implement this? – Cocoadelica Feb 20 '11 at 12:33
  • @Rog OK, i tried some NSOperation code and added it to the post above, no joy so far though. Perhaps I'm not implementing it correctly? – Cocoadelica Feb 20 '11 at 13:55
  • @Rog Here's the instruments readings on the allocations: – Cocoadelica Feb 20 '11 at 15:03
  • I've boiled it down to... If I replace this line: NSData *imageFile = [NSData dataWithContentsOfURL:urlSource options:NSDataReadingUncached error:nil]; with this: NSData *imageFile = [NSData dataWithContentsOfFile:@"placeholder.png"]; Then it works totally fine without the memory build up. I can only think it's something specific to loading the data from a URL and the URL connection it uses. But I can't find anything that refers to how to deal with this or an alternate method for handling the requirement. Sigh. – Cocoadelica Feb 20 '11 at 15:33
  • Here's something else for you to try - you're using an NSData convenience method to load the images. This is returning you an autoreleased object which is probably sticking around for longer than you need. Try and take ownership of it by either allocating/initialising it yourself with `[[NSData alloc] initWithContentsOfURL:]` or create an autorelease pool for the problematic part of your code - here's a good example http://stackoverflow.com/questions/742196/how-do-i-create-an-local-autorelease-pool-to-save-up-memory – Rog Feb 20 '11 at 21:52