Swift Calendar.current утечка памяти? - PullRequest
0 голосов
/ 28 августа 2018

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

Простой контроллер вида, подобный этому:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()

        while Calendar.current.component(.year, from: Date()) > 0
        {
            // why does the memory keep increasing?
        }
    }
}

Кажется, причиной утечки памяти.

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

Как мне обойти эту проблему?

Обновление

Цитата из комментариев:

К вашему сведению - ваша проблема не имеет ничего общего с NSCalendar. Ваша проблема в том, что цикл while не позволяет очистить память

Все эти экземпляры Date тоже занимают память

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

while Date() > Date(timeIntervalSince1970: 200)
{
    // no increase of memory here
}

Ответы [ 4 ]

0 голосов
/ 10 сентября 2018

Как уже отмечали другие, проблема заключается в том, что Calendar.current.component(_:from:) скрытно представляет объект автоматического выпуска, объект, который не освобождается до тех пор, пока пул автоматического выпуска не будет удален.

Еще в первые дни подсчета ссылок в коде Objective-C распространенным способом возврата вновь выделенного объекта, который будет автоматически освобожден, когда вызывающий объект покончит с ним, было возвращение объекта "autorelease". Это был объект, который будет освобожден только тогда, когда вы вернетесь к циклу выполнения, который истощит пул автоматического выпуска *1005*. Кроме того, вы можете контролировать свою верхнюю отметку на больших циклах, которые неоднократно создавали объекты автоматического выпуска, добавляя свои собственные пулы автоматического выпуска.

Swift изначально не создает объекты автоматического выпуска, так что эта проблема представляет собой анахронизм Objective-C, с которым мы обычно не сталкиваемся в нашем собственном коде Swift. Но мы должны быть чувствительны к этому всякий раз, когда пишем код, который зацикливается и вызывает API-интерфейсы Cocoa, которые могут использовать объекты автоматического выпуска за кулисами, как в этом случае.

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

Хитрость в том, что нам нужно поместить autoreleasepool внутри цикла. В следующем примере мы используем тот факт, что autoreleasepool является универсальным, который возвращает все, что возвращается внутри его замыкания:

while autoreleasepool(invoking: { Calendar.current.component(.minute, from: Date()) }) == previousValue {
    // do something
}

Обратите внимание: если вам кажется, что этот шаблон труден для чтения (требуется некоторое время, чтобы привыкнуть к замыканиям в качестве параметров методов), вы можете использовать цикл repeat - until, чтобы выполнить в основном то же самое:

var minute: Int!
repeat {
    minute = autoreleasepool {
        Calendar.current.component(.minute, from: Date())
    }

    // do something
} while minute == previousValue

Кроме того, этот процесс с циклом, который вращается так быстро, очень неэффективен. Конечно, как вы упомянули, вы никогда не сделаете этого в главном потоке (потому что мы никогда не хотим блокировать основной поток). Но вы, как правило, не будете делать это на любом потоке, потому что вращение требует больших вычислительных ресурсов. Иногда вам приходится делать это (например, делать сложные вычисления в фоновом потоке, и вы хотите, чтобы он останавливался в определенное время), но 9 раз из 10 это запах кода для более глубокой проблемы в дизайне. Часто разумное использование таймеров или тому подобного может достичь желаемого эффекта без вычислительных затрат.

Трудно посоветовать вам лучшее решение в вашем случае, так как мы не знаем, какую более широкую проблему вы пытаетесь решить. Но просто имейте в виду, что вращение на нити обычно нежелательно.


Вы спрашиваете:

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

Нет, просто потому, что Date () просто не вводит в микс объекты авто-релиза, как, очевидно, делает Calendar.current.component(_:from:). (Кстати, Apple хорошо справлялась с медленным удалением объектов автоматического высвобождения по всей их базе кода, поэтому вы, вероятно, обнаружите это в будущем, даже если это не потребует автоматического повторного запуска вручную.)

0 голосов
/ 09 сентября 2018

Идея состоит в том, чтобы автоматически высвобождать каждую переменную или функцию, что приводит к пиковому потреблению памяти.

var myDate: Date = Date()

var condition : Bool  {
get{

   return { 
      autoreleasepool{
      Calendar .current.component(.year, from: myDate) > 0
                }
          }() 
   }
}

 while condition {
            autoreleasepool{ 
                myDate = Date(); 
                print (condition) }
}
0 голосов
/ 09 сентября 2018

На первый взгляд, выглядит , как будто проблема может заключаться в том, что ваш цикл while никогда не заканчивается, что может помешать когда-либо автоматически освобожденным объектам иметь возможность быть освобожденными. Или что-то типа того. Чтобы проверить это, я переписал ваш код, используя цикл for, который выполняется всего за миллион итераций. (Оператор print просто немного замедляет процесс, что делает графику лучше.) Вот код:

for i in 1...1000000 //Date() < d
{
    Calendar.current.component(.year, from:Date())
    print("running")
}

Когда я запускаю код таким образом, я получаю граф памяти в Xcode, который выглядит следующим образом:

for loop memory graph

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

let d = Date(timeIntervalSinceNow: 30)
for i in 1...1000000 //Date() < d
{
    Calendar.current.component(.year, from:d)
    print("running")
}

Это дает точно такой же график. Если здесь есть утечка, это не объект Date, который просачивается. Пришло время для более сильного лекарства. Я профилировал тот же код в Инструментах, используя инструменты Распределения и Утечки, и я получил следующие распределения:

memory allocations

Таким образом, создается ровно миллион NSDateComponents объектов, что соответствует количеству итераций цикла. Если есть утечка, то это, вероятно, утечка. Но инструмент Утечки говорит, что никакие объекты не просочились:

leaks graph

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

Обновление:

Я посмотрел на это немного более подробно с помощью инструментов, и на графике видно, что рассматриваемые объекты в конечном итоге выпускаются:

instruments allocations graph

Это подтверждает то, что мы подозревали - Calendar.current.component() создает некоторые автоматически высвобождаемые объекты, которые впоследствии будут освобождены при сливе текущего пула автоматического выпуска. Опять же, здесь нет утечки. Это выглядело только как утечка, потому что цикл в вашем коде никогда не завершался, и у пула автоматического выпуска никогда не было шанса быть очищенным.

Обратите также внимание, что диаграмма памяти в отладочном навигаторе Xcode несколько вводит в заблуждение: она показывает не то, сколько памяти фактически используется, а скорее, сколько памяти в настоящее время выделено вашему процессу. Таким образом, тот факт, что вы не видите его уменьшения, не означает, что ваше приложение по-прежнему использует такого большого объема памяти, но только то, что приложение в настоящее время имеет так много для работы.

0 голосов
/ 06 сентября 2018

Нет утечки памяти. Это связано с тем, что вы злоупотребляете основным потоком: тысячи дорогих NSDateComponents объектов создаются в секунду, каждый занимает 176 байт, что составляет от нескольких секунд до десятков мегабайт. Calendar.current.component(_:from:) является ответственным за все эти распределения, который вызывает _NSCopyOnWriteCalendarWrapper на Calendar.current.

Я подозреваю / думаю, что это связано с тем, как работает ARC: так как в каждом цикле есть ссылка на Calendar.current, поэтому вызывается много копий при записи, а затем получается NSDateComponents из новой даты ( ). Соедините это с ARC, не освобождающим объект, который думает, что он все еще необходим, и у вас есть это

Что касается вашего контрпримера while Date() > Date(timeIntervalSince1970: 200): Date() - это дешево, поскольку в конце дня это число секунд, и сравнение несвязанных Date экземпляров не потребует создания копий при записи.

Решение заключается в использовании autoreleasepool . В ARC Apple утверждает, что разработчикам не нужно освобождать объекты вручную из памяти. Однако в некоторых случаях создание автозапуска может помочь уменьшить пиковый объем памяти приложения. Автозапускы полезны, потому что вместо того, чтобы ждать, пока система освободит все созданные вами объекты, вы говорите системе освободить эти объекты в конце закрытия. В приложении для iOS основная функция выполняется в автозапуске. Операция очистки этого пула будет выполняться в конце каждого основного цикла выполнения. проблема возникает, когда создано больше объектов, чем осушено.

Итак, вы должны использовать autoreleasepool для частей вашего кода, которые занимают много памяти:

autoreleasepool {
    while Calendar.current.component(.year, from: Date()) > 0 {
        print("Hello world")
    }
}

Это замедлит скорость заполнения памяти.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...