使用NSURLSession.downloadTaskWithURL时内存泄漏

时间:2015-01-29 19:50:25

标签: ios swift memory memory-leaks nsurlsession

所以我在与Swift的努力中遇到了另一个障碍。我正在尝试将多个图像加载到图像库中 - 除了一件事以外,一切正常。尽管我清除了图像,但应用程序的内存使用仍在不断增长和增长。在基本上消除了所有代码之后,我发现这是由我的图像加载脚本引起的:

func loadImageWithIndex(index: Int) {
    let imageURL = promotions[index].imageURL
    let url = NSURL(string: imageURL)!
    let urlSession = NSURLSession.sharedSession()
    let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in

    })
    query.resume()
}

正如您所看到的,这段代码基本上没有任何功能。然而,每次我调用它,我的应用程序内存使用量都在增长。如果我注释掉查询,则内存使用量不会改变。

我已经阅读了几个类似的问题但他们都涉及使用委托。那么,在这种情况下,没有委托,但存在内存问题。有人知道如何消除它以及导致它的原因吗?

编辑:这是一个完整的测试课程。似乎只有在可以加载图像时内存才会增长,就像指向图像的指针永远保存在内存中一样。当找不到图像时,没有任何反应,内存使用率保持低水平。也许有人暗示如何清理这些指针?

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        //memory usage: approx. 23MB with 1 load according to the debug navigator
        //loadImage()

        //memory usage approx 130MB with the cycle below according to the debug navigator
        for i in 1...50 {
            loadImage()
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

    func loadImage() {
        let imageURL = "http://mama-beach.com/mama2/wp-content/uploads/2013/07/photodune-4088822-beauty-beach-and-limestone-rocks-l.jpg" //random image from the internet
        let url = NSURL(string: imageURL)!
        let urlSession = NSURLSession.sharedSession()
        let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in
            //there is nothing in here
        })
        query.resume()
    }
}

对不起,我还不知道如何使用探查器(在整个iOS爵士乐中非常棒),至少我会附上上面代码生成的探查器截图: Profiler

4 个答案:

答案 0 :(得分:29)

首先,您必须在会话中致电invalidateAndCancelfinishTasksAndInvalidate ...否则,热潮,内存泄漏。

Apple在旁边框中管理会话部分中的 NSURLSession类参考状态:

  

重要   ---会话对象保留对委托的强引用,直到您的应用程序退出或显式使会话无效。如果你不   使会话无效,您的应用程序会泄漏内存,直到它退出。

所以是的。

您可能还会考虑以下两种方法:

  • flushWithCompletionHandler:
  

将Cookie和凭据刷新到磁盘,清除瞬态缓存,以及   确保将来的请求发生在新的TCP连接上。

  • resetWithCompletionHandler:
  

清空所有Cookie,缓存和凭据存储,删除磁盘文件,   将正在进行的下载刷新到磁盘,并确保未来   请求发生在新套接字上。

...直接引用上述 NSURLSession类参考。

我还应该注意你的会话配置会产生影响:

- (void)setupSession {
    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
    config.URLCache = nil;
    config.timeoutIntervalForRequest = 20.0;
    config.URLCredentialStorage = nil;
    config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
    self.defaultSession = [NSURLSession sessionWithConfiguration:config];
    config = nil;
}

如果您没有使用[NSURLSession sharedSession]单例,那么一个键就是拥有自己的自定义单例NSObject子类,该子类将会话作为属性。这样,您的会话对象就会被重用。每个会话都将创建an SSL cache associated to your app, which takes 10 minutes to clear,如果为每个请求为新会话分配新内存,那么无论您是invalidateAndCancel还是URLCache,您都会看到来自SSL缓存的无限内存增长持续10分钟刷新/重置会话。

这是因为安全框架私有地管理SSL缓存,但会为您的应用程序收取锁定的内存。无论您是否将配置的backgroundSessionConfiguration属性设置为nil,都会发生这种情况。

例如,如果您习惯于说,每秒100个不同的Web请求,并且每个请求使用一个新的NSURLSession对象,那么您每秒创建400k的SSL缓存之类的东西。 (我观察到每个新会话都负责appx.4k的安全框架分配。)10分钟后,这是234兆字节!

因此从Apple获取提示并使用具有NSURLSession属性的单例。

请注意backgroundSessionConfiguration类型保存此SSL缓存内存以及所有其他缓存内存(如NSURLCache)的原因是因为backgroundSession将其处理委托给现在进行会话的系统,而不是您的应用程序,因此即使您的应用未运行,也可能发生这种情况。所以它只是隐藏在你身上......但它就在那里......所以如果那里有巨大的内存增长(甚至是非常重要的话,我也不会让它超过Apple拒绝你的应用程序或终止它的后台会话虽然乐器不会向你展示,但我敢打赌他们可以看到它。

Apple的文档说NSURLRequestReloadIgnoringLocalCacheData URLCache的默认值为零,而不仅仅是零容量。因此,尝试一个短暂的会话或默认会话,然后将其URLCache属性设置为nil,如上例所示。

如果您不打算使用缓存,那么将NSURLRequest对象的cachePolicy属性设置为 override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) { if indexPath.section == 2 && indexPath.row == 0 { myLabel.text = "This is the dynamic text to display" myLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody) myLabel.accessibilityLabel = myLabel.text } if indexPath.section == 1 && indexPath.row == 1 { myOtherLabel.text = "I also want this dynamic text to display" myOtherLabel.font = UIFont.preferredFontForTextStyle(UIFontTextStyleBody) myOtherLabel.accessibilityLabel = myOtherLabel.text } } 也可能是个好主意:D

答案 1 :(得分:9)

我在最近的应用中遇到了类似的问题。

在我的情况下,我从API下载了许多图像。对于每个图像,我创建了一个NSURLSessionDownloadTask并将其添加到具有临时配置的NSURLSession。任务完成后,我调用了一个完成块来处理下载的数据。

我添加的每个下载任务都会导致额外的内存(根据Xcode调试器)在之后的任何时候都没有释放。下载大约100张图像时,调试器的内存使用量为~600 MB。下载任务越多,分配和未释放的内存就越多。在任何时候我都没有以任何方式显示或使用图像数据,它只是存储在磁盘上。

试图在仪器中诊断问题并不富有成效。仪器没有泄漏,也没有与下载任务或图像数据相对应的分配。由于我的块等中的保留周期没有内存泄漏,只有在调试器中才有内存螺旋。

经过几个小时的试验和错误,包括:

  • 在完成块设置为nil的情况下下载图像(以确保块中没有保留周期)。

  • 注释掉我的代码的各个部分,以确保在调用'[downloadTask resume]'时恰好发生了分配。

  • 将会话的URLCache设置为nil和大小为0的缓存。根据:http://www.drdobbs.com/architecture-and-design/memory-leaks-in-ios-7/240168600

  • 将NSURLSession配置更改为默认配置。

最后,我将NSURLSession配置从短暂类型切换为背景类型。这要求我不使用完成块来处理图像。我改为在委托中处理它们。这解决了我的问题。将下载任务添加到后台NSURLSession导致内存增长几乎为零,因为下载已启动并处理。

我希望我能更好地理解为什么会这样,但遗憾的是我没有。我已经看到这个问题也出现在其他人身上,并且还没有找到更好的解决方案或解释。

答案 2 :(得分:0)

在URLSession恢复其数据任务后,如果您不再需要会话,则可以通过调用   invalidateAndCancel()(取消未完成的任务)  或 finishTasksAndInvalidate() (允许在使对象无效之前完成未完成的任务)。 我认为你没有使URLSession无效。所以这里发生了内存泄漏所以你需要添加上述任何一个函数并检查内存泄漏部分中的工具。你修改过的函数将是这样的

func loadImageWithIndex(index: Int) {
    let imageURL = promotions[index].imageURL
    let url = NSURL(string: imageURL)!
    let urlSession = NSURLSession.sharedSession()
    let query = urlSession.downloadTaskWithURL(url, completionHandler: { location, response, error -> Void in

    })
    query.resume()
    query.finishTasksAndInvalidate()
}

答案 3 :(得分:0)

在我的项目中,这是因为默认的urlCache可能会消耗一些内存。如果要减少内存使用量,请尝试将urlCache设置为0

configuration.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)

我的整个urlSession如下:

private var urlSession: URLSession = {
    let configuration = URLSessionConfiguration.default
    configuration.allowsCellularAccess = true
    configuration.httpShouldSetCookies = true
    configuration.httpShouldUsePipelining = true
    configuration.requestCachePolicy = .useProtocolCachePolicy
    configuration.timeoutIntervalForRequest = 60.0
    configuration.urlCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
    return  URLSession(configuration: configuration)
}()