Источник: Nuances of Programming
Процесс управления памятью может стать фактором, весьма затрудняющим работу в приложениях iOS. В связи с этим нужно уделять пристальное внимание размещенным в памяти object instances и обеспечивать их корректное высвобождение.
Как известно, экземпляры высвобождаются из памяти посредством механизма автоматического подсчета ссылок (Automatic Reference Counting или ARC). В iOS существуют 2 типа таких ссылок: сильные и слабые.
Сильные ссылки — это тип ссылки по умолчанию. Рассмотрим простой пример объявления объекта. Допустим, у нас есть класс Person , и мы объявляем объект следующим образом:
var person = Person()
Здесь мы создаем сильную ссылку на Person . При каждом ее создании число ссылок на объект увеличивается на 1. Что это значит? И зачем нам это нужно? Дело в том, что ARC не будет удалять объект из памяти до тех пор, пока число ссылок на него не приравняется к 0.
Иначе себя ведут слабые ссылки, которые не увеличивают общее число ссылок на объект. Вашему вниманию — очередной пример:
var person = Person()
weak var anotherPerson: Person? = person
Здесь создается слабая ссылка на тот же самый объект person , так что теперь у нас есть сильная ссылка person и слабая — anotherPerson . Это значит, что при наличии 2 ссылок на один и тот же объект общее их число по-прежнему равняется 1, поскольку одна из них — слабая. Также важно знать, что слабые ссылки являются необязательными, поскольку доступ к ним можно получить даже после их удаления из памяти.
Поиск циклов retain и утечек памяти с помощью тестов
Цикл retain возникает, когда 2 объекта имеют сильную ссылку друг на друга. В результате этого они удерживают друг друга в памяти, поскольку число ссылок на них никогда не будет равно 0. Следовательно, наша задача — нарушить этот цикл, позволив ARC освободить область памяти, занятую такими объектами.
Предположим, у нас есть два объекта. Один из них имеет тип HTTPClient и при этом состоит во втором объекте UserProfileLoader .
В данном примере UserProfileLoader содержит метод loadProfile , вызывающий метод get в созданном client типа HTTPClient . Как видно, реализация get в HTTPClientSpy сохраняет замыкание в свойстве completion , что делает его доступным для тестов.
Представленная выше реализация, несомненно, содержит цикл retain , но вот для какой цели и в каком месте так сразу и не скажешь. Попробуем разобраться. UserProfileLoader содержит сильную ссылку на объект HTTPClient в свойстве client . С другой стороны, HTTPClient также имеет сильную ссылку на UserProfileLoader . “Где же она?” — спросите вы, поскольку может показаться, что никакого UserProfileLoader в HTTPClientSpy не существует. Однако он там точно есть. Обратим внимание на метод get . В свойстве completion он хранит полученное замыкание, которое содержит сильную ссылку на объект UserProfileLoader в вызове метода map . Как видно, мы добавили сильную ссылку с помощью явного использования self .
Как правило, ошибки подобного рода встречаются во многих базах кода. Порой они совсем не очевидны, так что никто от них не застрахован. Но мы можем сократить риск их появления с помощью автоматизированных тестов. Как же? Для этого необходимо убедиться, что тестируемая система (System Under Test или SUT) и ее составляющие высвобождаются должным образом.
Начнем с написания первого теста, который задаст нам правильный вектор. В этом примере экземпляр UserProfileLoader будет нашим SUT.
Мы располагаем исходными данными, и теперь нам необходимо где-то сохранить результат загрузки для создания утверждений, поскольку, как вы помните, метод loadProfile завершится замыканием типа (Result) -> Void . Сохраним этот результат в тесте.
В вышеуказанном тесте замыкание загрузки никогда не будет выполнено. Причина в том, что loadProfile вызывает реализацию get . В экземпляре HTTPClientSpy метод get сохраняет полученное замыкание в свойстве. Для получения результата нужно вызвать в тесте этот сохраненный блок completion .
Вот теперь мы можем сделать утверждения с помощью массива capturedResults . Метод loadProfile будет всегда завершаться с успешным результатом, содержащим экземпляр UserProfile . Именно этого нам и нужно ожидать в тесте.
Тест прошел! Гарантирует ли он корректное высвобождение SUT и его составляющих компонентов? Пока нет. Добавим еще утверждений для большей уверенности в высвобождении теста из памяти.
Класс MemoryLeaksTests расширяется из класса XCTestCase , предоставляющего метод addTearDownBlock . Этот метод получает блок типа () -> Void, который будет выполняться каждый раз по завершении тестовой функции. Мы могли бы добавить блок teardown сразу после создания SUT и его составляющих.
Теперь блоки на своих местах. Обратите внимание, что в них мы добавили слабые ссылки на экземпляры sut и client , чтобы обойтись без дальнейшего увеличения их числа. Теперь зеленый тест уже не зеленый, потому что утверждения блоков teardown не срабатывают.
Для устранения цикла retain стоит воздержаться от явного использования self в методе loadProfile . И у нас есть несколько вариантов:
- Применить в замыкании слабую ссылку и добавить блок guard let для развертывания результата.
- Превратить метод map в статический.
- Переместить map в статический метод класса-преобразователя.
В следующем примере мы воспользовались 2 вариантом, чтобы нарушить цикл retain и успешно пройти тест.
Теперь тест проходит, но при этом выглядит немного неряшливо, поэтому стоит провести рефакторинг и немного его почистить. Допустим, нам понадобились дополнительные тестовые функции. Тогда мы бы создали SUT и его компоненты, а также добавили бы для каждого из них блоки tear down , что свидетельствовало бы о повторении.
Для рефакторинга этого теста создадим несколько фабричных методов. Первым станет makeSUT , который вернет SUT и его компоненты.
Вот теперь тест стал более чистым и читаемым. К тому же у нас появилось еще одно дополнительное и большое преимущество. Присмотревшись к фабричному методу makeSUT , вы увидите, что он добавляет блоки tear down . Это значит, что по завершении каждый тест, который создает SUT, используя этот метод, будет автоматически проверять корректное высвобождение данного SUT и его объектов из памяти. Если SUT и его компоненты не будут удалены из памяти, то тест покажет ошибку при вызове makeSUT , поскольку мы передаем в XCTAssertNil параметры file и line .
Данный подход позволит уверенно проводить рефакторинг и послужит гарантией правильного управления памятью.
Читайте также:
Перевод статьи Luis Piura : Find potential memory leaks with automated tests