使用 NSURLSession.downloadTaskWithURL 时的内存泄漏

Posted

技术标签:

【中文标题】使用 NSURLSession.downloadTaskWithURL 时的内存泄漏【英文标题】:Memory leak when using NSURLSession.downloadTaskWithURL 【发布时间】:2015-03-29 04:31:07 【问题描述】:

因此,我在使用 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 爵士乐中非常菜鸟),至少我会附上上面代码生成的分析器的屏幕截图:

【问题讨论】:

您在completionHandler 中使用什么代码?听起来像是一个保留周期正在发生。你可以使用工具来分析内存分配和磁盘的读/写吗? @LouisTur 问题已更新。 将您的代码嵌入到 auroreleasepool 块中。我没有看到仪器报告的任何泄漏。 您在 1.2MB+ 的图像文件上调用此 loadImage 方法 50 次,使用下载任务将结果保存到磁盘。有可能这就是你记忆力猛增的原因。如果只调用一次,它的行为是否相同? @LouisTur 我在这里感到困惑吗?内存信息是可用的 RAM 内存,不是吗?这里的问题不是尖峰,而是它实际上从未下降。我假设如果不访问文件数据,它就会被垃圾收集。但似乎并非如此。如果我调用一次这个方法,当然它只是在内存中产生了一点点差异,没有什么特别的。 【参考方案1】:

您必须在会话中调用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 或刷新/重置会议。

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

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

所以从 Apple 那里得到启发,使用带有 NSURLSession 属性的单例。

请注意,backgroundSessionConfiguration 类型为您保存此 SSL 缓存内存以及所有其他缓存内存(如 NSURLCache)的原因是因为 backgroundSession 将其处理委托给现在创建会话的系统,而不是您的应用程序,因此它可以即使您的应用程序没有运行,也会发生。所以它只是对你隐藏......但它就在那里......所以如果那里有巨大的内存增长,我不会让 Apple 拒绝你的应用程序或终止它的后台会话(即使 Instruments 不会显示它)你,我敢打赌他们能看到)。

Apple 的文档说backgroundSessionConfiguration 的 URLCache 默认值为 nil,而不仅仅是零容量。因此,请尝试临时会话或默认会话,然后将其 URLCache 属性设置为 nil,如我上面的示例所示。

如果您不打算拥有缓存,将 NSURLRequest 对象的 cachePolicy 属性设置为 NSURLRequestReloadIgnoringLocalCacheData 也是一个好主意:D

【讨论】:

如果我们使用单例 URLSession,那么我们无法获得每个请求的委托,对吧?单例 URLSession 将有一个委托。在我的使用中,每个请求(包含在 NSOperation 中)创建一个 URLSession,然后实现它的委托来处理传入的数据、接收的数据等。你将如何使用单例 URLSession 处理这种情况?【参考方案2】:

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

在我的情况下,我从 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 导致在启动和处理下载时内存增长几乎为零。

我希望我能更好地理解为什么会这样,但不幸的是我没有。我也看到其他人也遇到过这个问题,但还没有找到更好的解决方案或解释。

【讨论】:

我的内存使用率也很高,我可以确认从 defaultSessionConfiguration 切换到 backgroundSessionConfigurationWithIdentifier 最终解决了这个问题。 我遇到了同样的问题,这个答案帮助了我。后台配置 + 委托 = 没有内存泄漏。谢谢【参考方案3】:

在 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()

【讨论】:

【参考方案4】:

在我的项目中,这是因为默认的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)
()

【讨论】:

以上是关于使用 NSURLSession.downloadTaskWithURL 时的内存泄漏的主要内容,如果未能解决你的问题,请参考以下文章

在使用加载数据流步骤的猪中,使用(使用 PigStorage)和不使用它有啥区别?

今目标使用教程 今目标任务使用篇

Qt静态编译时使用OpenSSL有三种方式(不使用,动态使用,静态使用,默认是动态使用)

MySQL db 在按日期排序时使用“使用位置;使用临时;使用文件排序”

使用“使用严格”作为“使用强”的备份

Kettle java脚本组件的使用说明(简单使用升级使用)