NSURLSession内存泄漏问题

3
即使使用invalidate NSURLSession,使用Instruments运行分析时,一些类(可能是私有的)如TubeManager、HTTPConnectionCache和HTTPConnectionCacheDictionary仍然存在于内存中。
复现代码片段:
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
NSURLSessionDataTask* sessionDataTask = [session dataTaskWithRequest:request
                                               completionHandler:^(NSData *data, NSURLResponse *response, NSError *error)
{
    [session finishTasksAndInvalidate];
}];
[sessionDataTask resume];

Instruments screenshot


我不确定你是如何测量内存增长的,但如果我查看总分配量,进行100次下载,再次检查总分配量,并重复五次,我会看到峰值分配量趋于稳定。如果我发出内存警告,甚至有一些内存也会被回收。现在,如果我查看一段时间内的分配变化或使用“代”,它看起来像是在增长,但净净地,它趋于稳定。此外,请确保关闭僵尸进程。 - Rob
你试过运行Instruments并查找我提到的类吗? - Victor Barros
请查看此截图:https://www.evernote.com/shard/s70/sh/14637ee5-11ea-4744-889d-56ca50b647da/3a0e9e1c7e59e516d21f27b3da7d04da您是否没有得到类似的结果? 您正在运行哪个版本的iOS? - Victor Barros
是的,在我创建的示例项目中,为了重现问题,我为每个请求创建一个新会话,但一旦请求完成,我就会使会话失效。这只是为了重现问题。在实际情况中,我不需要每次都创建新会话,但如果我需要创建一堆会话并在不再需要时释放它们,那么没有办法保持内存消耗低。 - Victor Barros
不,我认为你无法采取任何措施来减轻内存行为。我认为解决方案是设计方法来减轻需要数百/数千个会话对象的需求。但如果你想引起苹果的注意,可以随意发布到http://bugreport.apple.com。 - Rob
显示剩余4条评论
3个回答

1

在错误的位置调用了finishTasksAndInvalidate... completionHandler 用于处理响应,与会话无关

以下是正确的代码:

NSURLSessionConfiguration* config = [NSURLSessionConfigurationdefaultSessionConfiguration];
NSURLSession* session = [NSURLSession sessionWithConfiguration:config];
NSURLRequest* request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]];
NSURLSessionDataTask* sessionDataTask = [session dataTaskWithRequest:request
                                           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
   // handle response...
}];
[sessionDataTask resume];
[session finishTasksAndInvalidate];

0
请注意,在iOS 9上,安全框架会分配大约4k的SSL缓存数据,并在您为新的NSURLSession对象恢复任务时首次向您的应用程序收取费用。Apple Technical Q&A QA1727告诉我们,这个SSL缓存会持续10分钟,无论如何,因为它是私有的并且完全由系统管理(因为安全!)。
在您的代码示例中,每次发出请求时都会创建一个新的NSURLSession对象。但是,您只是使用了defaultSessionConfiguration,并没有指定可能存在强引用的委托,那么您应该做的是使用单例[NSURLSession sharedSession]并使用resetWithCompletionHandler来清除非安全分配。或者如果您想自定义配置,则可以创建自定义单例。
(NSURLSession *)sharedSession 讨论
对于基本请求,URL 会话类提供了一个共享的单例会话对象,它为您提供了合理的默认行为。通过使用共享会话,您可以仅用几行代码将 URL 的内容获取到内存中。
与其他会话类型不同,您不需要创建共享会话;您只需调用 [NSURLSession sharedSession] 来请求它。因此,您不提供委托或配置对象。因此,使用共享会话:
- 您无法逐步获取从服务器到达的数据。 - 您无法显着自定义默认连接行为。 - 您的执行身份验证的能力有限。 - 您不能在应用程序未运行时执行后台下载或上传。
共享会话使用共享 NSURLCache、NSHTTPCookieStorage 和 NSURLCredentialStorage 对象,使用共享的自定义网络协议列表(使用 registerClass: 和 unregisterClass: 配置),并基于默认配置。
在使用共享会话时,通常应避免自定义缓存、cookie 存储或凭据存储(除非您已经在使用 NSURLConnection 进行自定义),因为很有可能您最终会超出默认会话的功能范围,此时您将不得不以适用于自定义 URL 会话的方式重写所有这些自定义内容。
换句话说,如果您正在处理缓存、cookie、身份验证或自定义网络协议等任何内容,则应该使用默认会话而不是默认会话。

(来自NSURLSession类参考文档...斜体是我加的;P)

如果您没有使用Apple提供的sharedSession单例,那么您应该至少从Apple那里学习并创建一个具有会话属性的单例。会话的目的是它旨在比仅限于一个请求更长时间地存在。尽管他们的文档不太清楚,但Apple提供了一个单例,并将其称为“会话”,这表明会话对象的寿命应该比单个请求更长。

是的,你应该在某个时候调用invalidateAndCancel,但不是在每个单独的请求之后,甚至不是在每个请求都发送到完全不同的服务器上的情况下(这几乎从未发生)。只有当您要断开与特定会话的引用时,才需要使其无效并取消;否则,您可以在会话上调用flushWithCompletionHandlerresetWithCompletionHandler刷新会话的堆分配到VM,或重置以清除堆和VM存储。(还请参见我的答案here。)


0

那么,问题是什么?您想关闭缓存吗?网络响应通常在NSURLCache中进行内存和持久性存储的缓存。

如果此缓存使用有问题,请相应地更改会话配置的requestCachePolicy。或更改NSMutableURLRequest本身的cachePolicy。您还可以配置会话配置使用的URLCache的最大大小,以限制RAM使用量和持久性内存使用量。

即使关闭缓存,一般规则也不应该感到意外的是API调用会增加内存消耗,这并非您自己的错。应用程序第一次执行某些任务时,经常会出现一些适度的内存消耗。但是,在应用程序运行时,不应看到后续迭代中的相同增长。跟踪内存使用情况时,通常建议多次重复任务,并查看应用程序是否返回到某个稳定状态(这是理想的),或者它是否继续增长(这需要进一步调查以确保您的代码不是问题的根源)。但是,除非初始内存消耗非常大,否则我们很少担心初始内存消耗。

看了你的代码片段,没有明显的错误。我倾向于怀疑iOS常规内存消耗。假设问题不仅限于一般缓存行为,如果内存消耗剧烈并且/或每次应用程序执行此代码时都会继续增长,则提供更多详细信息,我们可以帮助您进一步诊断。

这是我发出了四批每批100个请求后,内存配置文件的情况;再加上在我发出内存警告后的最终标志:

enter image description here

(注意,这是一个组合图像,以便我可以展示第一批之前、第三批之前、最后一批之后以及我手动发布内存警告后的内存情况。我将它们结合在一起,以便更容易地看到这四个时间点的总分配情况。)


即使像这样更改requestCachePolicyURLCache,例如config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; config.URLCache = [[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil];,每个请求的内存消耗仍然会增长...因此,在执行800个请求后,内存消耗从4mb增长到约6mb,因此有2mb不应该存在。 - Victor Barros
请看我上面的答案:安全框架的堆SSL缓存是私有的,因此每个新的会话对象在10分钟内增加3.5k。这与10分钟后2.7MB的增长一致,但除非存在其他问题,否则您将不会看到它再次增加。因此,请运行25分钟的仪器,并从第15分钟到第25分钟标记一个代。观察增长量。请注意,如果请求的频率在此期间增加或减少,则会更改SSL缓存的内存占用的“最高水位线”。 - CommaToast

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接