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

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

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 ответа

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

Ссылка класса NSURLSession от Apple в разделе " Управление сессией " в боковом окне:

ВАЖНО --- Объект сеанса сохраняет строгую ссылку на делегат до тех пор, пока ваше приложение не закроет или явно не сделает недействительным сеанс. Если вы не аннулируете сеанс, ваше приложение будет терять память до тех пор, пока оно не завершится.

Так что да.

Вы могли бы также рассмотреть эти два метода:

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

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

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

Поэтому возьмите пример с Apple и используйте синглтон со свойством NSURLSession.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

В моем проекте это потому, что по умолчанию 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)
}()

После того, как 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()
}
Другие вопросы по тегам