【问题标题】:Memory buildup when parsing XML into a Core Data store using NSXMLParser使用 NSXMLParser 将 XML 解析为核心数据存储时的内存积累
【发布时间】:2011-02-19 19:32:56
【问题描述】:

我的应用程序接收 XML 提要,对其进行解析并将结果存储到 Core Data 中时遇到问题。

该问题仅在应用程序第一次运行时出现,此时商店中没有任何内容并且整个提要都被解析和存储。 问题只是内存分配不断增加,直到 50% 的尝试使应用程序崩溃,通常在 10Mb 左右。 分配的对象似乎是 CFData(store) 对象,我似乎找不到任何方法来强制释放它们。 如果您可以让它运行一次并成功完成解析并保存到核心数据,那么以后每次启动都很好,内存使用量永远不会超过 2.5Mb

在我们开始编写代码之前,我采用的一般方法是: 将提要放入 NSData 对象。使用 NSFileManager 将其存储为文件。 从文件路径创建一个 URL 并将其提供给 parseXMLFile: 方法。代表是自我。 在到达 parser:didStartElement:namespaceURI:qualifiedName:attributes: 我创建一个字典来从我需要的标签中捕获数据。 parser:foundCharacters: 方法将标签的内容保存到一个字符串中 parser:didEndElement:... 方法然后确定标记是否是我们需要的东西。如果是,则将其添加到字典中,否则将其忽略。一旦到达项目的末尾,它就会调用一个方法将其添加到核心数据存储中。

从这里查看示例代码和其他人的帖子来看,一般方法似乎没有任何问题。

代码如下,它来自一个更大的视图控制器上下文,但我省略了任何不直接相关的内容。

就我尝试过的事情而言: 提要是 XML,没有转换为 JSON 的选项,抱歉。 这不是在共享文档区域中找到和存储的图像。

关于它可能是什么的线索: 这是我从 Instruments 获得的关于分配的最大数量最多的东西(每项 256k)的条目:

对象地址类别创建时间实时大小负责 图书馆负责调用者

1 0xd710000 CFData (存储)16024774912 • 262144 CFNetwork URLConnectionClient::clientDidReceiveData(_CFData 常量*, URLConnectionClient::ClientConnectionEventQueue*)

这是代码,为了内容的匿名性而编辑;)

-(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];

}    

[编辑] @罗格 我试过玩 NSOperation 但到目前为止没有运气,同样的我,用同样的物品建立起来,每个大约 256k。代码如下:

- (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**
}

【问题讨论】:

  • 是的,代码插入功能不能很好地与我的代码配合使用,所以在 sn-p 中有一个 sn-p。对不起。我责怪 HTML ;)

标签: objective-c xml ios core-data nsxmlparser


【解决方案1】:

您可以禁用缓存:

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

或者清除它:

[[NSURLCache sharedURLCache] removeAllCachedResponses];

这应该可以解决您的问题。

【讨论】:

  • 这应该是正确的答案,我很确定大多数遭受同样问题困扰的人都不知道发生在幕后的缓存
【解决方案2】:

看看 AQXMLParser。有一个示例应用程序展示了如何在后台线程上将其与 Core Data 集成。它是一个流式解析器,因此内存使用量很小。

为 UI 设置第二个 MOC 和获取结果控制器,并在解析完成时重新获取以获取新数据。 (一种选择是在后台 MOC 保存以在上下文之间合并时注册更改通知)

对于图像,UIImageVew 上有许多第三方类别支持异步下载和占位符图像。因此,您将解析图像 URL,将其存储在 Core Data 中,然后在查看该项目时在图像视图上设置 URL。这样用户就不需要等待所有图像都被下载。

【讨论】:

  • NSManagedObjectContext 的配置实例
【解决方案3】:

你的问题在这里:

[newManagedObject release];

摆脱它,崩溃就会消失(你不释放 NSManagedObjects,Coredata 会为你处理)。

干杯,

罗格

【讨论】:

  • 您好,感谢您的回复。我试了一下,它确实减少了大内存分配,但它仍然超过 9Mb,我仍然有许多 CFData(store) 对象挂起,直到我强制退出并重新启动,然后它以稳定的 2.5mb 运行。关于我没有处理的可能存在的任何其他想法?
  • 我注意到您在解析过程中为图像加载了一些 NSData。您是否有机会在单独的操作或需要的基础上做到这一点?您可以使用 imageURL 引用完成解析,然后启动 NSOperationQueue 以开始下载图像并将它们分配给您的对象。它可能有助于降低解析期间的峰值内存消耗。
  • 或者在用户请求时使用 NSURLConnection 获取图像。还要看看这些图像有多大,如果 > 1mb(每个)你可能想看看为它们创建一个单独的图像实体,然后添加一个关系到你的主模型。缩略图应该可以留在您的主要实体模型中。
  • 对所有 cmets 感到抱歉,但还有一件事:我刚刚注意到您将 URLConnectionClient 作为 Instruments 上内存分配问题的来源,因此它肯定与您的 NSData:dataWithContentsOfURL: 调用有关。尝试先将它们注释掉,看看是否有区别。如果是这样,请参阅我上面的 2 cmets 以获取建议。
  • 嘿,非常感谢您的出色意见。我尝试使用资源包中已经存在的静态图像,并且您正确地将 URL 连接识别为看似原因。您会推荐什么作为最佳解决方案?设计决定是下载带有提要的完整图像。由于您解析这么多元素的唯一时间是第一次发布,所以在那个时候它只是要克服的事情。我对 NSOperationQueue 的想法很感兴趣,这是否意味着在单独的线程上处理图像下载?我该如何实现?
猜你喜欢
  • 2013-06-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多