Comments 18
Спасибо за статью! Интересный подход.
А Вы не замеряли, есть ли заметные накладные расходы по производительности при использовании этого подхода?
Насколько часто Вы пользуетесь этими аннотациями? Только для ViewModel или для каких-то более мелких объектов тоже?
Вы оставляете эти проверки в release-версии?
Примерно аналогичный описанному выше функционал предоставляет инструмент Leaks в Instruments. На нём подробно останавливаться не будем.
Но ведь не совсем? Instruments позволяет именно обнаружить утечки, они "подсвечиваются" по ходу работы приложения. Или применительно к SwiftUI этот инструмент работает не очень хорошо? Я в SwiftUI пока не пробовал им пользоваться.
А Вы не замеряли, есть ли заметные накладные расходы по производительности при использовании этого подхода?
Какие-то расходы есть, конечно, но они не сравнимы с последствиями утечки
Насколько часто Вы пользуетесь этими аннотациями? Только для ViewModel или для каких-то более мелких объектов тоже?
В основном – для моделей. Обычно, если "утекает" что-то мелкое внутри модели – сама модель тоже не освобождается. Однако, ограничений нет.
Вы оставляете эти проверки в release-версии?
Нет, в реальном коде проверки закрыты аннотациями #if DEBUG || ADHOC
Но ведь не совсем? Instruments позволяет именно обнаружить утечки, они "подсвечиваются" по ходу работы приложения. Или применительно к SwiftUI этот инструмент работает не очень хорошо? Я в SwiftUI пока не пробовал им пользоваться.
В самом Xcode примерно также, только нужно останавливать выполнение. Однако, не это главное. В описанном варианте утечку может обнаружить QA-инженер, тестирующий на реальном устройстве, а не только разработчик в Xcode/Instruments, когда специально этим озаботится. Разработчик может спокойно писать код, зная, что утечки будут найдены в процессе тестирования.
Deleted, перепутал блоки кода)
Вы могли просто добавить deinit с проверкой в класс LeakingViewModel и таким образом контролировать удаление объекта из памяти.
Если объект LeakingViewModel "утечёт" – его deinit не будет вызван, соответственно, проверка не состоится.
Всё правильно, таким образом вы и узнаете, что есть утечка, потому что deinit не был вызван.
Но в какой момент мы будем считать отсутствие вызова проверки в deinit утечкой?
В тот же момент, когда вы ожидаете вызов метода deinit в вашем примере у классов LeakWatcher и property wrapper.
Мы с FrD1 разные люди, я просто тоже не совсем понял, что Вы имеете в виду) Вы не могли бы пояснить? Что бы Вы написали в deinit
ViewModel-и / ViewController-а? И сделали ли бы Вы какие-нибудь ещё изменения?
Да ничего особенного. Можно просто добавить, например,
deinit {
debugPrint("👍 deinit \(self)")
}
И, соответственно, проконтролировать, что происходит вызов deinit там, где используется этот класс. Если в логах ничего нет, значит есть утечка.
А, я подумал, что Вы подход автора предлагаете доработать) Это-то понятно, я сам так и делаю обычно. Но это неравноценно.
Во-первых, это может сделать только разработчик, потому что только разработчик владеет полной информацией о том, какой метод когда должен быть вызван, какие внутри объекты и т.д. Да и то, когда проект большой, один разработчик знает одну часть, другой другую. Части друг друга они знают, но не досконально. Соответственно, во время тестирования (отделом QA) это делаться не будет. Во-вторых, это "ручной" подход. А это значит, что он муторный, времязатратный и провоцирует ошибки. В случае подхода из статьи же мы можем быть уверены, что если ViewModel (или что-либо, что мы отслеживаем) утечёт – мы об этом узнаем. Причём хоть сейчас, хоть в QA. И мы можем больше об этом не думать.
Так что то, что Вы предлагаете – вполне рабочий подход, имеющий своё место, но подход в статье обладает несомненными плюсами.
Если deinit у классов LeakWatcher и property wrapper не будет вызван из-за утечки, вы точно также об этом никогда не узнаете)
Да, но мы их вручную не создаём. Они не утекут.
Я бы не обольщался) При смешении value и reference type структура View может оказаться в куче и утекшее свойство ссылочного типа будет держать в памяти всю структуру и, соответственно, не вызовет deinit у классов LeakWatcher и property wrapper.
Спасибо, что поделились! Интересно, как часто с таким подходом Вы встречаетесь с ложноположительными срабатываниями? Например, из-за того что SwiftUI может потерять ссылку на StateObject или создать его несколько раз?
На практике, ложноположительные срабатывания могут быть, если модель очень сложная и разветвлённая, со множеством вложенных объектов и зависимостей. Мы заметили, что для того, чтобы полностью её освободить может не хватать дефолтной задержки в одну секунду. Собственно, этим и объясняется наличие опционального параметра задержки у враппера.
В остальных случаях, когда модель по каким-то причинам создаётся не один раз и предыдущий экземпляр не освобождается, мы уже не можем считать, что срабатывание было ложным.
Если посмотреть предложенные ссылки, там всё сводится как раз к такой ситуации. Относительно LazyVStack и им подобным, это – известная проблема. Похожее поведение будет и в UIKit, если модель UITableViewCell создавать внутри самого объекта UITableViewCell.
Для примеров в статье был выбран StateObject из соображений компактности примеров. В архитектуре реального проекта, использование StateObject не представляется оправданным, т.к. это не соответствует понятиям чистой архитектуры и SOLID. Проще говоря, мы не используем StateObject вообще, все модели у нас – ObservableObject, которые создаются однократно, вне скоупа SwiftUI. Соответственно, при переиспользовании View, они получают одну и ту же ссылку на один и тот же объект.
Отслеживание утечек памяти в iOS-приложении со SwiftUI в Runtime