Утечка памяти при использовании NSURLSession.downloadTaskWithURL

Итак, я ударил еще один блокпост в моих начинаниях с Swift. Я пытаюсь загрузить несколько изображений в галерею изображений - все работает отлично, за исключением одной вещи. Использование памяти в приложении продолжает расти и возрастать, несмотря на то, что я очищаю изображения. После того, как я пометил в основном весь код, я узнал, что это вызвано загрузкой моего изображения script:

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

Как вы можете видеть, этот фрагмент кода практически ничего не делает прямо сейчас. Но каждый раз, когда я его называю, растет использование памяти приложений. Если я закомментирую запрос, использование памяти не меняется.

Я прочитал несколько подобных вопросов, но все они связаны с использованием делегата. Ну, в этом случае делегата нет, но проблема с памятью. Кто-нибудь знает, как устранить его и что его вызывает?

EDIT: Вот полный тестовый класс. Похоже, память растет только тогда, когда изображение может быть загружено, например, указатели на изображение будут храниться в памяти навсегда. Когда изображение не было найдено, ничего не происходит, использование памяти остается низким. Может быть, какой-то намек на то, как очистить эти указатели?

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

Ответ 1

Вы должны называть invalidateAndCancel или finishTasksAndInvalidate в сеансе, в первую очередь... или же, бум, утечку памяти.

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], - это для вас собственный пользовательский подкласс класса Singleo NSObject, который имеет сеанс как свойство. Таким образом, ваш объект сеанса повторно используется. Каждый сеанс создаст кэш SSL, связанный с вашим приложением, на удаление которого требуется 10 минут, если вы выделите новую память для нового сеанса с каждым запросом, то вы увидите неограниченный рост памяти из кэшей SSL, которые сохраняются в течение 10 минут независимо от того, вы или нет invalidateAndCancel или flush/reset сеанс.

Это потому, что среда безопасности частным образом управляет кешем SSL, но заряжает ваше приложение для блокировки памяти. Это произойдет, если вы установили свойство конфигурации URLCache равным нулю.

Например, если у вас есть привычка говорить 100 различных веб-запросов в секунду, и каждый из них использует новый объект NSURLSession, то вы создаете что-то вроде 400k кеша SSL в секунду. (Я заметил, что каждый новый сеанс отвечает за appx. 4k распределений инфраструктуры безопасности.) Через 10 минут это 234 мегабайта!

Итак, возьмите реплику от Apple и используйте синглтон с свойством NSURLSession.

Обратите внимание, что причина, по которой тип backgroundSessionConfiguration сохраняет эту кэш-память SSL и всю другую кэш-память, такую ​​как NSURLCache, заключается в том, что backgroundSession делегирует свою обработку системе, которая сейчас делает сеанс, а не ваше приложение, поэтому оно может даже если ваше приложение не работает. Так что он просто скрыт от вас... но он там... поэтому я бы не стал пропустить Apple, чтобы отклонить ваше приложение или прекратить его фоновые сессии, если там будет огромный рост памяти (хотя инструменты не будут показывать его вы, я уверен, они могут это увидеть).

Apple docs заявляет, что backgroundSessionConfiguration имеет значение nil по умолчанию для URLCache, а не только нулевую емкость. Поэтому попробуйте выполнить эфемерную сессию или сеанс по умолчанию, а затем установите для свойства URLCache значение nil, как в моем примере выше.

Вероятно, также неплохо установить свойство cachePolicy объекта NSURLRequest для NSURLRequestReloadIgnoringLocalCacheData также, если у вас не будет кеша: D

Ответ 2

Я столкнулся с аналогичной проблемой в недавнем приложении.

В моей ситуации я загружал несколько изображений из API. Для каждого изображения я создавал NSURLSessionDownloadTask и добавлял его в NSURLSession с эфемерной конфигурацией. Когда задача была завершена, я вызвал блок завершения для обработки загруженных данных.

Каждая загружаемая задача, которую я добавил, вызвала выделение дополнительной памяти (в соответствии с отладчиком Xcode), которая впоследствии не была выпущена. При загрузке приблизительно 100 изображений отладчик имел использование памяти ~ 600 МБ. Чем больше загружаемых задач, тем больше памяти было выделено и не было выпущено. Ни при каких обстоятельствах я не отображал или не использовал данные изображения каким-либо образом, он просто сохранялся на диске.

Попытка диагностировать проблему в инструментах не была плодотворной. У приборов не было никаких утечек, и никаких распределений, соответствующих задачам загрузки или данным изображения. Не было утечек памяти из-за сохранения циклов в моих блоках или т.п., Только в отладчике была спираль памяти.

После нескольких часов проб и ошибок, включая:

  • Загрузка изображений с блоком завершения, установленным в nil (чтобы гарантировать, что у меня не было цикла сохранения в блоке).

  • Комментируя различные части моего кода, чтобы убедиться, что распределения произошли именно тогда, когда был вызван "[downloadTask resume]".

  • Установка URLCache для сеанса как для nil, так и для кеша с размером 0. В соответствии с: http://www.drdobbs.com/architecture-and-design/memory-leaks-in-ios-7/240168600

  • Изменение конфигурации NSURLSession для конфигурации по умолчанию.

Наконец, я переключил свою конфигурацию NSURLSession с эфемерного типа на фоновый. Это потребовало, чтобы я не использовал блок завершения для обработки изображений. Я вместо этого обработал их в делегате. Это решило проблему для меня. Добавление задач загрузки на задний план NSURLSession привело к почти нулевому росту памяти при загрузке и обработке загрузки.

Мне хотелось бы лучше понять, почему это так, но, к сожалению, нет. Я видел, как эта проблема возникает и для других, и еще предстоит найти лучшее решение или объяснение.

Ответ 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

@user3847320 ОТВЕТ должен быть принят! Также Valide для AFNetWorking:

NSURLSessionConfiguration* configurationSession = [NSURLSessionConfiguration backgroundSessionConfiguration:@"backgroundSession"];
_currentSessionManager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:configurationSession];
NSURLSessionDownloadTask * downloadTask = [_currentSessionManager downloadTaskWithRequest:...];