Titouan de Bailleul Titouan de Bailleul - 5 months ago 11
iOS Question

How can I improve the XML parsing performance of my iOS code?

This may have been asked a lot but I'm still lost. I need to parse an XML file that I retrieve from Google Reader's API. Basically, it contains objects such as below :

<object>
<string name="id">feed/http://developer.apple.com/news/rss/news.rss</string>
<string name="title">Apple Developer News</string>
<list name="categories">
<object>
<string name="id">user/17999068807557229152/label/Apple</string>
<string name="label">Apple</string>
</object>
</list>
<string name="sortid">DB67AFC7</string>
<number name="firstitemmsec">1317836072018</number>
<string name="htmlUrl">http://developer.apple.com/news/</string>
</object>


I have tried with NSXMLParser and it works but it is really slow. Maybe my code is not the most efficient but still, it can take more than 10 second to parse and save an object into Core Data. I also have taken a look a several other libraries but their use seem a bit complicated and heavy for such a small XML file.

What do you think I should use ?

Thank you.

EDIT

Here the parser code:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {

if([elementName isEqualToString:@"list"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"subscriptions"]){
subscriptionListFound = YES;
}

if(subscriptionListFound){
if([elementName isEqualToString:@"list"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"categories"]){
categoryFound = YES;
currentCategoryId = [[[NSMutableString alloc] init] autorelease];
currentCategoryLabel = [[[NSMutableString alloc] init] autorelease];
}
if([elementName isEqualToString:@"object"] && !subscriptionFound && !categoryFound){
subscriptionFound = YES;
currentSubscriptionTitle = [[[NSMutableString alloc] init] autorelease];
currentSubscriptionId = [[[NSMutableString alloc] init] autorelease];
currentSubscriptionHtmlURL = [[[NSMutableString alloc] init] autorelease];
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"id"]){
if(categoryFound){
categoryIdFound = YES;
}
else{
subscriptionIdFound = YES;
}
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"title"]){
subscriptionTitleFound = YES;
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"label"]){
categoryLabelFound = YES;
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"htmlUrl"]){
subscriptionHtmlURLFound = YES;
}
}
}

- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {

if([elementName isEqualToString:@"list"] && !categoryFound){
subscriptionListFound = NO;
}

if([elementName isEqualToString:@"list"] && categoryFound){
categoryFound = NO;
}

if([elementName isEqualToString:@"object"] && !categoryFound && subscriptionFound){
[self saveSubscription];
[[NSNotificationCenter defaultCenter] postNotificationName:@"currentSubscriptionNotification" object:currentSubscriptionTitle];
subscriptionFound = NO;
}

if([elementName isEqualToString:@"string"]){
if(subscriptionIdFound == YES) {
[currentSubscriptionId appendString:self.currentParsedCharacterData];
subscriptionIdFound = NO;
}
if(subscriptionTitleFound == YES) {
[currentSubscriptionTitle appendString:self.currentParsedCharacterData];
subscriptionTitleFound = NO;
}
if(subscriptionHtmlURLFound == YES) {
[currentSubscriptionHtmlURL appendString:self.currentParsedCharacterData];
subscriptionHtmlURLFound = NO;
}
if(categoryIdFound == YES) {
[currentCategoryId appendString:self.currentParsedCharacterData];
categoryIdFound = NO;
}
if(categoryLabelFound == YES) {
[currentCategoryLabel appendString:self.currentParsedCharacterData];
categoryLabelFound = NO;
}
}

[self.currentParsedCharacterData setString:@""];
}

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
[self.currentParsedCharacterData appendString:string];
}


Here the code to save by means of CoreData:

- (void) saveSubscription {

NSFetchRequest *fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:
[NSEntityDescription entityForName:@"Group" inManagedObjectContext:context]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: @"(id == %@)",self.currentCategoryId]];
[fetchRequest setSortDescriptors: [NSArray arrayWithObject:
[[[NSSortDescriptor alloc] initWithKey: @"id"
ascending:YES] autorelease]]];

NSError *error2 = nil;
NSArray *foundGroups = [context executeFetchRequest:fetchRequest error:&error2];

if ([foundGroups count] > 0) {
self.currentGroupObject = [foundGroups objectAtIndex:0];
}
else {
self.currentGroupObject = [NSEntityDescription insertNewObjectForEntityForName:@"Group" inManagedObjectContext:context];
[self.currentGroupObject setId:self.currentCategoryId];
[self.currentGroupObject setLabel:self.currentCategoryLabel];
}

fetchRequest = [[[NSFetchRequest alloc] init] autorelease];
[fetchRequest setEntity:
[NSEntityDescription entityForName:@"Subscription" inManagedObjectContext:context]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: @"(id == %@)", self.currentSubscriptionId]];
[fetchRequest setSortDescriptors: [NSArray arrayWithObject:
[[[NSSortDescriptor alloc] initWithKey: @"id"
ascending:YES] autorelease]]];

error2 = nil;
NSArray *foundSubscriptions = [context executeFetchRequest:fetchRequest error:&error2];

if ([foundSubscriptions count] > 0) {
self.currentSubscriptionObject = [foundSubscriptions objectAtIndex:0];
}
else {
self.currentSubscriptionObject = [NSEntityDescription insertNewObjectForEntityForName:@"Subscription" inManagedObjectContext:context];
[self.currentSubscriptionObject setId:self.currentSubscriptionId];
[self.currentSubscriptionObject setTitle:self.currentSubscriptionTitle];
[self.currentSubscriptionObject setHtmlURL:self.currentSubscriptionHtmlURL];
NSString *faviconURL = [self favIconUrlStringFromURL:self.currentSubscriptionHtmlURL];
NSString *faviconPath = [self saveFavicon:self.currentSubscriptionTitle url:faviconURL];
[self.currentSubscriptionObject setFaviconPath:faviconPath];
[self.currentSubscriptionObject setGroup:self.currentGroupObject];
[self.currentGroupObject addSubscriptionObject:self.currentSubscriptionObject];
}

NSError *error;
if (![context save:&error]) {
NSLog(@"Whoops, couldn't save: %@", [error localizedDescription]);
}
}

Answer

Your parsing logic is quite inefficient - you are doing the same test over and over again by saying

if (string and x) do this
if (string and y) do this
if (string and z) do this

Instead of

if (string)
    if (x) do this
    if (y) do this
    if (z) do this

All those unnecessary string comparisons are probably why your parsing is so slow. Same goes for all the object lookups. If you need a value multiple times, get it once and then store it in a variable.

Objective C method calls are relatively slow and can't be optimised away by the compiler, so if the value doesn't change you should call the method once and then store it.

So for example, this:

if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"id"]){
    if(categoryFound){
        categoryIdFound = YES; 
    }
    else{
        subscriptionIdFound = YES;
    }
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"title"]){
    subscriptionTitleFound = YES;
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"label"]){
    categoryLabelFound = YES;
}
if([elementName isEqualToString:@"string"] && [[attributeDict objectForKey:@"name"] isEqualToString:@"htmlUrl"]){
    subscriptionHtmlURLFound = YES;
}

Could be rewritten as this:

NSString *name = [attributeDict objectForKey:@"name"];
if([elementName isEqualToString:@"string"])
{
    if ([name isEqualToString:@"id"])
    {
        if(categoryFound){
            categoryIdFound = YES; 
        }
        else{
            subscriptionIdFound = YES;
        }
    }
    else if ([name isEqualToString:@"title"])
    {
        subscriptionTitleFound = YES;
    }
    else if ([name isEqualToString:@"label"])
    {
        categoryLabelFound = YES;
    }
    else if ([name isEqualToString:@"htmlUrl"])
    {
        subscriptionHtmlURLFound = YES;
    }
}

Which is way more efficient.