【问题标题】:Recursive / Iterative NSURLSessionDataTask causing memory leak递归/迭代 NSURLSessionDataTask 导致内存泄漏
【发布时间】:2014-11-01 12:41:55
【问题描述】:

我的代码中存在内存泄漏问题,我需要快速连续获取多个 URL,每个 GET 都会受到前一个 GET 的结果的影响。目的是在响应中查找特定的内容。

我发现最简洁的实现方式是递归,因为我可以使用相同的方法来确定响应中是否存在所需的值。从功能上讲,它工作得很好,但它会泄漏内存,如下所述。我还以迭代方式实现了相同的功能,这也会泄漏内存。

在我看来,NSURLSession API 似乎负责泄漏此内存,并且仅在非常快速地连续进行多次调用时才会发生。但是,如果有人能指出我所犯的任何明显错误,我将不胜感激。

10/09/14 更新:

更新为添加递归计数器,表明即使代码未无限次执行,泄漏仍然会发生。还稍微整理了实现,在视图控制器中重新使用 NSURLSessionNSURLSessionConfiguration 作为属性。

示例代码:

- (void)performURLCallRecursive {

    recursionLimiter++;

    if (recursionLimiter > 10) {
        [self.session finishTasksAndInvalidate];
        return;
    }

    NSURL * checkURL = [NSURL URLWithString:@"http://www.google.com"];

    __block NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:checkURL
                                                                            cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                                                        timeoutInterval:0.0f];

    __weak typeof(self) weakSelf = self;

    NSURLSessionDataTask * task = [self.session dataTaskWithRequest:urlRequest
                                                  completionHandler:^(NSData *data, NSURLResponse *response, NSError
*error) {

                                                      NSString * body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                                                      NSLog(@"Body: %@", body);

                                                      [weakSelf performURLCallRecursive];

                                                  }];

    [task resume];
 }

#pragma mark - Getters

- (NSURLSessionConfiguration *)sessionConfiguration {

    if (!_sessionConfiguration) {

        _sessionConfiguration = [NSURLSessionConfiguration ephemeralSessionConfiguration];

        [_sessionConfiguration setAllowsCellularAccess:NO];
        [_sessionConfiguration setTimeoutIntervalForRequest:10.0f];
        [_sessionConfiguration setTimeoutIntervalForResource:10.0f];

        [_sessionConfiguration setURLCache:[[NSURLCache alloc] initWithMemoryCapacity:0 diskCapacity:0 diskPath:nil]];

    }

    return _sessionConfiguration;
 }

- (NSURLSession *)session {

    if (_session == nil) {

        _session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration
                                                 delegate:[SPRSessionDelegate new]
                                            delegateQueue:nil];

    }

    return _session; 
}

仪器报告的内存泄漏。 (注意:这些每次都略有不同,但大部分都包含相同的泄漏,只是或多或少相同的泄漏):

进一步更新:

所以,我实际上迭代地实现了相同的代码,内存泄漏仍然发生。对于这个例子,我包含了一个循环限制器,所以它不会永远执行。谁能帮我弄清楚这里到底发生了什么?

- (void)performURLCallIterative
{

    int loopLimiter = 0;

    do {

        NSURLSessionConfiguration * defaultSession = [NSURLSessionConfiguration defaultSessionConfiguration];

        [defaultSession setAllowsCellularAccess:NO];
        [defaultSession setTimeoutIntervalForRequest:10.0f];
        [defaultSession setTimeoutIntervalForResource:10.0f];

        NSURLSession * session = [NSURLSession sessionWithConfiguration:defaultSession
                                                               delegate:self
                                                          delegateQueue:nil];


        NSURL * checkURL = [NSURL URLWithString:@"http://google.com"];

        NSMutableURLRequest * urlRequest = [[NSMutableURLRequest alloc] initWithURL:checkURL
                                                                        cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
                                                                    timeoutInterval:0.0f];

        __weak NSURLSession * weakSession = session;

        dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

        NSURLSessionDataTask * task = [session dataTaskWithRequest:urlRequest
                                                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

                                                     NSString * body = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
                                                     NSLog(@"Body: %@", body);

                                                     dispatch_semaphore_signal(semaphore);

                                                     [weakSession invalidateAndCancel];

                                                 }];

        [task resume];

        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

        loopLimiter++;

    } while (loopLimiter <= 6);

}

10/09/14 更新:

iOS 8 上的任何 Google 员工仍然会在 iOS 8 上找到自己的出路。就我而言,这是 iOS 中的一个错误。

【问题讨论】:

  • 你使用ARC还是MRC?
  • iOS8上真的解决了吗?我有完全相同的问题。它现在对你有用吗?
  • 不,抱歉。它似乎已在 iOS 8 GM 版本中得到修复,但现在在 iOS 8 的已发布版本中再次出现。
  • 好像我也遇到了同样的问题,你找到解决办法了吗?

标签: ios objective-c recursion memory-leaks nsurlsession


【解决方案1】:

我在 NSURLSession 的内存方面遇到了很多问题,我最终通过不为每个请求使用新会话来解决它。会话在 Wikipedia 上通常定义为:

半永久性交互式信息交换

因此,Apple 的便捷类方法 [NSURLSession sharedSession] 为我们提供了 NSURLSession 对象打算如何使用的线索:作为半永久对象,而不是像您正在做的那样为每个请求新创建的一次性对象。

您正在为大量请求创建一个新的会话对象每个请求,从服务器的角度来看,这些请求都是与单个客户端的单个会话的一部分。

我一直在做同样的事情,直到我意识到这是我痛苦的根源。我没有发现Apple的文档对此很清楚,但是在我意识到自己的方式错误之后,它使文档中的某些内容突然变得更有意义,例如为什么NSURLSession有一个sharedSession单例便捷方法,为什么这个词“tasks”在finishTasksAndInvalidate 中是复数,为什么他们称它为“会话”,为什么它有缓存等等。(如果只是针对一个请求,为什么会是“会话” ”,“缓存”有什么好处?)

了解 Safari 等浏览器如何看待会话会有所帮助。当您第一次连接到给定的服务器时,新会话就会开始。设置会话涉及创建 SSL 证书缓存、建立身份验证、握手等。每次页面上的某些 JavaScript 向同一服务器发出新请求时,执行所有这些操作会非常低效,特别是因为现代 Web 应用程序不断通过回调等方式发出请求。这就是为什么要为大量请求和响应建立单个会话的原因——如果您愿意,可以在客户端之间进行 对话和服务器。最终,会话会过期,但通常会在几分钟后发生,而不是在一个请求之后!

重点是,您应该如何使用 NSURLSession 对象是使用强引用的 NSURLSession 对象作为属性来创建单例。如果您需要自定义会话的配置(例如关闭缓存等),请执行此操作。但是,如果您不需要自定义它,只需使用 Apple 的sharedSession

如果您在自定义类上使用单例,那么,如果您永远不需要将会话属性设置为 nil,那么您永远不需要 invalidateAndCancelfinishTasksAndInvalidate。相反,只需 resetWithCompletionBlockflushWithCompletionBlock 定期清除连接缓存。

如果您讨厌单例,您仍然可以将会话用作属性,只需确保在会话的最后一个所有者被 ARC 运行时释放之前invalidateAndCancelfinishTasksAndInvalidate 会话。

还要注意,将 NSURLSession 对象的 URLCache 属性设置为 nil 是关闭缓存的正确方法。这就是 Apple 所说的他们为 backgroundSessionConfiguration 所做的事情。

请参阅我关于此主题的其他答案 herehere

【讨论】:

    【解决方案2】:

    向 Apple 提出的开发人员支持请求表明这是 iOS 7 中的一个错误。上面发布的代码示例(递归或迭代)没有错误,据报道,它已在 iOS 8 GM 版本中得到修复。

    更新:

    iOS 8.1 中仍然存在这种情况

    【讨论】:

    • 太棒了,你解决了这个问题!奇怪的错误。很高兴和你一起工作。祝你在 iOS 8 中好运。
    • 不幸的是,iOS 9 中仍然会发生这种情况。:( 有可以欺骗的雷达吗?
    【解决方案3】:

    - 2014 年 9 月 12 日更新

    解决方案:等待iOS8。

    - 2014 年 9 月 10 日更新

    哇,这是螺旋式上升到复杂性的第 N 维:P。我希望你能很快在这里休息一下。

    我还有一些其他的东西要你试试。

    1) 你能否确保 NSZombies 已关闭。在 Xcode 中,Product->Scheme->Edit Scheme...->Enable Zombie Objects(未勾选)。

    2) 也可以尝试将cachePolicy:NSURLCacheStorageNotAllowed 替换为您的NSMutableURLRequest

    3) 你能看看你完成时是否有错误吗?把它放在你的身体字符串分配周围......

    if (error == nil)
    {
        //Enter data->string code here
    }
    

    4) 你能看看你是否没有获得状态 200?

    NSInteger statusCode = [(NSHTTPURLResponse *)response statusCode];
    

    5) 很难准确描述您的项目是如何设置的。我将有一个 NSObject 类型的类来容纳 NSURLSession 方法,它与调用它的 UIViewController 类是分开的。然后,计时器或您希望选择的任何递归方法将从 UIViewController 调用与 url 会话相关的方法。

    - 2014 年 9 月 9 日更新

    您对我的问题 (2) 是正确的。数据任务在完成前恢复,数据任务完成后会话失效。我没有看到它这样做,但它是有道理的。刚刚在我这边进行了测试,关于 [session invalidateAndCancel] 没有泄漏...

    您能否检查您的完成处理程序是否执行?也许它没有,并且在新任务开始之前会话永远不会被取消?

    我注意到 Instruments Leaks 报告中有一些对 HTTP 标头的引用,也许如果您没有指定 [urlRequest setHTTPMethod:@"GET"] 请求缺少一些基本标头?

    (我会在找到解决方案后进行编辑,所以这看起来不像是讨论)。

    - 2014 年 9 月 8 日原创

    有趣的问题!我有与 NSURLSessions 相关的故障排除泄漏。到目前为止,@autoreleasepool{} 和其他人绝对是值得尝试的好建议……但是!

    恐怕你让我们看过去的东西可能是这里的罪魁祸首。

    首先只是一些观察:

    1) 我不清楚你为什么需要在这里__weak the self。您要避免的保留周期是什么?除了“示例”之外,您实际使用的代码可能更清楚这一点。

    2) 在与会话关联的数据任务甚至有机会完成之前调用使会话无效的原因是什么,更不用说恢复了。数据任务在恢复之前一直处于挂起状态。

    3) 如果您正在递归运行这样的方法,那么我认为指定或至少考虑什么委托队列至关重要,否则将其设置为 nil 会将其默认为串行操作队列。当委托在完成处理程序完成之前调用时会发生什么,在一个无限循环中 - 很可能是一个巨大的堆积。

    --

    我相信这里的主要问题是您正在开始一个新的或取消 NSURLSessionDataTask 之前它有机会完成。看+sessionWithConfiguration:

    (抱歉还不能包含图片,希望在这个答案之后)

    https://developer.apple.com/library/ios/documentation/Foundation/Reference/NSURLSession_class/Introduction/Introduction.html#//apple_ref/occ/clm/NSURLSession/sessionWithConfiguration:

    重点来了……

    重要

    会话对象保持对委托的强引用 直到您的应用明确地使会话无效。如果你不 通过调用 invalidateAndCancel 或 resetWithCompletionHandler: 方法,你的应用会泄漏内存。

    我的建议是……

     //Your code above...
        [task resume];
        [session finishTasksAndInvalidate];
    }
    

    理论上,这应该可以防止任何新会话在完成之前启动,根据描述,“......不能在会话中创建新任务,但现有任务会继续直到完成。在最后一个任务完成并且会话使最后一次委托调用,对委托和回调对象的引用被破坏..."

    我仍然不确定在恢复之前是否使会话无效。

    我希望这会有所帮助。祝你好运。

    【讨论】:

    • 感谢您的详细回答,我真的无法理解这一点。我让你回顾过去的事情只是这将永远执行的事实。在实际实施中,我有一个限制,以确保这最多只能执行 6 次(它仍然会泄漏。)关于你的观点。 1) 在一个块中强引用 self 会导致一个保留循环(参见:bit.ly/1xChWUP) 2) 会话在完成处理程序块的 end 处失效,这意味着任务已完成执行, [task resume] 确实首先被调用。 3)我尝试指定一个委托队列,仍然泄漏
    • 使用仍然泄漏的迭代实现向问题添加了更多示例代码。
    • 阅读关于 __weak self 的 SO 链接。我仍然认为没有必要这样做,您没有对 self 或块进行显式或隐式强引用。创建一个 __weak self 可能会导致 self = nil 在任何进一步的执行发生之前。
    • 感谢您一直以来的帮助!我的印象是我需要创建一个 __weak self 以在再次调用块的父方法时使用,例如[weakSelf performURLCall],但是即使没有这个 __weak 引用,内存仍然会泄漏。要回答您的问题,完成处理程序肯定每次都会执行。将 HTTP 方法显式设置为“GET”也不会阻止泄漏,并且 HTTP 标头仍显示在工具中。仅供参考,我已就此提出 Apple 开发者支持请求,但仍然感谢您的任何想法!
    • 感谢您的所有帮助,请参阅我接受的答案...原来是 iOS 7 中的一个错误(根据 Apple DTS),但是当您努力提供帮助时,就像我一样无论如何要松开它,您可以获得100赏金!
    【解决方案4】:

    我唯一的建议可能是使用@autoreleasepool{} 并将__weak id self 转换为__block id self。我不认为 __block__weak 会做任何不同的事情,但试一试。

    我不确定当以异步和递归方式运行某些东西时,ARC 会发生什么。查看异步递归调用和 ARC 的其他问题,没有任何一致的解决方案。 Take a look here, for example.

    【讨论】:

    • 感谢您的回答,将__weak 换成__block 并没有达到预期的效果,我倾向于同意@orkoden 的观点,即__weak 是这里的最佳选择。其他答案似乎围绕着纯粹的递归块,我的例子不是真的递归,因为我从块内调用块的父方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-19
    • 2015-07-06
    • 2014-06-07
    • 2013-11-20
    相关资源
    最近更新 更多