【问题标题】:decoding a HUGE NSString, running out of memory解码一个巨大的 NSString,内存不足
【发布时间】:2025-12-12 01:50:02
【问题描述】:

我正在寻找有关如何改进使用 base64 编码解码 40+MB NSString 并将其保存到文件的过程的想法,同时能够将该过程适应 iPad 1 的 256 MB RAM

我从 NSXMLParser 得到 NSString:

id pointerToString;

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
if ([currentElement isEqualToString:@"myElement"]) 
    {
    pointerToString = [string retain];
}
}

然后我在回调中使用pointerToString:

[handler performSelector: action withObject: pointerToString];

在回调中(id 值是pointerToString)。我用pointerToString初始化NSData,同时用base64编码解码它。

^(id value)
{
    if ( [[value class] isSubclassOfClass:[NSString class]] ) 
    {
    NSData *data = [NSData dataFromBase64String:value];
    [data writeToFile:file.path atomically:YES];
}
}

在 NSData 调用之后或期间,当内存分配达到大约 130MB 时,iPad 1 设备内存不足并被 iOS 杀死。

我已经确定,为了以这种方式处理 40+MB 的 NSString,我需要大约 180+MB 的 RAM(这是 iPad 2 和 3 上的最大内存分配,该过程在其中工作是因为更多内存)

有什么想法/提示吗?

谢谢

【问题讨论】:

  • 我不知道,但这是一个很好的问题。不过,我想知道为什么你的 NSString 这么大?
  • 修改 base64 编码以在您从解析器接收字符时将字符解码为文件。
  • 关键字:“流式传输”(整个过程,这还涉及到删除NSData的全部内存使用)
  • 看来您实际上是在尝试解码 base64 字符串。
  • iPad1 上通常有大约 140mb 的可用 RAM - 通常更少。是的,我把它弄混了——iPad 1 确实有 256mb 共享 RAM。很多都被操作系统和屏幕使用。

标签: ios xml memory-management base64 nsdata


【解决方案1】:

编辑

在处理这种大小的文件时,您可能不想一次将整个数兆字节的文件加载到内存中,无论是巨大的输入文件还是几乎一样大的输出文件。您应该以流式方式解析它,在进行过程中解码 foundCharacters 中的数据,而不是在内存中保存任何重要部分。

不过,传统技术可能会在该过程的三个阶段中保存整个 XML 文件内存:

  1. 当您从服务器下载 XML 文件时;

  2. 当 XML 解析器解析该文件时;和

  3. 正如您对文件进行 Base64 解码一样。

诀窍是采用流技术,对单个大型 XML 文件的小块同时执行这三个过程。最重要的是,当您下载整个 50mb 文件时,抓取几 kb,解析 XML,如果您要解析 Base64 编码字段,则对那几 kb 执行 Base64 解码,然后继续下一步数据块。

有关这方面的示例(至少是流式 XML 下载和解析,不包括 Base64 解码),请参阅 Apple 的 XMLPerformance sample project。您将看到它将演示两个 XML 解析器,我们都熟悉的 NSXMLParser,以及不太熟悉的 LibXML 解析器。 NSXMLParser 的问题在于,留给它自己的设备,将在开始解析之前将整个 XML 文件加载到内存中,即使您使用 initWithContentsOfURL

在我之前的回答中,我错误地声称通过使用initWithContentsOfURLNSXMLParser 会在下载 URL 的内容时将其解析为漂亮的小数据包。 NSXMLParserDelegate 协议的 foundCharacters 方法似乎与 NSURLConnectionDelegate 方法 didReceiveData 非常相似,以至于我确信 NSXMLParser 会像 NSURLConnection 一样处理流,即返回信息作为正在下载。可悲的是,它没有。

不过,通过使用LibXML,就像Apple XMLPerformance 示例项目一样,您实际上可以使用NSURLConnection 的流能力,从而动态解析XML。

我创建了一个小test project,但我可能会建议您详细了解 Apple 的 XMLPerformance 示例项目。但在我的实验中,一个 56mb 的 XML 文件在通过 NSXMLParser 解析和转换时消耗了超过 100mb,但在使用 LibXML2 时只消耗了 2mb。


在您的 cmets 中,您描述了将 Base64 编码数据下载到文件然后对其进行解码的愿望。这种方法似乎效率低得多,但肯定可以奏效。顺便说一句,在最初的下载中,你有同样的内存问题(我在上面解决了)。我敦促您确保最初下载的 Base64 编码数据不会像大多数例程那样轻松地将其加载到 RAM 中。假设您使用NSURLConnection,您希望在收到didReceiveData 中的数据时将数据写入NSOutputStream,而不是将其保存在RAM 中。

请参阅 Apple 的 AdvancedURLConnections example 的 AdvancedGetController.m 中的 didReceiveResponse,了解如何在收到文件时写入文件,而不是典型的将文件添加到 NSMutableData 的模式(因为这些例程中的大多数只是假设您正在处理一个大小合理的文件)。 (忽略 AdvancedURLConnections 示例中有关身份验证等的所有内容,但重点了解它是如何写入NSOutputStream 的。)该技术将解决此答案顶部列出的三个问题中的第一个,但不是后两者。为此,您必须考虑使用 Apple 的 XMLPerformance 示例项目中所示的 LibXML2 或其他类似技术。

【讨论】:

  • 感谢您精彩的文章!不幸的是,我对后端设置没有影响。我在 foundCharacters 中得到了一个 HUGE 元素,我必须处理它。所以目前我正在尝试提出一个解决方案,将编码的 NSString 保存到文件系统中,然后我会在回调中(部分)读回它,通过 base64 解码器提供这些部分,重新​​组装它到解码的 NSString 中,然后再次将其保存到文件系统中。我会暂时搁置我的问题,然后接受你的问题,如果没有更接近我的方案的方式出现。
  • 谢谢罗布。我看你对这些东西很了解。感谢您的努力,我将考虑实施您的建议。
  • 如果您想进一步讨论,我们应该将其移至聊天。
  • 哇。流媒体有什么不同。从 ~190MB 到 ~84MB 的内存使用......现在在 iPad 1 上工作。谢谢 Rob!
  • 是的,我认为必须有改进的余地。我目前正在使用 NSInputStream *inputStream = [[NSInputStream alloc] initWithFileAtPath:self.fileIncomingData];然后 xmlParser = [[NSXMLParser alloc] initWithStream:inputStream];
【解决方案2】:

方法

- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string

可能不会一次接收所有数据。 医生说

"由解析器对象发送,为其委托提供一个字符串,该字符串表示当前元素的全部或部分字符。"

所以它被多次调用。 看起来您正在尝试一次编写整个字符串(如果我错了,请见谅)。 因此,您可以通过执行以下操作将接收到的数据附加到文件中:

你可以结合使用

-writeData: 

-seekToEndOfFile 

NSFileHandle 类中用于将 NSData 写入文件末尾的方法。

但要小心在接收部分数据时使用 base64 编码!

【讨论】:

  • 嗯,好吧,如果您真的要一次获取所有数据,我确实会尝试将整个字符串拆分为更小的部分并将它们写入文件(肯定不是一次全部)。您可以考虑在后台线程上执行此操作以避免阻塞主线程。我不知道完美的表现,但你可以稍微过期一下。例如从 1000 个字符开始...但是,再次检查您的 base64 编码是否因拆分而正常工作!
  • 1.000 字符是一个不好的例子,可能是因为 24 位块的 base 64 编码。尝试使用可以除以 3 的字符数(例如使用 utf-8 字符串)
  • @Alexander - 我认为 Base64 编码文件的边界是每 4 个字节。我认为您将此与解码文件的边界(每 3 个字节)混淆了。无论如何,我不建议更改 XML 以将文件分解为多个位,而是让 Base64 解码过程动态解码流,并在进行过程中将其写入持久存储。而且,如果 radvan72 对 XML 有任何控制权,那么解决方法是完全避免 Base64 编码(根据我的答案结尾处的 cmets)。
  • @Rob 是的,你是对的!我说的是有一个纯字符串并将其编码为base64。这就是我谈到 3 字节边界的原因。编码文件本身确实每 4 个字节有一个边界