递归/迭代NSURLSessionDataTask导致内存泄漏

时间:2014-09-08 10:08:02

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

我的代码中存在内存泄漏问题,我需要快速连续获取许多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; 
}

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

Image showing instruments reporting memory leaks

进一步更新:

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

- (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:

对于可能在这里找到自己的方式的任何Google员工,iOS 8上仍然会出现这种情况。就我而言,这是iOS中的一个错误。

4 个答案:

答案 0 :(得分:4)

- 2014年9月12日更新

解决方案:等待iOS8。

- 2014年9月10日更新

哇,这正在成为复杂性的第N个维度:P。我希望你能以这种或那种方式在这里快速休息。

我还有其他一些事情要你尝试。

1)你能确定关闭NSZombies吗?在Xcode中,Product-&gt; Scheme-&gt; Edit Scheme ...-&gt;启用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 Headers的引用,如果你没有指定[urlRequest setHTTPMethod:@“GET”]请求缺少一些基本头文件?

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

- 原作9/8/2014

有趣的问题!我有与NSURLSessions相关的麻烦泄漏。绝对是@autoreleasepool {}和其他人是目前为止尝试的好建议......但是!

我担心你要求我们过去的事情可能是罪魁祸首。

首先是几点意见:

1)我不清楚为什么你需要__弱自我。您试图避免的保留周期是什么?除了你的“样本”之外,你在实际使用的代码中可能会更清楚。

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

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

-

我认为这里的主要问题是你在有机会完成之前开始新的或取消NSURLSessionDataTask。看看+ sesssionWithConfiguration:

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

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

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

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

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

答案 1 :(得分:3)

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

<强>更新

这仍然发生在iOS 8.1

答案 2 :(得分:1)

我在NSURLSession中遇到了很多内存问题,我最后通过不为每个请求使用新会话来修复它。会话通常在维基百科上定义为:

  

半永久性互动信息交流

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

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

我做了同样的事情,直到我意识到这是我的困境的根源。我没有发现Apple的文档非常明确,但在我意识到我的方式错误之后,它使文档中的某些内容突然变得更有意义,就像为什么有一个sharedSession单身方便的NSURLSession方法,为什么单词“tasks”在finishTasksAndInvalidate中是复数,为什么称它为“会话”,为什么它有缓存等等(如果它只是一个请求,为什么会这样是一个“会话”,“缓存”会有什么好处?)

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

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

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

如果您讨厌单身人士,您仍然可以将会话用作属性,只需确保在其最后一个所有者被ARC运行时解除分配之前invalidateAndCancelfinishTasksAndInvalidate会话。

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

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

答案 3 :(得分:-1)

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

我不确定在异步和递归地运行某些东西时应该对ARC有什么期望。通过异步递归调用和ARC查看其他问题,没有任何一致的解决方案。 Take a look here, for example.